Introduce a new IOriginTransitions.aidl interface for handling origin
return transitions.

The interface allows an app to link a launching transition with a return transition.
And the APIs are protected with permission
android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS.

Flag: com.google.android.clockwork.systemui.flags.transitions_enable_origin_transitions_backend
Bug: 347060315
Test: manual test using test app
Change-Id: Iaffb16fcb39a7a8fbd49bd752af71521c468270f
diff --git a/packages/SystemUI/animation/lib/Android.bp b/packages/SystemUI/animation/lib/Android.bp
new file mode 100644
index 0000000..4324d463
--- /dev/null
+++ b/packages/SystemUI/animation/lib/Android.bp
@@ -0,0 +1,41 @@
+// 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 {
+    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+java_library {
+    name: "PlatformAnimationLib-server",
+    srcs: [
+        "src/com/android/systemui/animation/server/*.java",
+        ":PlatformAnimationLib-aidl",
+    ],
+    static_libs: [
+        "WindowManager-Shell-shared",
+    ],
+}
+
+filegroup {
+    name: "PlatformAnimationLib-aidl",
+    srcs: [
+        "src/**/*.aidl",
+    ],
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/server/IOriginTransitionsImpl.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/server/IOriginTransitionsImpl.java
new file mode 100644
index 0000000..3cbb688
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/server/IOriginTransitionsImpl.java
@@ -0,0 +1,424 @@
+/*
+ * 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.server;
+
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.app.TaskInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionFilter;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.Change;
+import android.window.WindowAnimationState;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.systemui.animation.shared.IOriginTransitions;
+import com.android.wm.shell.shared.ShellTransitions;
+import com.android.wm.shell.shared.TransitionUtil;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.function.Predicate;
+
+/** An implementation of the {@link IOriginTransitions}. */
+public class IOriginTransitionsImpl extends IOriginTransitions.Stub {
+    private static final boolean DEBUG = true;
+    private static final String TAG = "OriginTransitions";
+
+    private final Object mLock = new Object();
+    private final ShellTransitions mShellTransitions;
+    private final Context mContext;
+
+    @GuardedBy("mLock")
+    private final Map<IBinder, OriginTransitionRecord> mRecords = new ArrayMap<>();
+
+    public IOriginTransitionsImpl(Context context, ShellTransitions shellTransitions) {
+        mShellTransitions = shellTransitions;
+        mContext = context;
+    }
+
+    @Override
+    public RemoteTransition makeOriginTransition(
+            RemoteTransition launchTransition, RemoteTransition returnTransition)
+            throws RemoteException {
+        if (DEBUG) {
+            Log.d(
+                    TAG,
+                    "makeOriginTransition: (" + launchTransition + ", " + returnTransition + ")");
+        }
+        enforceRemoteTransitionPermission();
+        synchronized (mLock) {
+            OriginTransitionRecord record =
+                    new OriginTransitionRecord(launchTransition, returnTransition);
+            mRecords.put(record.getToken(), record);
+            return record.asLaunchableTransition();
+        }
+    }
+
+    @Override
+    public void cancelOriginTransition(RemoteTransition originTransition) {
+        if (DEBUG) {
+            Log.d(TAG, "cancelOriginTransition: " + originTransition);
+        }
+        enforceRemoteTransitionPermission();
+        synchronized (mLock) {
+            if (!mRecords.containsKey(originTransition.asBinder())) {
+                return;
+            }
+            mRecords.get(originTransition.asBinder()).destroy();
+        }
+    }
+
+    private void enforceRemoteTransitionPermission() {
+        mContext.enforceCallingPermission(
+                Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
+                "Missing permission "
+                        + Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS);
+    }
+
+    public void dump(IndentingPrintWriter ipw) {
+        ipw.println("IOriginTransitionsImpl");
+        ipw.println("Active records:");
+        ipw.increaseIndent();
+        synchronized (mLock) {
+            if (mRecords.isEmpty()) {
+                ipw.println("none");
+            } else {
+                for (OriginTransitionRecord record : mRecords.values()) {
+                    record.dump(ipw);
+                }
+            }
+        }
+        ipw.decreaseIndent();
+    }
+
+    /**
+     * An {@link IRemoteTransition} that delegates animation to another {@link IRemoteTransition}
+     * and notify callbacks when the transition starts.
+     */
+    private static class RemoteTransitionDelegate extends IRemoteTransition.Stub {
+        private final IRemoteTransition mTransition;
+        private final Predicate<TransitionInfo> mOnStarting;
+        private final Executor mExecutor;
+
+        RemoteTransitionDelegate(
+                Executor executor,
+                IRemoteTransition transition,
+                Predicate<TransitionInfo> onStarting) {
+            mExecutor = executor;
+            mTransition = transition;
+            mOnStarting = onStarting;
+        }
+
+        @Override
+        public void startAnimation(
+                IBinder token,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishCallback)
+                throws RemoteException {
+            if (DEBUG) {
+                Log.d(TAG, "startAnimation: " + info);
+            }
+            if (!mOnStarting.test(info)) {
+                Log.w(TAG, "Skipping cancelled transition " + mTransition);
+                t.addTransactionCommittedListener(
+                                mExecutor,
+                                () -> {
+                                    try {
+                                        finishCallback.onTransitionFinished(null, null);
+                                    } catch (RemoteException e) {
+                                        Log.e(TAG, "Unable to report finish.", e);
+                                    }
+                                })
+                        .apply();
+                return;
+            }
+            mTransition.startAnimation(token, info, t, finishCallback);
+        }
+
+        @Override
+        public void mergeAnimation(
+                IBinder transition,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IBinder mergeTarget,
+                IRemoteTransitionFinishedCallback finishCallback)
+                throws RemoteException {
+            if (DEBUG) {
+                Log.d(TAG, "mergeAnimation: " + info);
+            }
+            mTransition.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+        }
+
+        @Override
+        public void takeOverAnimation(
+                IBinder transition,
+                TransitionInfo info,
+                SurfaceControl.Transaction t,
+                IRemoteTransitionFinishedCallback finishCallback,
+                WindowAnimationState[] states)
+                throws RemoteException {
+            if (DEBUG) {
+                Log.d(TAG, "takeOverAnimation: " + info);
+            }
+            mTransition.takeOverAnimation(transition, info, t, finishCallback, states);
+        }
+
+        @Override
+        public void onTransitionConsumed(IBinder transition, boolean aborted)
+                throws RemoteException {
+            if (DEBUG) {
+                Log.d(TAG, "onTransitionConsumed: aborted=" + aborted);
+            }
+            mTransition.onTransitionConsumed(transition, aborted);
+        }
+
+        @Override
+        public String toString() {
+            return "RemoteTransitionDelegate{transition=" + mTransition + "}";
+        }
+    }
+
+    /** A data record containing the origin transition pieces. */
+    private class OriginTransitionRecord implements IBinder.DeathRecipient {
+        private final RemoteTransition mWrappedLaunchTransition;
+        private final RemoteTransition mWrappedReturnTransition;
+
+        @GuardedBy("mLock")
+        private boolean mDestroyed;
+
+        OriginTransitionRecord(RemoteTransition launchTransition, RemoteTransition returnTransition)
+                throws RemoteException {
+            mWrappedLaunchTransition = wrap(launchTransition, this::onLaunchTransitionStarting);
+            mWrappedReturnTransition = wrap(returnTransition, this::onReturnTransitionStarting);
+            linkToDeath();
+        }
+
+        private boolean onLaunchTransitionStarting(TransitionInfo info) {
+            synchronized (mLock) {
+                if (mDestroyed) {
+                    return false;
+                }
+                TransitionFilter filter = createFilterForReverseTransition(info);
+                if (filter != null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Registering filter " + filter);
+                    }
+                    mShellTransitions.registerRemote(filter, mWrappedReturnTransition);
+                }
+                return true;
+            }
+        }
+
+        private boolean onReturnTransitionStarting(TransitionInfo info) {
+            synchronized (mLock) {
+                if (mDestroyed) {
+                    return false;
+                }
+                // Clean up stuff.
+                destroy();
+                return true;
+            }
+        }
+
+        public void destroy() {
+            synchronized (mLock) {
+                if (mDestroyed) {
+                    // Already destroyed.
+                    return;
+                }
+                if (DEBUG) {
+                    Log.d(TAG, "Destroying origin transition record " + this);
+                }
+                mDestroyed = true;
+                unlinkToDeath();
+                mShellTransitions.unregisterRemote(mWrappedReturnTransition);
+                mRecords.remove(getToken());
+            }
+        }
+
+        private void linkToDeath() throws RemoteException {
+            asDelegate(mWrappedLaunchTransition).mTransition.asBinder().linkToDeath(this, 0);
+            asDelegate(mWrappedReturnTransition).mTransition.asBinder().linkToDeath(this, 0);
+        }
+
+        private void unlinkToDeath() {
+            asDelegate(mWrappedLaunchTransition).mTransition.asBinder().unlinkToDeath(this, 0);
+            asDelegate(mWrappedReturnTransition).mTransition.asBinder().unlinkToDeath(this, 0);
+        }
+
+        public IBinder getToken() {
+            return asLaunchableTransition().asBinder();
+        }
+
+        public RemoteTransition asLaunchableTransition() {
+            return mWrappedLaunchTransition;
+        }
+
+        @Override
+        public void binderDied() {
+            destroy();
+        }
+
+        @Override
+        public String toString() {
+            return "OriginTransitionRecord{launch="
+                    + mWrappedReturnTransition
+                    + ", return="
+                    + mWrappedReturnTransition
+                    + "}";
+        }
+
+        public void dump(IndentingPrintWriter ipw) {
+            synchronized (mLock) {
+                ipw.println("OriginTransitionRecord");
+                ipw.increaseIndent();
+                ipw.println("mDestroyed: " + mDestroyed);
+                ipw.println("Launch transition:");
+                ipw.increaseIndent();
+                ipw.println(mWrappedLaunchTransition);
+                ipw.decreaseIndent();
+                ipw.println("Return transition:");
+                ipw.increaseIndent();
+                ipw.println(mWrappedReturnTransition);
+                ipw.decreaseIndent();
+                ipw.decreaseIndent();
+            }
+        }
+
+        private static RemoteTransitionDelegate asDelegate(RemoteTransition transition) {
+            return (RemoteTransitionDelegate) transition.getRemoteTransition();
+        }
+
+        private RemoteTransition wrap(
+                RemoteTransition transition, Predicate<TransitionInfo> onStarting) {
+            return new RemoteTransition(
+                    new RemoteTransitionDelegate(
+                            mContext.getMainExecutor(),
+                            transition.getRemoteTransition(),
+                            onStarting),
+                    transition.getDebugName());
+        }
+
+        @Nullable
+        private static TransitionFilter createFilterForReverseTransition(TransitionInfo info) {
+            TaskInfo launchingTaskInfo = null;
+            TaskInfo launchedTaskInfo = null;
+            ComponentName launchingActivity = null;
+            ComponentName launchedActivity = null;
+            for (Change change : info.getChanges()) {
+                int mode = change.getMode();
+                TaskInfo taskInfo = change.getTaskInfo();
+                ComponentName activity = change.getActivityComponent();
+                if (TransitionUtil.isClosingMode(mode)
+                        && launchingTaskInfo == null
+                        && taskInfo != null) {
+                    // Found the launching task!
+                    launchingTaskInfo = taskInfo;
+                } else if (TransitionUtil.isOpeningMode(mode)
+                        && launchedTaskInfo == null
+                        && taskInfo != null) {
+                    // Found the launched task!
+                    launchedTaskInfo = taskInfo;
+                } else if (TransitionUtil.isClosingMode(mode)
+                        && launchingActivity == null
+                        && activity != null) {
+                    // Found the launching activity
+                    launchingActivity = activity;
+                } else if (TransitionUtil.isOpeningMode(mode)
+                        && launchedActivity == null
+                        && activity != null) {
+                    // Found the launched activity!
+                    launchedActivity = activity;
+                }
+            }
+            if (DEBUG) {
+                Log.d(
+                        TAG,
+                        "createFilterForReverseTransition: launchingTaskInfo="
+                                + launchingTaskInfo
+                                + ", launchedTaskInfo="
+                                + launchedTaskInfo
+                                + ", launchingActivity="
+                                + launchedActivity
+                                + ", launchedActivity="
+                                + launchedActivity);
+            }
+            if (launchingTaskInfo == null && launchingActivity == null) {
+                Log.w(
+                        TAG,
+                        "createFilterForReverseTransition: unable to find launching task or"
+                                + " launching activity!");
+                return null;
+            }
+            if (launchedTaskInfo == null && launchedActivity == null) {
+                Log.w(
+                        TAG,
+                        "createFilterForReverseTransition: unable to find launched task or launched"
+                                + " activity!");
+                return null;
+            }
+            if (launchedTaskInfo != null && launchedTaskInfo.launchCookies.isEmpty()) {
+                Log.w(
+                        TAG,
+                        "createFilterForReverseTransition: skipped - launched task has no launch"
+                                + " cookie!");
+                return null;
+            }
+            TransitionFilter filter = new TransitionFilter();
+            filter.mTypeSet = new int[] {TRANSIT_CLOSE, TRANSIT_TO_BACK};
+
+            // The opening activity of the return transition must match the activity we just closed.
+            TransitionFilter.Requirement req1 = new TransitionFilter.Requirement();
+            req1.mModes = new int[] {TRANSIT_OPEN, TRANSIT_TO_FRONT};
+            req1.mTopActivity =
+                    launchingActivity == null ? launchingTaskInfo.topActivity : launchingActivity;
+
+            TransitionFilter.Requirement req2 = new TransitionFilter.Requirement();
+            req2.mModes = new int[] {TRANSIT_CLOSE, TRANSIT_TO_BACK};
+            if (launchedTaskInfo != null) {
+                // For task transitions, the closing task's cookie must match the task we just
+                // launched.
+                req2.mLaunchCookie = launchedTaskInfo.launchCookies.get(0);
+            } else {
+                // For activity transitions, the closing activity of the return transition must
+                // match
+                // the activity we just launched.
+                req2.mTopActivity = launchedActivity;
+            }
+
+            filter.mRequirements = new TransitionFilter.Requirement[] {req1, req2};
+            return filter;
+        }
+    }
+}
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/shared/IOriginTransitions.aidl b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/shared/IOriginTransitions.aidl
new file mode 100644
index 0000000..31cca70
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/shared/IOriginTransitions.aidl
@@ -0,0 +1,44 @@
+/*
+ * 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.shared;
+
+import android.window.RemoteTransition;
+
+/**
+ * An interface for an app to link a launch transition and a return transition together into an
+ * origin transition.
+ */
+interface IOriginTransitions {
+
+    /**
+     * Create a new "origin transition" which wraps a launch transition and a return transition.
+     * The returned {@link RemoteTransition} is expected to be passed to
+     * {@link ActivityOptions#makeRemoteTransition(RemoteTransition)} to create an
+     * {@link ActivityOptions} and being used to launch an intent. When being used with
+     * {@link ActivityOptions}, the launch transition will be triggered for launching the intent,
+     * and the return transition will be remembered and triggered for returning from the launched
+     * activity.
+     */
+    RemoteTransition makeOriginTransition(in RemoteTransition launchTransition,
+            in RemoteTransition returnTransition) = 1;
+
+    /**
+     * Cancels an origin transition. Any parts not yet played will no longer be triggered, and the
+     * origin transition object will reset to a single frame animation.
+     */
+    void cancelOriginTransition(in RemoteTransition originTransition) = 2;
+}