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

Commit 40957714 authored by Josh Yang's avatar Josh Yang Committed by Android (Google) Code Review
Browse files

Merge "Add TransitionPlayer and UIComponent interface to the origin transition...

Merge "Add TransitionPlayer and UIComponent interface to the origin transition foundation lib." into main
parents 617f5961 5e90eda2
Loading
Loading
Loading
Loading
+374 −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.systemui.animation;

import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.SurfaceControl;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
import android.window.TransitionInfo;
import android.window.TransitionInfo.Change;
import android.window.WindowAnimationState;

import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.wm.shell.shared.TransitionUtil;

import java.util.ArrayList;
import java.util.List;

/**
 * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin
 * and automatically attaches it to the transition leash before the transition starts.
 */
public class OriginRemoteTransition extends IRemoteTransition.Stub {
    private static final String TAG = "OriginRemoteTransition";

    private final Context mContext;
    private final boolean mIsEntry;
    private final UIComponent mOrigin;
    private final TransitionPlayer mPlayer;
    private final long mDuration;
    private final Handler mHandler;

    @Nullable private SurfaceControl.Transaction mStartTransaction;
    @Nullable private IRemoteTransitionFinishedCallback mFinishCallback;
    @Nullable private UIComponent.Transaction mOriginTransaction;
    @Nullable private ValueAnimator mAnimator;
    @Nullable private SurfaceControl mOriginLeash;
    private boolean mCancelled;

    OriginRemoteTransition(
            Context context,
            boolean isEntry,
            UIComponent origin,
            TransitionPlayer player,
            long duration,
            Handler handler) {
        mContext = context;
        mIsEntry = isEntry;
        mOrigin = origin;
        mPlayer = player;
        mDuration = duration;
        mHandler = handler;
    }

    @Override
    public void startAnimation(
            IBinder token,
            TransitionInfo info,
            SurfaceControl.Transaction t,
            IRemoteTransitionFinishedCallback finishCallback) {
        logD("startAnimation - " + info);
        mHandler.post(
                () -> {
                    mStartTransaction = t;
                    mFinishCallback = finishCallback;
                    startAnimationInternal(info);
                });
    }

    @Override
    public void mergeAnimation(
            IBinder transition,
            TransitionInfo info,
            SurfaceControl.Transaction t,
            IBinder mergeTarget,
            IRemoteTransitionFinishedCallback finishCallback) {
        logD("mergeAnimation - " + info);
        mHandler.post(this::cancel);
    }

    @Override
    public void takeOverAnimation(
            IBinder transition,
            TransitionInfo info,
            SurfaceControl.Transaction t,
            IRemoteTransitionFinishedCallback finishCallback,
            WindowAnimationState[] states) {
        logD("takeOverAnimation - " + info);
    }

    @Override
    public void onTransitionConsumed(IBinder transition, boolean aborted) {
        logD("onTransitionConsumed - aborted: " + aborted);
        mHandler.post(this::cancel);
    }

