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

Commit 5e90eda2 authored by Josh Yang's avatar Josh Yang
Browse files

Add TransitionPlayer and UIComponent interface to the origin transition

foundation lib.

Also adds a OriginRemoteTransition wrapper class that wraps a
TransitionPlayer and an UIComponent to handle surface preparation and
clean-up.

This change also provides default UIComponent implementation wrapping
View and SurfaceControl.

Flag: com.google.android.clockwork.systemui.flags.transitions_enable_origin_transitions_backend
Bug: 347060315
Test: manual test
Change-Id: Id51a23c2739d030a0377de3b60b51cb161e07176
parent e94b365d
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