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

Commit 30f3544f authored by Evan Rosky's avatar Evan Rosky
Browse files

Add support for remote transition playback in shell transitions

This adds IRemoteTransition interface for other processes (like
launcher) to register for playing transition animations.

Then there are 2 supported ways to "register":

1. Is to attach an IRemoteTransition to ActivityOptions. To
   support this pathway, requestStartTransition now takes a
   TransitionRequest object which can contain whatever info
   is needed at the time. One piece of that is the passed-in
   IRemoteTransition.

2. Is to register directly with shell (via systemuiproxy for
   now) with a TransitionFilter. This filter allows shell
   transitions to dispatch a transition to a remote without
   need to make queries over binder.

This also hooks up launcher animations by converting the new
transition info to the old RemoteAnimationTargetCompat stuff.

Bug: 169035082
Test: launch app from launcher and observe the icon animation.
      atest ShellTransitionTests
Change-Id: I8014a6f46cffda3a48a1a4ae101e2643078f4a14
parent 58679dbe
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@ import android.view.RemoteAnimationAdapter;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.window.IRemoteTransition;
import android.window.WindowContainerToken;

import java.lang.annotation.Retention;
@@ -298,6 +299,8 @@ public class ActivityOptions {
    private static final String KEY_SPECS_FUTURE = "android:activity.specsFuture";
    private static final String KEY_REMOTE_ANIMATION_ADAPTER
            = "android:activity.remoteAnimationAdapter";
    private static final String KEY_REMOTE_TRANSITION =
            "android:activity.remoteTransition";

    /**
     * @see #setLaunchCookie
@@ -380,6 +383,7 @@ public class ActivityOptions {
    private IAppTransitionAnimationSpecsFuture mSpecsFuture;
    private RemoteAnimationAdapter mRemoteAnimationAdapter;
    private IBinder mLaunchCookie;
    private IRemoteTransition mRemoteTransition;

    /**
     * Create an ActivityOptions specifying a custom animation to run when
@@ -959,6 +963,21 @@ public class ActivityOptions {
        return opts;
    }

    /**
     * Create an {@link ActivityOptions} instance that lets the application control the entire
     * animation using a {@link RemoteAnimationAdapter}.
     * @hide
     */
    @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
    public static ActivityOptions makeRemoteAnimation(RemoteAnimationAdapter remoteAnimationAdapter,
            IRemoteTransition remoteTransition) {
        final ActivityOptions opts = new ActivityOptions();
        opts.mRemoteAnimationAdapter = remoteAnimationAdapter;
        opts.mAnimationType = ANIM_REMOTE_ANIMATION;
        opts.mRemoteTransition = remoteTransition;
        return opts;
    }

    /** @hide */
    public boolean getLaunchTaskBehind() {
        return mAnimationType == ANIM_LAUNCH_TASK_BEHIND;
@@ -1064,6 +1083,8 @@ public class ActivityOptions {
        }
        mRemoteAnimationAdapter = opts.getParcelable(KEY_REMOTE_ANIMATION_ADAPTER);
        mLaunchCookie = opts.getBinder(KEY_LAUNCH_COOKIE);
        mRemoteTransition = IRemoteTransition.Stub.asInterface(opts.getBinder(
                KEY_REMOTE_TRANSITION));
    }

    /**
@@ -1222,6 +1243,11 @@ public class ActivityOptions {
        mRemoteAnimationAdapter = remoteAnimationAdapter;
    }

    /** @hide */
    public IRemoteTransition getRemoteTransition() {
        return mRemoteTransition;
    }

    /** @hide */
    public static ActivityOptions fromBundle(Bundle bOptions) {
        return bOptions != null ? new ActivityOptions(bOptions) : null;
@@ -1724,6 +1750,9 @@ public class ActivityOptions {
        if (mLaunchCookie != null) {
            b.putBinder(KEY_LAUNCH_COOKIE, mLaunchCookie);
        }
        if (mRemoteTransition != null) {
            b.putBinder(KEY_REMOTE_TRANSITION, mRemoteTransition.asBinder());
        }
        return b;
    }

+46 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.window;

import android.view.IRemoteAnimationFinishedCallback;
import android.view.SurfaceControl;
import android.window.TransitionInfo;

/**
 * Interface allowing remote processes to play transition animations.
 * The usage flow is as follows:
 * <p><ol>
 *  <li>The remote tags a lifecycle event with an IRemoteTransition (via a parameter in
 *      ActivityOptions#makeRemoteAnimation) or a transition matches a filter registered via
 *      Transitions#registerRemote.
 *  <li>Shell then associates the transition for the event with the IRemoteTransition
 *  <li>Shell receives onTransitionReady and delegates the animation to the IRemoteTransition
 *      via {@link #startAnimation}.
 *  <li>Once the IRemoteTransition is done animating, it will call the finishCallback.
 *  <li>Shell/Core finish-up the transition.
 * </ul>
 *
 * {@hide}
 */
oneway interface IRemoteTransition {
    /**
     * Starts a transition animation. Once complete, the implementation should call
     * `finishCallback`.
     */
    void startAnimation(in TransitionInfo info, in SurfaceControl.Transaction t,
            in IRemoteAnimationFinishedCallback finishCallback);
}
+3 −7
Original line number Diff line number Diff line
@@ -16,10 +16,9 @@

package android.window;

import android.app.ActivityManager;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import android.window.WindowContainerTransaction;
import android.window.TransitionRequestInfo;

/**
 * Implemented by WMShell to initiate and play transition animations.
@@ -56,12 +55,9 @@ oneway interface ITransitionPlayer {
     * Called when something in WMCore requires a transition to play -- for example when an Activity
     * is started in a new Task.
     *
     * @param type The {@link WindowManager#TransitionType} of the transition to start.
     * @param transitionToken An identifying token for the transition that needs to be started.
     *                        Pass this to {@link IWindowOrganizerController#startTransition}.
     * @param triggerTask If non-null, the task containing the activity whose lifecycle change
     *                    (start or finish) has caused this transition to occur.
     * @param request Information about this particular request.
     */
    void requestStartTransition(int type, in IBinder transitionToken,
            in ActivityManager.RunningTaskInfo triggerTask);
    void requestStartTransition(in IBinder transitionToken, in TransitionRequestInfo request);
}
+19 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.window;

parcelable TransitionFilter;
+215 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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 android.window;

import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
import android.os.Parcel;
import android.os.Parcelable;

/**
 * A parcelable filter that can be used for rerouting transitions to a remote. This is a local
 * representation so that the transition system doesn't need to make blocking queries over
 * binder.
 *
 * @hide
 */
public final class TransitionFilter implements Parcelable {

    /**
     * When non-null: this is a list of transition types that this filter applies to. This filter
     * will fail for transitions that aren't one of these types.
     */
    @Nullable public int[] mTypeSet = null;

    /**
     * A list of required changes. To pass, a transition must meet all requirements.
     */
    @Nullable public Requirement[] mRequirements = null;

    public TransitionFilter() {
    }

    private TransitionFilter(Parcel in) {
        mTypeSet = in.createIntArray();
        mRequirements = in.createTypedArray(Requirement.CREATOR);
    }

    /** @return true if `info` meets all the requirements to pass this filter. */
    public boolean matches(@NonNull TransitionInfo info) {
        if (mTypeSet != null) {
            // non-null typeset, so make sure info is one of the types.
            boolean typePass = false;
            for (int i = 0; i < mTypeSet.length; ++i) {
                if (info.getType() == mTypeSet[i]) {
                    typePass = true;
                    break;
                }
            }
            if (!typePass) return false;
        }
        // Make sure info meets all of the requirements.
        if (mRequirements != null) {
            for (int i = 0; i < mRequirements.length; ++i) {
                if (!mRequirements[i].matches(info)) return false;
            }
        }
        return true;
    }

    @Override
    /** @hide */
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeIntArray(mTypeSet);
        dest.writeTypedArray(mRequirements, flags);
    }

    @NonNull
    public static final Creator<TransitionFilter> CREATOR =
            new Creator<TransitionFilter>() {
                @Override
                public TransitionFilter createFromParcel(Parcel in) {
                    return new TransitionFilter(in);
                }

                @Override
                public TransitionFilter[] newArray(int size) {
                    return new TransitionFilter[size];
                }
            };

    @Override
    /** @hide */
    public int describeContents() {
        return 0;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("{types=[");
        if (mTypeSet != null) {
            for (int i = 0; i < mTypeSet.length; ++i) {
                sb.append((i == 0 ? "" : ",") + mTypeSet[i]);
            }
        }
        sb.append("] checks=[");
        if (mRequirements != null) {
            for (int i = 0; i < mRequirements.length; ++i) {
                sb.append((i == 0 ? "" : ",") + mRequirements[i]);
            }
        }
        return sb.append("]}").toString();
    }

    /**
     * Matches a change that a transition must contain to pass this filter. All requirements in a
     * filter must be met to pass the filter.
     */
    public static final class Requirement implements Parcelable {
        public int mActivityType = ACTIVITY_TYPE_UNDEFINED;
        public int[] mModes = null;

        public Requirement() {
        }

        private Requirement(Parcel in) {
            mActivityType = in.readInt();
            mModes = in.createIntArray();
        }

        /** Go through changes and find if at-least one change matches this filter */
        boolean matches(@NonNull TransitionInfo info) {
            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                final TransitionInfo.Change change = info.getChanges().get(i);
                if (change.getParent() != null) {
                    // Only look at the top animating windows.
                    continue;
                }
                if (mActivityType != ACTIVITY_TYPE_UNDEFINED) {
                    if (change.getTaskInfo() == null
                            || change.getTaskInfo().getActivityType() != mActivityType) {
                        continue;
                    }
                }
                if (mModes != null) {
                    boolean pass = false;
                    for (int m = 0; m < mModes.length; ++m) {
                        if (mModes[m] == change.getMode()) {
                            pass = true;
                            break;
                        }
                    }
                    if (!pass) continue;
                }
                return true;
            }
            return false;
        }

        /** Check if the request matches this filter. It may generate false positives */
        boolean matches(@NonNull TransitionRequestInfo request) {
            // Can't check modes since the transition hasn't been built at this point.
            if (mActivityType == ACTIVITY_TYPE_UNDEFINED) return true;
            return request.getTriggerTask() != null
                    && request.getTriggerTask().getActivityType() == mActivityType;
        }

        @Override
        /** @hide */
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            dest.writeInt(mActivityType);
            dest.writeIntArray(mModes);
        }

        @NonNull
        public static final Creator<Requirement> CREATOR =
                new Creator<Requirement>() {
                    @Override
                    public Requirement createFromParcel(Parcel in) {
                        return new Requirement(in);
                    }

                    @Override
                    public Requirement[] newArray(int size) {
                        return new Requirement[size];
                    }
                };

        @Override
        /** @hide */
        public int describeContents() {
            return 0;
        }

        @Override
        public String toString() {
            StringBuilder out = new StringBuilder();
            out.append("{atype=" + WindowConfiguration.activityTypeToString(mActivityType));
            out.append(" modes=[");
            if (mModes != null) {
                for (int i = 0; i < mModes.length; ++i) {
                    out.append((i == 0 ? "" : ",") + TransitionInfo.modeToString(mModes[i]));
                }
            }
            return out.append("]}").toString();
        }
    }
}
Loading