    private void startAnimationInternal(TransitionInfo info) {
        if (!prepareUIs(info)) {
            logE("Unable to prepare UI!");
            finishAnimation(/* finished= */ false);
            return;
        }
        // Notify player that we are starting.
        mPlayer.onStart(info, mStartTransaction, mOrigin, mOriginTransaction);

        // Start the animator.
        mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mAnimator.setDuration(mDuration);
        mAnimator.addListener(
                new AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator a) {}

                    @Override
                    public void onAnimationEnd(Animator a) {
                        finishAnimation(/* finished= */ !mCancelled);
                    }

                    @Override
                    public void onAnimationCancel(Animator a) {
                        mCancelled = true;
                    }

                    @Override
                    public void onAnimationRepeat(Animator a) {}
                });
        mAnimator.addUpdateListener(
                a -> {
                    mPlayer.onProgress((float) a.getAnimatedValue());
                });
        mAnimator.start();
    }

    private boolean prepareUIs(TransitionInfo info) {
        if (info.getRootCount() == 0) {
            logE("prepareUIs: no root leash!");
            return false;
        }
        if (info.getRootCount() > 1) {
            logE("prepareUIs: multi-display transition is not supported yet!");
            return false;
        }
        if (info.getChanges().isEmpty()) {
            logE("prepareUIs: no changes!");
            return false;
        }

        SurfaceControl rootLeash = info.getRoot(0).getLeash();
        int displayId = info.getChanges().get(0).getEndDisplayId();
        Rect displayBounds = getDisplayBounds(displayId);
        float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
        logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds);

        // Create the origin leash and add to the transition root leash.
        mOriginLeash =
                new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build();
        mStartTransaction
                .reparent(mOriginLeash, rootLeash)
                .show(mOriginLeash)
                .setCornerRadius(mOriginLeash, windowRadius)
                .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height());

        // Process surfaces
        List<SurfaceControl> openingSurfaces = new ArrayList<>();
        List<SurfaceControl> closingSurfaces = new ArrayList<>();
        for (Change change : info.getChanges()) {
            int mode = change.getMode();
            SurfaceControl leash = change.getLeash();
            // Reparent leash to the transition root.
            mStartTransaction.reparent(leash, rootLeash);
            if (TransitionUtil.isOpeningMode(mode)) {
                openingSurfaces.add(change.getLeash());
                // For opening surfaces, ending bounds are base bound. Apply corner radius if
                // it's full screen.
                Rect bounds = change.getEndAbsBounds();
                if (displayBounds.equals(bounds)) {
                    mStartTransaction
                            .setCornerRadius(leash, windowRadius)
                            .setWindowCrop(leash, bounds.width(), bounds.height());
                }
            } else if (TransitionUtil.isClosingMode(mode)) {
                closingSurfaces.add(change.getLeash());
                // For closing surfaces, starting bounds are base bounds. Apply corner radius if
                // it's full screen.
                Rect bounds = change.getStartAbsBounds();
                if (displayBounds.equals(bounds)) {
                    mStartTransaction
                            .setCornerRadius(leash, windowRadius)
                            .setWindowCrop(leash, bounds.width(), bounds.height());
                }
            }
        }

        // Set relative order:
        // ----  App1  ----
        // ---- origin ----
        // ----  App2  ----
        if (mIsEntry) {
            mStartTransaction
                    .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1)
                    .setRelativeLayer(
                            openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1);
        } else {
            mStartTransaction
                    .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1)
                    .setRelativeLayer(
                            closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1);
        }

        // Attach origin UIComponent to origin leash.
        mOriginTransaction = mOrigin.newTransaction();
        mOriginTransaction
                .attachToTransitionLeash(
                        mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height())
                .commit();

        // Apply all surface changes.
        mStartTransaction.apply();
        return true;
    }

    private Rect getDisplayBounds(int displayId) {
        DisplayManager dm = mContext.getSystemService(DisplayManager.class);
        DisplayMetrics metrics = new DisplayMetrics();
        dm.getDisplay(displayId).getMetrics(metrics);
        return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels);
    }

    private void finishAnimation(boolean finished) {
        logD("finishAnimation: finished=" + finished);
        if (mAnimator == null) {
            // The transition didn't start. Ensure we apply the start transaction and report
            // finish afterwards.
            mStartTransaction
                    .addTransactionCommittedListener(
                            mContext.getMainExecutor(), this::finishInternal)
                    .apply();
            return;
        }
        mAnimator = null;
        // Notify client that we have ended.
        mPlayer.onEnd(finished);
        // Detach the origin from the transition leash and report finish after it's done.
        mOriginTransaction
                .detachFromTransitionLeash(
                        mOrigin, mContext.getMainExecutor(), this::finishInternal)
                .commit();
    }

    private void finishInternal() {
        logD("finishInternal");
        if (mOriginLeash != null) {
            // Release origin leash.
            mOriginLeash.release();
            mOriginLeash = null;
        }
        try {
            mFinishCallback.onTransitionFinished(null, null);
        } catch (RemoteException e) {
            logE("Unable to report transition finish!", e);
        }
        mStartTransaction = null;
        mOriginTransaction = null;
        mFinishCallback = null;
    }

    private void cancel() {
        if (mAnimator != null) {
            mAnimator.cancel();
        }
    }

    private static void logD(String msg) {
        if (OriginTransitionSession.DEBUG) {
            Log.d(TAG, msg);
        }
    }

    private static void logE(String msg) {
        Log.e(TAG, msg);
    }

    private static void logE(String msg, Throwable e) {
        Log.e(TAG, msg, e);
    }

    private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) {
        List<SurfaceControl> surfaces = new ArrayList<>();
        Rect maxBounds = new Rect();
        for (Change change : info.getChanges()) {
            int mode = change.getMode();
            if (TransitionUtil.isOpeningMode(mode) == isOpening) {
                surfaces.add(change.getLeash());
                Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds();
                maxBounds.union(bounds);
            }
        }
        return new SurfaceUIComponent(
                surfaces,
                /* alpha= */ 1.0f,
                /* visible= */ true,
                /* bounds= */ maxBounds,
                /* baseBounds= */ maxBounds);
    }

    /** An interface that represents an origin transitions. */
    public interface TransitionPlayer {

        /**
         * Called when an origin transition starts. This method exposes the raw {@link
         * TransitionInfo} so that clients can extract more information from it.
         */
        default void onStart(
                TransitionInfo transitionInfo,
                SurfaceControl.Transaction sfTransaction,
                UIComponent origin,
                UIComponent.Transaction uiTransaction) {
            // Wrap transactions.
            Transactions transactions =
                    new Transactions()
                            .registerTransactionForClass(origin.getClass(), uiTransaction)
                            .registerTransactionForClass(
                                    SurfaceUIComponent.class,
                                    new SurfaceUIComponent.Transaction(sfTransaction));
            // Wrap surfaces and start.
            onStart(
                    transactions,
                    origin,
                    wrapSurfaces(transitionInfo, /* isOpening= */ false),
                    wrapSurfaces(transitionInfo, /* isOpening= */ true));
        }

        /**
         * Called when an origin transition starts. This method exposes the opening and closing
         * windows as wrapped {@link UIComponent} to provide simplified interface to clients.
         */
        void onStart(
                UIComponent.Transaction transaction,
                UIComponent origin,
                UIComponent closingApp,
                UIComponent openingApp);

        /** Called to update the transition frame. */
        void onProgress(float progress);

        /** Called when the transition ended. */
        void onEnd(boolean finished);
    }
}
+40 −0
Original line number Diff line number Diff line
@@ -24,11 +24,14 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import android.window.IRemoteTransition;
import android.window.RemoteTransition;

