Remote animations (app-controlled animations)

Adds the ability for another app to control an entire app
transition. It does so by creating an ActivityOptions object that
contains a RemoteAnimationAdapter object that describes how the
animation should be run: Along of some meta-data, this object
contains a callback that gets invoked from WM when the transition
is ready to be started.

Window manager supplies a list of RemoteAnimationApps into the
callback. Each app contains information about the app as well as
the animation leash. The controlling app can modify the leash like
any other surface, including the possibility to synchronize
updating the leash's surface properties with a frame to be drawn
using the Transaction.deferUntil API.

When the animation is done, the app can invoke the finished
callback to get WM out of the animating state, which will also
clean up any closing apps.

We use a timeout of 2000ms such that a buggy controlling app can
not break window manager forever (duration subject to change).

Test: go/wm-smoke
Test: RemoteAnimationControllerTest

Bug: 64674361
Change-Id: I34e0c9a91b28badebac74896f95c6390f1b947ab
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index b74c8da..8d82996 100644
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -21,6 +21,7 @@
 import static android.app.ActivityManager.TaskDescription.ATTR_TASKDESCRIPTION_PREFIX;
 import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
 import static android.app.ActivityOptions.ANIM_CUSTOM;
+import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION;
 import static android.app.ActivityOptions.ANIM_SCALE_UP;
 import static android.app.ActivityOptions.ANIM_SCENE_TRANSITION;
 import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS;
@@ -1481,6 +1482,10 @@
                 case ANIM_OPEN_CROSS_PROFILE_APPS:
                     service.mWindowManager.overridePendingAppTransitionStartCrossProfileApps();
                     break;
+                case ANIM_REMOTE_ANIMATION:
+                    service.mWindowManager.overridePendingAppTransitionRemote(
+                            pendingOptions.getRemoteAnimationAdapter());
+                    break;
                 default:
                     Slog.e(TAG, "applyOptionsLocked: Unknown animationType=" + animationType);
                     break;
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index b567303..16c2d2d 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import static android.Manifest.permission.ACTIVITY_EMBEDDING;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
 import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
 import static android.Manifest.permission.START_ANY_ACTIVITY;
 import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
@@ -162,6 +163,7 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
+import android.view.RemoteAnimationAdapter;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.ReferrerIntent;
@@ -1680,6 +1682,18 @@
                 Slog.w(TAG, msg);
                 throw new SecurityException(msg);
             }
+
+            // Check permission for remote animations
+            final RemoteAnimationAdapter adapter = options.getRemoteAnimationAdapter();
+            if (adapter != null && mService.checkPermission(
+                    CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, callingPid, callingUid)
+                            != PERMISSION_GRANTED) {
+                final String msg = "Permission Denial: starting " + intent.toString()
+                        + " from " + callerApp + " (pid=" + callingPid
+                        + ", uid=" + callingUid + ") with remoteAnimationAdapter";
+                Slog.w(TAG, msg);
+                throw new SecurityException(msg);
+            }
         }
 
         return true;
diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java
index d4b437a..f129fe1 100644
--- a/services/core/java/com/android/server/wm/AppTransition.java
+++ b/services/core/java/com/android/server/wm/AppTransition.java
@@ -75,6 +75,7 @@
 import android.view.AppTransitionAnimationSpec;
 import android.view.DisplayListCanvas;
 import android.view.IAppTransitionAnimationSpecsFuture;
+import android.view.RemoteAnimationAdapter;
 import android.view.RenderNode;
 import android.view.ThreadedRenderer;
 import android.view.WindowManager;
@@ -213,6 +214,8 @@
      * }.
      */
     private static final int NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS = 9;
+    private static final int NEXT_TRANSIT_TYPE_REMOTE = 10;
+
     private int mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE;
 
     // These are the possible states for the enter/exit activities during a thumbnail transition
@@ -275,6 +278,8 @@
     private final boolean mGridLayoutRecentsEnabled;
     private final boolean mLowRamRecentsEnabled;
 
+    private RemoteAnimationController mRemoteAnimationController;
+
     AppTransition(Context context, WindowManagerService service) {
         mContext = context;
         mService = service;
@@ -454,6 +459,9 @@
                 app.startDelayingAnimationStart();
             }
         }
+        if (mRemoteAnimationController != null) {
+            mRemoteAnimationController.goodToGo();
+        }
         return redoLayout;
     }
 
@@ -468,6 +476,7 @@
         mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE;
         mNextAppTransitionPackage = null;
         mNextAppTransitionAnimationsSpecs.clear();