import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer;
import com.android.systemui.animation.shared.IOriginTransitions;

import java.lang.annotation.Retention;
@@ -182,6 +185,7 @@ public class OriginTransitionSession {
        @Nullable private final IOriginTransitions mOriginTransitions;
        @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
        @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
        private Handler mHandler = new Handler(Looper.getMainLooper());
        private String mName;
        @Nullable private Predicate<RemoteTransition> mIntentStarter;

@@ -259,12 +263,48 @@ public class OriginTransitionSession {
            return this;
        }

        /** Add an origin entry transition to the builder. */
        public Builder withEntryTransition(
                UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) {
            mEntryTransitionSupplier =
                    () ->
                            new OriginRemoteTransition(
                                    mContext,
                                    /* isEntry= */ true,
                                    entryOrigin,
                                    entryPlayer,
                                    entryDuration,
                                    mHandler);
            return this;
        }

        /** Add an exit transition to the builder. */
        public Builder withExitTransition(IRemoteTransition transition) {
            mExitTransitionSupplier = () -> transition;
            return this;
        }

        /** Add an origin exit transition to the builder. */
        public Builder withExitTransition(
                UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) {
            mExitTransitionSupplier =
                    () ->
                            new OriginRemoteTransition(
                                    mContext,
                                    /* isEntry= */ false,
                                    exitTarget,
                                    exitPlayer,
                                    exitDuration,
                                    mHandler);
            return this;
        }

        /** Supply a handler where transition callbacks will run. */
        public Builder withHandler(Handler handler) {
            mHandler = handler;
            return this;
        }

        /** Build an {@link OriginTransitionSession}. */
        public OriginTransitionSession build() {
            if (mIntentStarter == null) {
+169 −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.systemui.animation;

import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.SurfaceControl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.Executor;

/** A {@link UIComponent} representing a {@link SurfaceControl}. */
public class SurfaceUIComponent implements UIComponent {
    private final Collection<SurfaceControl> mSurfaces;
    private final Rect mBaseBounds;
    private final float[] mFloat9 = new float[9];

    private float mAlpha;
    private boolean mVisible;
    private Rect mBounds;

    public SurfaceUIComponent(
            SurfaceControl sc, float alpha, boolean visible, Rect bounds, Rect baseBounds) {
        this(Arrays.asList(sc), alpha, visible, bounds, baseBounds);
    }

    public SurfaceUIComponent(
            Collection<SurfaceControl> surfaces,
            float alpha,
            boolean visible,
            Rect bounds,
            Rect baseBounds) {
        mSurfaces = surfaces;
        mAlpha = alpha;
        mVisible = visible;
        mBounds = bounds;
        mBaseBounds = baseBounds;
    }

    @Override
    public float getAlpha() {
        return mAlpha;
    }

    @Override
    public boolean isVisible() {
        return mVisible;
    }

    @Override
    public Rect getBounds() {
        return mBounds;
    }

    @Override
    public Transaction newTransaction() {
        return new Transaction(new SurfaceControl.Transaction());
    }

    @Override
    public String toString() {
        return "SurfaceUIComponent{mSurfaces="
                + mSurfaces
                + ", mAlpha="
                + mAlpha
                + ", mVisible="
                + mVisible
                + ", mBounds="
                + mBounds
                + ", mBaseBounds="
                + mBaseBounds
                + "}";
    }

    /** A {@link Transaction} wrapping a {@link SurfaceControl.Transaction}. */
    public static class Transaction implements UIComponent.Transaction<SurfaceUIComponent> {
        private final SurfaceControl.Transaction mTransaction;
        private final ArrayList<Runnable> mChanges = new ArrayList<>();

        public Transaction(SurfaceControl.Transaction transaction) {
            mTransaction = transaction;
        }

        @Override
        public Transaction setAlpha(SurfaceUIComponent ui, float alpha) {
            mChanges.add(
                    () -> {
                        ui.mAlpha = alpha;
                        ui.mSurfaces.forEach(s -> mTransaction.setAlpha(s, alpha));
                    });
            return this;
        }

        @Override
        public Transaction setVisible(SurfaceUIComponent ui, boolean visible) {
            mChanges.add(
                    () -> {
                        ui.mVisible = visible;
                        if (visible) {
                            ui.mSurfaces.forEach(s -> mTransaction.show(s));
                        } else {
                            ui.mSurfaces.forEach(s -> mTransaction.hide(s));
                        }
                    });
            return this;
        }

        @Override
        public Transaction setBounds(SurfaceUIComponent ui, Rect bounds) {
            mChanges.add(
                    () -> {
                        if (ui.mBounds.equals(bounds)) {
                            return;
                        }
                        ui.mBounds = bounds;
                        Matrix matrix = new Matrix();
                        matrix.setRectToRect(
                                new RectF(ui.mBaseBounds),
                                new RectF(ui.mBounds),
                                Matrix.ScaleToFit.FILL);
                        ui.mSurfaces.forEach(s -> mTransaction.setMatrix(s, matrix, ui.mFloat9));
                    });
            return this;
        }

        @Override
        public Transaction attachToTransitionLeash(
                SurfaceUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
            mChanges.add(
                    () -> ui.mSurfaces.forEach(s -> mTransaction.reparent(s, transitionLeash)));
            return this;
        }

        @Override
        public Transaction detachFromTransitionLeash(
                SurfaceUIComponent ui, Executor executor, Runnable onDone) {
            mChanges.add(
                    () -> {
                        ui.mSurfaces.forEach(s -> mTransaction.reparent(s, null));
                        mTransaction.addTransactionCommittedListener(executor, onDone::run);
                    });
            return this;
        }

        @Override
        public void commit() {
            mChanges.forEach(Runnable::run);
            mChanges.clear();
            mTransaction.apply();
        }
    }
}
+86 −0

File added.

Preview size limit exceeded, changes collapsed.

+72 −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.systemui.animation;

import android.annotation.FloatRange;
import android.graphics.Rect;
import android.view.SurfaceControl;

import java.util.concurrent.Executor;

/** An interface representing an UI component on the display. */
public interface UIComponent {

    /** Get the current alpha of this UI. */
    float getAlpha();

    /** Check if this UI is visible. */
    boolean isVisible();

    /** Get the bounds of this UI in its display. */
    Rect getBounds();

    /** Create a new {@link Transaction} that can update this UI. */
    Transaction newTransaction();

    /**
     * A transaction class for updating {@link UIComponent}.
     *
     * @param <T> the subtype of {@link UIComponent} that this {@link Transaction} can handle.
     */
    interface Transaction<T extends UIComponent> {
        /** Update alpha of an UI. Execution will be delayed until {@link #commit()} is called. */
        Transaction setAlpha(T ui, @FloatRange(from = 0.0, to = 1.0) float alpha);

        /**
         * Update visibility of an UI. Execution will be delayed until {@link #commit()} is called.
         */
        Transaction setVisible(T ui, boolean visible);

        /** Update bounds of an UI. Execution will be delayed until {@link #commit()} is called. */
        Transaction setBounds(T ui, Rect bounds);

        /**
         * Attach a ui to the transition leash. Execution will be delayed until {@link #commit()} is
         * called.
         */
        Transaction attachToTransitionLeash(T ui, SurfaceControl transitionLeash, int w, int h);

        /**
         * Detach a ui from the transition leash. Execution will be delayed until {@link #commit} is
         * called.
         */
        Transaction detachFromTransitionLeash(T ui, Executor executor, Runnable onDone);

        /** Commit any pending changes added to this transaction. */
        void commit();
    }
}
Loading