+        mRemoteAnimationController = null;
         mNextAppTransitionAnimationsSpecsFuture = null;
         mDefaultNextAppTransitionAnimationSpec = null;
         mAnimationFinishedCallback = null;
@@ -1551,6 +1560,10 @@
                 && mNextAppTransition != TRANSIT_KEYGUARD_GOING_AWAY;
     }
 
+    RemoteAnimationController getRemoteAnimationController() {
+        return mRemoteAnimationController;
+    }
+
     /**
      *
      * @param frame These are the bounds of the window when it finishes the animation. This is where
@@ -1788,7 +1801,7 @@
 
     void overridePendingAppTransition(String packageName, int enterAnim, int exitAnim,
             IRemoteCallback startedCallback) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM;
             mNextAppTransitionPackage = packageName;
@@ -1796,14 +1809,12 @@
             mNextAppTransitionExit = exitAnim;
             postAnimationCallback();
             mNextAppTransitionCallback = startedCallback;
-        } else {
-            postAnimationCallback();
         }
     }
 
     void overridePendingAppTransitionScaleUp(int startX, int startY, int startWidth,
             int startHeight) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_SCALE_UP;
             putDefaultNextAppTransitionCoordinates(startX, startY, startWidth, startHeight, null);
@@ -1813,7 +1824,7 @@
 
     void overridePendingAppTransitionClipReveal(int startX, int startY,
                                                 int startWidth, int startHeight) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_CLIP_REVEAL;
             putDefaultNextAppTransitionCoordinates(startX, startY, startWidth, startHeight, null);
@@ -1823,7 +1834,7 @@
 
     void overridePendingAppTransitionThumb(GraphicBuffer srcThumb, int startX, int startY,
                                            IRemoteCallback startedCallback, boolean scaleUp) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP
                     : NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN;
@@ -1831,14 +1842,12 @@
             putDefaultNextAppTransitionCoordinates(startX, startY, 0, 0, srcThumb);
             postAnimationCallback();
             mNextAppTransitionCallback = startedCallback;
-        } else {
-            postAnimationCallback();
         }
     }
 
     void overridePendingAppTransitionAspectScaledThumb(GraphicBuffer srcThumb, int startX, int startY,
             int targetWidth, int targetHeight, IRemoteCallback startedCallback, boolean scaleUp) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP
                     : NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
@@ -1847,15 +1856,13 @@
                     srcThumb);
             postAnimationCallback();
             mNextAppTransitionCallback = startedCallback;
-        } else {
-            postAnimationCallback();
         }
     }
 
-    public void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs,
+    void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs,
             IRemoteCallback onAnimationStartedCallback, IRemoteCallback onAnimationFinishedCallback,
             boolean scaleUp) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP
                     : NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
@@ -1878,15 +1885,13 @@
             postAnimationCallback();
             mNextAppTransitionCallback = onAnimationStartedCallback;
             mAnimationFinishedCallback = onAnimationFinishedCallback;
-        } else {
-            postAnimationCallback();
         }
     }
 
     void overridePendingAppTransitionMultiThumbFuture(
             IAppTransitionAnimationSpecsFuture specsFuture, IRemoteCallback callback,
             boolean scaleUp) {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP
                     : NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
@@ -1896,14 +1901,21 @@
         }
     }
 
-    void overrideInPlaceAppTransition(String packageName, int anim) {
+    void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter) {
         if (isTransitionSet()) {
             clear();
+            mNextAppTransitionType = NEXT_TRANSIT_TYPE_REMOTE;
+            mRemoteAnimationController = new RemoteAnimationController(mService,
+                    remoteAnimationAdapter, mService.mH);
+        }
+    }
+
+    void overrideInPlaceAppTransition(String packageName, int anim) {
+        if (canOverridePendingAppTransition()) {
+            clear();
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE;
             mNextAppTransitionPackage = packageName;
             mNextAppTransitionInPlace = anim;
-        } else {
-            postAnimationCallback();
         }
     }
 
@@ -1911,13 +1923,18 @@
      * @see {@link #NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS}
      */
     void overridePendingAppTransitionStartCrossProfileApps() {
-        if (isTransitionSet()) {
+        if (canOverridePendingAppTransition()) {
             clear();
             mNextAppTransitionType = NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS;
             postAnimationCallback();
         }
     }
 
+    private boolean canOverridePendingAppTransition() {
+        // Remote animations always take precedence
+        return isTransitionSet() &&  mNextAppTransitionType != NEXT_TRANSIT_TYPE_REMOTE;
+    }
+
     /**
      * If a future is set for the app transition specs, fetch it in another thread.
      */
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index 0dfef3a..5466db6 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -1585,27 +1585,37 @@
         // different animation is running.
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "AWT#applyAnimationLocked");
         if (okToAnimate()) {
-            final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
-            if (a != null) {
-                final TaskStack stack = getStack();
-                mTmpPoint.set(0, 0);
-                mTmpRect.setEmpty();
-                if (stack != null) {
-                    stack.getRelativePosition(mTmpPoint);
-                    stack.getBounds(mTmpRect);
-                    mTmpRect.offsetTo(0, 0);
+            final AnimationAdapter adapter;
+            final TaskStack stack = getStack();
+            mTmpPoint.set(0, 0);
+            mTmpRect.setEmpty();
+            if (stack != null) {
+                stack.getRelativePosition(mTmpPoint);
+                stack.getBounds(mTmpRect);
+                mTmpRect.offsetTo(0, 0);
+            }
+            if (mService.mAppTransition.getRemoteAnimationController() != null) {
+                adapter = mService.mAppTransition.getRemoteAnimationController()
+                        .createAnimationAdapter(this, mTmpPoint, mTmpRect);
+            } else {
+                final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
+                if (a != null) {
+                    adapter = new LocalAnimationAdapter(
+                            new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
+                                    mService.mAppTransition.canSkipFirstFrame(),
+                                    mService.mAppTransition.getAppStackClipMode()),
+                            mService.mSurfaceAnimationRunner);
+                    if (a.getZAdjustment() == Animation.ZORDER_TOP) {
+                        mNeedsZBoost = true;
+                    }
+                    mTransit = transit;
+                    mTransitFlags = mService.mAppTransition.getTransitFlags();
+                } else {
+                    adapter = null;
                 }
-                final AnimationAdapter adapter = new LocalAnimationAdapter(
-                        new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
-                                mService.mAppTransition.canSkipFirstFrame(),
-                                mService.mAppTransition.getAppStackClipMode()),
-                        mService.mSurfaceAnimationRunner);
-                if (a.getZAdjustment() == Animation.ZORDER_TOP) {
-                    mNeedsZBoost = true;
-                }
+            }
+            if (adapter != null) {
                 startAnimation(getPendingTransaction(), adapter, !isVisible());
-                mTransit = transit;
-                mTransitFlags = mService.mAppTransition.getTransitFlags();
             }
         } else {
             cancelAnimation();
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
new file mode 100644
index 0000000..688b4ff
--- /dev/null
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2018 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.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationFinishedCallback.Stub;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import java.util.ArrayList;
+
+/**
+ * Helper class to run app animations in a remote process.
+ */
+class RemoteAnimationController {
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "RemoteAnimationController" : TAG_WM;
+    private static final long TIMEOUT_MS = 2000;
+
+    private final WindowManagerService mService;
+    private final RemoteAnimationAdapter mRemoteAnimationAdapter;
+    private final ArrayList<RemoteAnimationAdapterWrapper> mPendingAnimations = new ArrayList<>();
+    private final Rect mTmpRect = new Rect();
+    private final Handler mHandler;
+
+    private final IRemoteAnimationFinishedCallback mFinishedCallback = new Stub() {
+        @Override
+        public void onAnimationFinished() throws RemoteException {
+            RemoteAnimationController.this.onAnimationFinished();
+        }
+    };
+
+    private final Runnable mTimeoutRunnable = () -> {
+        onAnimationFinished();
+        invokeAnimationCancelled();
+    };
+
+    RemoteAnimationController(WindowManagerService service,
+            RemoteAnimationAdapter remoteAnimationAdapter, Handler handler) {
+        mService = service;
+        mRemoteAnimationAdapter = remoteAnimationAdapter;
+        mHandler = handler;
+    }
+
+    /**
+     * Creates an animation for each individual {@link AppWindowToken}.
+     *
+     * @param appWindowToken The app to animate.
+     * @param position The position app bounds, in screen coordinates.
+     * @param stackBounds The stack bounds of the app.
+     * @return The adapter to be run on the app.
+     */
+    AnimationAdapter createAnimationAdapter(AppWindowToken appWindowToken, Point position,
+            Rect stackBounds) {
+        final RemoteAnimationAdapterWrapper adapter = new RemoteAnimationAdapterWrapper(
+                appWindowToken, position, stackBounds);
+        mPendingAnimations.add(adapter);
+        return adapter;
+    }
+
+    /**
+     * Called when the transition is ready to be started, and all leashes have been set up.
+     */
+    void goodToGo() {
+        mHandler.postDelayed(mTimeoutRunnable, TIMEOUT_MS);
+        try {
+            mRemoteAnimationAdapter.getRunner().onAnimationStart(createAnimations(),
+                    mFinishedCallback);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to start remote animation", e);
+            onAnimationFinished();
+        }
+    }
+
+    private RemoteAnimationTarget[] createAnimations() {
+        final RemoteAnimationTarget[] result = new RemoteAnimationTarget[mPendingAnimations.size()];
+        for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+            result[i] = mPendingAnimations.get(i).createRemoteAppAnimation();
+        }
+        return result;
+    }
+
+    private void onAnimationFinished() {
+        mHandler.removeCallbacks(mTimeoutRunnable);
+        synchronized (mService.mWindowMap) {
+            mService.openSurfaceTransaction();
+            try {
+                for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+                    final RemoteAnimationAdapterWrapper adapter = mPendingAnimations.get(i);
+                    adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+                }
+            } finally {
+                mService.closeSurfaceTransaction("RemoteAnimationController#finished");
+            }
+        }
+    }
+
+    private void invokeAnimationCancelled() {
+        try {
+            mRemoteAnimationAdapter.getRunner().onAnimationCancelled();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to notify cancel", e);
+        }
+    }
+
+    private class RemoteAnimationAdapterWrapper implements AnimationAdapter {
+
+        private final AppWindowToken mAppWindowToken;
+        private SurfaceControl mCapturedLeash;
+        private OnAnimationFinishedCallback mCapturedFinishCallback;
+        private final Point mPosition = new Point();
+        private final Rect mStackBounds = new Rect();
+
+        RemoteAnimationAdapterWrapper(AppWindowToken appWindowToken, Point position,
+                Rect stackBounds) {
+            mAppWindowToken = appWindowToken;
+            mPosition.set(position.x, position.y);
+            mStackBounds.set(stackBounds);
+        }
+
+        RemoteAnimationTarget createRemoteAppAnimation() {
+            return new RemoteAnimationTarget(mAppWindowToken.getTask().mTaskId, getMode(),
+                    mCapturedLeash, !mAppWindowToken.fillsParent(),
+                    mAppWindowToken.findMainWindow().mWinAnimator.mLastClipRect,
+                    mAppWindowToken.getPrefixOrderIndex(), mPosition, mStackBounds);
+        }
+
+        private int getMode() {
+            if (mService.mOpeningApps.contains(mAppWindowToken)) {
+                return RemoteAnimationTarget.MODE_OPENING;
+            } else {
+                return RemoteAnimationTarget.MODE_CLOSING;
+            }
+        }
+
+        @Override
+        public boolean getDetachWallpaper() {
+            return false;
+        }
+
+        @Override
+        public int getBackgroundColor() {
+            return 0;
+        }
+
+        @Override
+        public void startAnimation(SurfaceControl animationLeash, Transaction t,
+                OnAnimationFinishedCallback finishCallback) {
+
+            // Restore z-layering, position and stack crop until client has a chance to modify it.
+            t.setLayer(animationLeash, mAppWindowToken.getPrefixOrderIndex());
+            t.setPosition(animationLeash, mPosition.x, mPosition.y);
+            mTmpRect.set(mStackBounds);
+            mTmpRect.offsetTo(0, 0);
+            t.setWindowCrop(animationLeash, mTmpRect);
+            mCapturedLeash = animationLeash;
+            mCapturedFinishCallback = finishCallback;
+        }
+
+        @Override
+        public void onAnimationCancelled(SurfaceControl animationLeash) {
+            mPendingAnimations.remove(this);
+            if (mPendingAnimations.isEmpty()) {
+                mHandler.removeCallbacks(mTimeoutRunnable);
+                invokeAnimationCancelled();
+            }
+        }
+
+        @Override
+        public long getDurationHint() {
+            return mRemoteAnimationAdapter.getDuration();
+        }
+
+        @Override
+        public long getStatusBarTransitionsStartTime() {
+            return SystemClock.uptimeMillis()
+                    + mRemoteAnimationAdapter.getStatusBarTransitionDelay();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index fcc9988..d7b07f7 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
 import static android.Manifest.permission.MANAGE_APP_TOKENS;
 import static android.Manifest.permission.READ_FRAME_BUFFER;
 import static android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS;
@@ -210,6 +211,7 @@
 import android.view.MagnificationSpec;
 import android.view.MotionEvent;
 import android.view.PointerIcon;
+import android.view.RemoteAnimationAdapter;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Builder;
@@ -2624,6 +2626,18 @@
     }
 
     @Override
+    public void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter) {
+        if (!checkCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
+                "overridePendingAppTransitionRemote()")) {
+            throw new SecurityException(
+                    "Requires CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS permission");
+        }
+        synchronized (mWindowMap) {
+            mAppTransition.overridePendingAppTransitionRemote(remoteAnimationAdapter);
+        }
+    }
+
+    @Override
     public void endProlongedAnimations() {
         synchronized (mWindowMap) {
             for (final WindowState win : mWindowMap.values()) {
diff --git a/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java
new file mode 100644
index 0000000..897be34
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/RemoteAnimationControllerTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 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.server.wm;
+
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Postsubmit;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.server.testutils.OffsettableClock;
+import com.android.server.testutils.TestHandler;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * atest FrameworksServicesTests:com.android.server.wm.RemoteAnimationControllerTest
+ */
+@SmallTest
+@FlakyTest(detail = "Promote to presubmit if non-flakyness is established")
+@RunWith(AndroidJUnit4.class)
+public class RemoteAnimationControllerTest extends WindowTestsBase {
+
+    @Mock SurfaceControl mMockLeash;
+    @Mock Transaction mMockTransaction;
+    @Mock OnAnimationFinishedCallback mFinishedCallback;
+    @Mock IRemoteAnimationRunner mMockRunner;
+    private RemoteAnimationAdapter mAdapter;
+    private RemoteAnimationController mController;
+    private final OffsettableClock mClock = new OffsettableClock.Stopped();
+    private TestHandler mHandler;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mAdapter = new RemoteAnimationAdapter(mMockRunner, 100, 50);
+        sWm.mH.runWithScissors(() -> {
+            mHandler = new TestHandler(null, mClock);
+        }, 0);
+        mController = new RemoteAnimationController(sWm, mAdapter, mHandler);
+    }
+
+    @Test
+    public void testRun() throws Exception {
+        final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+        sWm.mOpeningApps.add(win.mAppToken);
+        try {
+            final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+                    new Point(50, 100), new Rect(50, 100, 150, 150));
+            adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+            mController.goodToGo();
+
+            final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor =
+                    ArgumentCaptor.forClass(RemoteAnimationTarget[].class);
+            final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor =
+                    ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class);
+            verify(mMockRunner).onAnimationStart(appsCaptor.capture(), finishedCaptor.capture());
+            assertEquals(1, appsCaptor.getValue().length);
+            final RemoteAnimationTarget app = appsCaptor.getValue()[0];
+            assertEquals(new Point(50, 100), app.position);
+            assertEquals(new Rect(50, 100, 150, 150), app.sourceContainerBounds);
+            assertEquals(win.mAppToken.getPrefixOrderIndex(), app.prefixOrderIndex);
+            assertEquals(win.mAppToken.getTask().mTaskId, app.taskId);
+            assertEquals(mMockLeash, app.leash);
+            assertEquals(win.mWinAnimator.mLastClipRect, app.clipRect);
+            assertEquals(false, app.isTranslucent);
+            verify(mMockTransaction).setLayer(mMockLeash, app.prefixOrderIndex);
+            verify(mMockTransaction).setPosition(mMockLeash, app.position.x, app.position.y);
+            verify(mMockTransaction).setWindowCrop(mMockLeash, new Rect(0, 0, 100, 50));
+
+            finishedCaptor.getValue().onAnimationFinished();
+            verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+        } finally {
+            sWm.mOpeningApps.clear();
+        }
+    }
+
+    @Test
+    public void testCancel() throws Exception {
+        final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+        final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+                new Point(50, 100), new Rect(50, 100, 150, 150));
+        adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+        mController.goodToGo();
+
+        adapter.onAnimationCancelled(mMockLeash);
+        verify(mMockRunner).onAnimationCancelled();
+    }
+
+    @Test
+    public void testTimeout() throws Exception {
+        final WindowState win = createWindow(null /* parent */, TYPE_BASE_APPLICATION, "testWin");
+        final AnimationAdapter adapter = mController.createAnimationAdapter(win.mAppToken,
+                new Point(50, 100), new Rect(50, 100, 150, 150));
+        adapter.startAnimation(mMockLeash, mMockTransaction, mFinishedCallback);
+        mController.goodToGo();
+
+        mClock.fastForward(2500);
+        mHandler.timeAdvance();
+
+        verify(mMockRunner).onAnimationCancelled();
+        verify(mFinishedCallback).onAnimationFinished(eq(adapter));
+    }
+}