Fixing issue with wrong transition when tasks are offscreen.

- Fixing issue with wrong animation specs being created for tasks that
  are offscreen
- Removing unused code and simplifying the logic for task launching, also
  pulling this out into another file so that it is easier to read and
  understand
- Removing old incorrect call to moveTaskToFront() instead of
  startActivityFromRecents()

Bug: 25590404

Change-Id: I25d9530d089a7984fb8c94954a34dd124420755a
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index a58bc58..bf5417d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -386,8 +386,8 @@
      * Handle screen pinning request.
      */
     public final void onBusEvent(final ScreenPinningRequestEvent event) {
-        int processUser = event.systemServicesProxy.getProcessUser();
-        if (event.systemServicesProxy.isSystemUser(processUser)) {
+        int processUser = sSystemServicesProxy.getProcessUser();
+        if (sSystemServicesProxy.isSystemUser(processUser)) {
             mImpl.onStartScreenPinning(event.applicationContext);
         } else {
             postToSystemUser(new Runnable() {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index 6874247..58f7124 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -46,6 +46,8 @@
 import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.activity.IterateRecentsEvent;
+import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
+import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
 import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
 import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
@@ -108,7 +110,7 @@
     DozeTrigger mIterateTrigger = new DozeTrigger(500, new Runnable() {
         @Override
         public void run() {
-            boolean dismissed = dismissRecentsToFocusedTask(false);
+            dismissRecentsToFocusedTask(false);
         }
     });
 
@@ -569,12 +571,6 @@
     /**** RecentsView.RecentsViewCallbacks Implementation ****/
 
     @Override
-    public void onTaskLaunchFailed() {
-        // Return to Home
-        dismissRecentsToHome(true);
-    }
-
-    @Override
     public void onAllTaskViewsDismissed() {
         mFinishLaunchHomeRunnable.run();
     }
@@ -701,6 +697,17 @@
         setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_BEHIND);
     }
 
+    public final void onBusEvent(LaunchTaskSucceededEvent event) {
+        MetricsLogger.histogram(this, "overview_task_launch_index", event.taskIndexFromStackFront);
+    }
+
+    public final void onBusEvent(LaunchTaskFailedEvent event) {
+        // Return to Home
+        dismissRecentsToHome(true);
+
+        MetricsLogger.count(this, "overview_task_launch_failed", 1);
+    }
+
     public final void onBusEvent(ScreenPinningRequestEvent event) {
         MetricsLogger.count(this, "overview_screen_pinned", 1);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index db65e00..4059543 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -503,12 +503,7 @@
         MetricsLogger.count(mContext, "overview_affiliated_task_launch", 1);
 
         // Launch the task
-        if (toTask.isActive) {
-            // Bring an active task to the foreground
-            ssp.moveTaskToFront(toTask.key.id, launchOpts);
-        } else {
-            ssp.startActivityFromRecents(mContext, toTask.key.id, toTask.activityLabel, launchOpts);
-        }
+        ssp.startActivityFromRecents(mContext, toTask.key.id, toTask.activityLabel, launchOpts);
     }
 
     public void showNextAffiliatedTask() {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskFailedEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskFailedEvent.java
new file mode 100644
index 0000000..3a2d58c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskFailedEvent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2015 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.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * This is sent when we fail to launch a task.
+ */
+public class LaunchTaskFailedEvent extends EventBus.Event {
+    // Simple event
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskSucceededEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskSucceededEvent.java
new file mode 100644
index 0000000..ec5089f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/LaunchTaskSucceededEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 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.recents.events.activity;
+
+import com.android.systemui.recents.events.EventBus;
+
+/**
+ * This is sent when we successfully launch a task.
+ */
+public class LaunchTaskSucceededEvent extends EventBus.Event {
+
+    public final int taskIndexFromStackFront;
+
+    public LaunchTaskSucceededEvent(int taskIndexFromStackFront) {
+        this.taskIndexFromStackFront = taskIndexFromStackFront;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/component/ScreenPinningRequestEvent.java b/packages/SystemUI/src/com/android/systemui/recents/events/component/ScreenPinningRequestEvent.java
index 5cb4ccf..9d96d8e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/events/component/ScreenPinningRequestEvent.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/component/ScreenPinningRequestEvent.java
@@ -26,10 +26,8 @@
 public class ScreenPinningRequestEvent extends EventBus.Event {
 
     public final Context applicationContext;
-    public final SystemServicesProxy systemServicesProxy;
 
-    public ScreenPinningRequestEvent(Context context, SystemServicesProxy systemServicesProxy) {
+    public ScreenPinningRequestEvent(Context context) {
         this.applicationContext = context.getApplicationContext();
-        this.systemServicesProxy = systemServicesProxy;
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/ForegroundThread.java b/packages/SystemUI/src/com/android/systemui/recents/misc/ForegroundThread.java
index 8dc2983..784ac4e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/ForegroundThread.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/ForegroundThread.java
@@ -28,7 +28,7 @@
     private static Handler sHandler;
 
     private ForegroundThread() {
-        super("recents.fg", android.os.Process.THREAD_PRIORITY_BACKGROUND);
+        super("recents.fg");
     }
 
     private static void ensureThreadLocked() {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
index 62493d6..7b04493 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -130,10 +130,9 @@
                     ? t.taskDescription.getIconFilename() : null;
 
             // Add the task to the stack
-            Task task = new Task(taskKey, (t.id != INVALID_TASK_ID), t.affiliatedTaskId,
-                    t.affiliatedTaskColor, activityLabel, contentDescription, activityIcon,
-                    activityColor, (i == (taskCount - 1)), config.lockToAppEnabled, icon,
-                    iconFilename, t.bounds);
+            Task task = new Task(taskKey, t.affiliatedTaskId, t.affiliatedTaskColor, activityLabel,
+                    contentDescription, activityIcon, activityColor, (i == (taskCount - 1)),
+                    config.lockToAppEnabled, icon, iconFilename, t.bounds);
             task.thumbnail = loader.getAndUpdateThumbnail(taskKey, ssp, false);
             if (DEBUG) {
                 Log.d(TAG, activityLabel + " bounds: " + t.bounds);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
index 12bd556b..67e18f3 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -103,7 +103,6 @@
     public int colorPrimary;
     public boolean useLightOnPrimaryColor;
     public Bitmap thumbnail;
-    public boolean isActive;
     public boolean lockToThisTask;
     public boolean lockToTaskEnabled;
     public Bitmap icon;
@@ -116,7 +115,7 @@
         // Do nothing
     }
 
-    public Task(TaskKey key, boolean isActive, int taskAffiliation, int taskAffiliationColor,
+    public Task(TaskKey key, int taskAffiliation, int taskAffiliationColor,
                 String activityTitle, String contentDescription, Drawable activityIcon,
                 int colorPrimary, boolean lockToThisTask, boolean lockToTaskEnabled, Bitmap icon,
                 String iconFilename, Rect bounds) {
@@ -131,7 +130,6 @@
         this.colorPrimary = hasAffiliationGroupColor ? taskAffiliationColor : colorPrimary;
         this.useLightOnPrimaryColor = Utilities.computeContrastBetweenColors(this.colorPrimary,
                 Color.WHITE) > 3f;
-        this.isActive = isActive;
         this.lockToThisTask = lockToTaskEnabled && lockToThisTask;
         this.lockToTaskEnabled = lockToTaskEnabled;
         this.icon = icon;
@@ -149,7 +147,6 @@
         this.activityIcon = o.activityIcon;
         this.colorPrimary = o.colorPrimary;
         this.useLightOnPrimaryColor = o.useLightOnPrimaryColor;
-        this.isActive = o.isActive;
         this.lockToThisTask = o.lockToThisTask;
         this.lockToTaskEnabled = o.lockToTaskEnabled;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
new file mode 100644
index 0000000..a28601b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsTransitionHelper.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2014 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.recents.views;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IRemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.AppTransitionAnimationSpec;
+import android.view.IAppTransitionAnimationSpecsFuture;
+import android.view.WindowManagerGlobal;
+import com.android.internal.annotations.GuardedBy;
+import com.android.systemui.recents.Constants;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.events.EventBus;
+import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
+import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
+import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
+import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
+import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskStack;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
+import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
+import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+
+/**
+ * A helper class to create transitions to/from Recents
+ */
+public class RecentsTransitionHelper {
+
+    private static final String TAG = "RecentsTransitionHelper";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
+     * waiting for the specs to be retrieved.
+     */
+    private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
+
+    @GuardedBy("this")
+    private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
+
+    private Context mContext;
+    private Handler mHandler;
+    private TaskViewTransform mTmpTransform = new TaskViewTransform();
+
+    private Runnable mStartScreenPinningRunnable = new Runnable() {
+        @Override
+        public void run() {
+            EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext));
+        }
+    };
+
+    public RecentsTransitionHelper(Context context, Handler handler) {
+        mContext = context;
+        mHandler = handler;
+    }
+
+    /**
+     * Launches the specified {@link Task}.
+     */
+    public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
+            final TaskStackView stackView, final TaskView taskView,
+            final boolean lockToTask, final Rect bounds, int destinationStack) {
+        final ActivityOptions opts = ActivityOptions.makeBasic();
+        if (bounds != null) {
+            opts.setBounds(bounds.isEmpty() ? null : bounds);
+        }
+
+        final ActivityOptions.OnAnimationStartedListener animStartedListener;
+        final IAppTransitionAnimationSpecsFuture transitionFuture;
+        if (task.thumbnail != null && task.thumbnail.getWidth() > 0 &&
+                task.thumbnail.getHeight() > 0) {
+            transitionFuture = getAppTransitionFuture(task, stackView, destinationStack);
+            animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
+                @Override
+                public void onAnimationStarted() {
+                    // If we are launching into another task, cancel the previous task's
+                    // window transition
+                    EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
+
+                    if (lockToTask) {
+                        // Request screen pinning after the animation runs
+                        mHandler.postDelayed(mStartScreenPinningRunnable, 350);
+                    }
+                }
+            };
+        } else {
+            // This is only the case if the task is not on screen (scrolled offscreen for example)
+            transitionFuture = null;
+            animStartedListener = null;
+        }
+
+        if (taskView == null) {
+            // If there is no task view, then we do not need to worry about animating out occluding
+            // task views, and we can launch immediately
+            startTaskActivity(stack, task, taskView, opts, transitionFuture, animStartedListener);
+        } else {
+            if (task.group != null && !task.group.isFrontMostTask(task)) {
+                stackView.startLaunchTaskAnimation(taskView, new Runnable() {
+                    @Override
+                    public void run() {
+                        startTaskActivity(stack, task, taskView, opts, transitionFuture,
+                                animStartedListener);
+                    }
+                }, lockToTask);
+            } else {
+                stackView.startLaunchTaskAnimation(taskView, null, lockToTask);
+                startTaskActivity(stack, task, taskView, opts, transitionFuture,
+                        animStartedListener);
+            }
+        }
+    }
+
+    /**
+     * Starts the activity for the launch task.
+     *
+     * @param taskView this is the {@link TaskView} that we are launching from. This can be null if
+     *                 we are toggling recents and the launch-to task is now offscreen.
+     */
+    private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
+            ActivityOptions opts, IAppTransitionAnimationSpecsFuture transitionFuture,
+            final ActivityOptions.OnAnimationStartedListener animStartedListener) {
+        SystemServicesProxy ssp = Recents.getSystemServices();
+        if (ssp.startActivityFromRecents(mContext, task.key.id, task.activityLabel, opts)) {
+            // Keep track of the index of the task launch
+            int taskIndexFromFront = 0;
+            int taskIndex = stack.indexOfTask(task);
+            if (taskIndex > -1) {
+                taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
+            }
+            EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
+        } else {
+            // Dismiss the task if we fail to launch it
+            EventBus.getDefault().send(new DismissTaskViewEvent(task, taskView));
+
+            // Keep track of failed launches
+            EventBus.getDefault().send(new LaunchTaskFailedEvent());
+        }
+        if (transitionFuture != null) {
+            IRemoteCallback.Stub callback = null;
+            if (animStartedListener != null) {
+                callback = new IRemoteCallback.Stub() {
+                    @Override
+                    public void sendResult(Bundle data) throws RemoteException {
+                        mHandler.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                if (animStartedListener != null) {
+                                    animStartedListener.onAnimationStarted();
+                                }
+                            }
+                        });
+                    }
+                };
+            }
+            try {
+                WindowManagerGlobal.getWindowManagerService()
+                        .overridePendingAppTransitionMultiThumbFuture(transitionFuture,
+                                callback, true /* scaleUp */);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to override transition: " + e);
+            }
+        }
+    }
+
+    /**
+     * Creates a future which will later be queried for animation specs for this current transition.
+     */
+    private IAppTransitionAnimationSpecsFuture getAppTransitionFuture(final Task task,
+            final TaskStackView stackView, final int destinationStack) {
+        return new IAppTransitionAnimationSpecsFuture.Stub() {
+            @Override
+            public AppTransitionAnimationSpec[] get() throws RemoteException {
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        synchronized (RecentsTransitionHelper.this) {
+                            mAppTransitionAnimationSpecs = composeAnimationSpecs(task, stackView,
+                                    destinationStack);
+                            RecentsTransitionHelper.this.notifyAll();
+                        }
+                    }
+                });
+                synchronized (RecentsTransitionHelper.this) {
+                    while (mAppTransitionAnimationSpecs == SPECS_WAITING) {
+                        try {
+                            RecentsTransitionHelper.this.wait();
+                        } catch (InterruptedException e) {}
+                    }
+                    if (mAppTransitionAnimationSpecs == null) {
+                        return null;
+                    }
+                    AppTransitionAnimationSpec[] specs
+                            = new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()];
+                    mAppTransitionAnimationSpecs.toArray(specs);
+                    mAppTransitionAnimationSpecs = SPECS_WAITING;
+                    return specs;
+                }
+            }
+        };
+    }
+
+    /**
+     * Composes the animation specs for all the tasks in the target stack.
+     */
+    private List<AppTransitionAnimationSpec> composeAnimationSpecs(final Task task,
+            final TaskStackView stackView, final int destinationStack) {
+        // Ensure we have a valid target stack id
+        final int targetStackId = destinationStack != INVALID_STACK_ID ?
+                destinationStack : task.key.stackId;
+        if (targetStackId != FREEFORM_WORKSPACE_STACK_ID
+                && targetStackId != FULLSCREEN_WORKSPACE_STACK_ID) {
+            return null;
+        }
+
+        // Calculate the offscreen task rect (for tasks that are not backed by views)
+        float stackScroll = stackView.getScroller().getStackScroll();
+        TaskView taskView = stackView.getChildViewForTask(task);
+        TaskStackLayoutAlgorithm layoutAlgorithm = stackView.getStackAlgorithm();
+        Rect offscreenTaskRect = new Rect(layoutAlgorithm.mTaskRect);
+        offscreenTaskRect.offsetTo(offscreenTaskRect.left,
+                layoutAlgorithm.mCurrentStackRect.bottom);
+
+        // If this is a full screen stack, the transition will be towards the single, full screen
+        // task. We only need the transition spec for this task.
+        List<AppTransitionAnimationSpec> specs = new ArrayList<>();
+        if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID) {
+            if (taskView == null) {
+                specs.add(composeOffscreenAnimationSpec(task, offscreenTaskRect));
+            } else {
+                layoutAlgorithm.getStackTransform(task, stackScroll, mTmpTransform, null);
+                specs.add(composeAnimationSpec(taskView, mTmpTransform, true /* addHeaderBitmap */));
+            }
+            return specs;
+        }
+
+        // Otherwise, for freeform tasks, create a new animation spec for each task we have to
+        // launch
+        TaskStack stack = stackView.getStack();
+        ArrayList<Task> tasks = stack.getTasks();
+        int taskCount = tasks.size();
+        for (int i = taskCount - 1; i >= 0; i--) {
+            Task t = tasks.get(i);
+            if (t.isFreeformTask()) {
+                TaskView tv = stackView.getChildViewForTask(t);
+                if (tv == null) {
+                    // TODO: Create a different animation task rect for this case (though it should
+                    //       never happen)
+                    specs.add(composeOffscreenAnimationSpec(t, offscreenTaskRect));
+                } else {
+                    layoutAlgorithm.getStackTransform(task, stackScroll, mTmpTransform, null);
+                    specs.add(composeAnimationSpec(tv, mTmpTransform, true /* addHeaderBitmap */));
+                }
+            }
+        }
+
+        return specs;
+    }
+
+    /**
+     * Composes a single animation spec for the given {@link Task}
+     */
+    private static AppTransitionAnimationSpec composeOffscreenAnimationSpec(Task task,
+            Rect taskRect) {
+        return new AppTransitionAnimationSpec(task.key.id, null, taskRect);
+    }
+
+    /**
+     * Composes a single animation spec for the given {@link TaskView}
+     */
+    private static AppTransitionAnimationSpec composeAnimationSpec(TaskView taskView,
+            TaskViewTransform transform, boolean addHeaderBitmap) {
+        // Disable any focused state before we draw the header
+        // Upfront the processing of the thumbnail
+        if (taskView.isFocusedTask()) {
+            taskView.setFocusedState(false, false /* animated */, false /* requestViewFocus */);
+        }
+
+        Bitmap b = null;
+        if (addHeaderBitmap) {
+            float scale = transform.scale;
+            int fromHeaderWidth = (int) (taskView.mHeaderView.getMeasuredWidth() * scale);
+            int fromHeaderHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale);
+            b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight,
+                    Bitmap.Config.ARGB_8888);
+
+            if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) {
+                b.eraseColor(0xFFff0000);
+            } else {
+                Canvas c = new Canvas(b);
+                c.scale(scale, scale);
+                taskView.mHeaderView.draw(c);
+                c.setBitmap(null);
+            }
+            b = b.createAshmemBitmap();
+        }
+
+        Rect taskRect = new Rect();
+        transform.rect.round(taskRect);
+        return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index af30268..e37b7dc 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -16,45 +16,31 @@
 
 package com.android.systemui.recents.views;
 
-import android.app.ActivityOptions;
-import android.app.ActivityOptions.OnAnimationStartedListener;
 import android.content.Context;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.IRemoteCallback;
-import android.os.RemoteException;
+import android.os.Handler;
 import android.util.ArraySet;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
 import android.view.AppTransitionAnimationSpec;
-import android.view.IAppTransitionAnimationSpecsFuture;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.WindowInsets;
-import android.view.WindowManagerGlobal;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
-
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
-import com.android.systemui.recents.Constants;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsActivity;
-import com.android.systemui.recents.RecentsActivityLaunchState;
 import com.android.systemui.recents.RecentsAppWidgetHostView;
 import com.android.systemui.recents.RecentsConfiguration;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
 import com.android.systemui.recents.events.activity.DismissRecentsToHomeAnimationStarted;
-import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
-import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragDropTargetChangedEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
@@ -65,8 +51,6 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
-import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 
 /**
@@ -78,29 +62,22 @@
     private static final String TAG = "RecentsView";
     private static final boolean DEBUG = false;
 
-    private static final boolean ADD_HEADER_BITMAP = true;
-
-    /**
-     * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
-     * waiting for the specs to be retrieved.
-     */
-    private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
-
     private int mStackViewVisibility = View.VISIBLE;
 
     /** The RecentsView callbacks */
     public interface RecentsViewCallbacks {
-        public void onTaskLaunchFailed();
         public void onAllTaskViewsDismissed();
     }
 
     LayoutInflater mInflater;
+    Handler mHandler;
 
     ArrayList<TaskStack> mStacks;
     TaskStackView mTaskStackView;
     RecentsAppWidgetHostView mSearchBar;
     RecentsViewCallbacks mCb;
 
+    RecentsTransitionHelper mTransitionHelper;
     RecentsViewTouchHandler mTouchHandler;
     DragView mDragView;
     TaskStack.DockState[] mVisibleDockStates = {
@@ -114,10 +91,6 @@
 
     Rect mSystemInsets = new Rect();
 
-
-    @GuardedBy("this")
-    List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
-
     public RecentsView(Context context) {
         super(context);
     }
@@ -134,6 +107,8 @@
         super(context, attrs, defStyleAttr, defStyleRes);
         setWillNotDraw(false);
         mInflater = LayoutInflater.from(context);
+        mHandler = new Handler();
+        mTransitionHelper = new RecentsTransitionHelper(getContext(), mHandler);
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_slow_in);
         mTouchHandler = new RecentsViewTouchHandler(this);
@@ -201,7 +176,7 @@
             Task task = mTaskStackView.getFocusedTask();
             if (task != null) {
                 TaskView taskView = mTaskStackView.getChildViewForTask(task);
-                onTaskViewClicked(mTaskStackView, taskView, stack, task, false, false, null,
+                onTaskViewClicked(mTaskStackView, taskView, stack, task, false, null,
                         INVALID_STACK_ID);
                 return true;
             }
@@ -219,7 +194,7 @@
             for (int j = 0; j < taskViewCount; j++) {
                 TaskView tv = taskViews.get(j);
                 if (tv.getTask() == task) {
-                    onTaskViewClicked(mTaskStackView, tv, stack, task, false, taskBounds != null,
+                    onTaskViewClicked(mTaskStackView, tv, stack, task, false,
                             taskBounds, destinationStack);
                     return true;
                 }
@@ -424,282 +399,14 @@
         }
     }
 
-    private IAppTransitionAnimationSpecsFuture getAppTransitionFuture(final TaskStackView stackView,
-            final TaskView clickedTask, final int offsetX, final int offsetY,
-            final float stackScroll, final int destinationStack) {
-        return new IAppTransitionAnimationSpecsFuture.Stub() {
-            @Override
-            public AppTransitionAnimationSpec[] get() throws RemoteException {
-                post(new Runnable() {
-                    @Override
-                    public void run() {
-                        synchronized (RecentsView.this) {
-                            mAppTransitionAnimationSpecs = getAppTransitionAnimationSpecs(stackView,
-                                    clickedTask, offsetX, offsetY, stackScroll, destinationStack);
-                            RecentsView.this.notifyAll();
-                        }
-                    }
-                });
-                synchronized (RecentsView.this) {
-                    while (mAppTransitionAnimationSpecs == SPECS_WAITING) {
-                        try {
-                            RecentsView.this.wait();
-                        } catch (InterruptedException e) {}
-                    }
-                    if (mAppTransitionAnimationSpecs == null) {
-                        return null;
-                    }
-                    AppTransitionAnimationSpec[] specs
-                            = new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()];
-                    mAppTransitionAnimationSpecs.toArray(specs);
-                    mAppTransitionAnimationSpecs = SPECS_WAITING;
-                    return specs;
-                }
-            }
-        };
-    }
-
-    private List<AppTransitionAnimationSpec> getAppTransitionAnimationSpecs(TaskStackView stackView,
-            TaskView clickedTask, int offsetX, int offsetY, float stackScroll,
-            int destinationStack) {
-        final int targetStackId = destinationStack != INVALID_STACK_ID ?
-                destinationStack : clickedTask.getTask().key.stackId;
-        if (targetStackId != FREEFORM_WORKSPACE_STACK_ID
-                && targetStackId != FULLSCREEN_WORKSPACE_STACK_ID) {
-            return null;
-        }
-        // If this is a full screen stack, the transition will be towards the single, full screen
-        // task. We only need the transition spec for this task.
-        List<AppTransitionAnimationSpec> specs = new ArrayList<>();
-        if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID) {
-            specs.add(createThumbnailHeaderAnimationSpec(
-                    stackView, offsetX, offsetY, stackScroll, clickedTask,
-                    clickedTask.getTask().key.id, ADD_HEADER_BITMAP));
-            return specs;
-        }
-        // This is a free form stack or full screen stack, so there will be multiple windows
-        // animating from thumbnails. We need transition animation specs for all of them.
-
-        // We will use top and bottom task views as a base for tasks, that aren't visible on the
-        // screen. This is necessary for cascade recents list, where some of the tasks might be
-        // hidden.
-        List<TaskView> taskViews = stackView.getTaskViews();
-        int childCount = taskViews.size();
-        TaskView topChild = taskViews.get(0);
-        TaskView bottomChild = taskViews.get(childCount - 1);
-        SparseArray<TaskView> taskViewsByTaskId = new SparseArray<>();
-        for (int i = 0; i < childCount; i++) {
-            TaskView taskView = taskViews.get(i);
-            taskViewsByTaskId.put(taskView.getTask().key.id, taskView);
-        }
-
-        TaskStack stack = stackView.getStack();
-        // We go through all tasks now and for each generate transition animation spec. If there is
-        // a view associated with a task, we use that view as a base for the animation. If there
-        // isn't, we use bottom or top view, depending on which one would be closer to the task
-        // view if it existed.
-        ArrayList<Task> tasks = stack.getTasks();
-        boolean passedClickedTask = false;
-        for (int i = 0, n = tasks.size(); i < n; i++) {
-            Task task = tasks.get(i);
-            TaskView taskView = taskViewsByTaskId.get(task.key.id);
-            if (taskView != null) {
-                specs.add(createThumbnailHeaderAnimationSpec(stackView, offsetX, offsetY,
-                        stackScroll, taskView, taskView.getTask().key.id, ADD_HEADER_BITMAP));
-                if (taskView == clickedTask) {
-                    passedClickedTask = true;
-                }
-            } else {
-                taskView = passedClickedTask ? bottomChild : topChild;
-                specs.add(createThumbnailHeaderAnimationSpec(stackView, offsetX, offsetY,
-                        stackScroll, taskView, task.key.id, !ADD_HEADER_BITMAP));
-            }
-        }
-
-        return specs;
-    }
-
-    private AppTransitionAnimationSpec createThumbnailHeaderAnimationSpec(TaskStackView stackView,
-            int offsetX, int offsetY, float stackScroll, TaskView tv, int taskId,
-            boolean addHeaderBitmap) {
-        // Disable any focused state before we draw the header
-        // Upfront the processing of the thumbnail
-        if (tv.isFocusedTask()) {
-            tv.setFocusedState(false, false /* animated */, false /* requestViewFocus */);
-        }
-        TaskViewTransform transform = new TaskViewTransform();
-        transform = stackView.getStackAlgorithm().getStackTransform(tv.mTask, stackScroll,
-                transform, null);
-
-        float scale = tv.getScaleX();
-        int fromHeaderWidth = (int) (tv.mHeaderView.getMeasuredWidth() * scale);
-        int fromHeaderHeight = (int) (tv.mHeaderView.getMeasuredHeight() * scale);
-
-        Bitmap b = null;
-        if (addHeaderBitmap) {
-            b = Bitmap.createBitmap(fromHeaderWidth, fromHeaderHeight,
-                    Bitmap.Config.ARGB_8888);
-
-            if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) {
-                b.eraseColor(0xFFff0000);
-            } else {
-                Canvas c = new Canvas(b);
-                c.scale(tv.getScaleX(), tv.getScaleY());
-                tv.mHeaderView.draw(c);
-                c.setBitmap(null);
-
-            }
-            b = b.createAshmemBitmap();
-        }
-
-        int[] pts = new int[2];
-        tv.getLocationOnScreen(pts);
-
-        final int left = pts[0] + offsetX;
-        final int top = pts[1] + offsetY;
-        final Rect rect = new Rect(left, top, left + (int) transform.rect.width(),
-                top + (int) transform.rect.height());
-
-        return new AppTransitionAnimationSpec(taskId, b, rect);
-    }
-
     /**** TaskStackView.TaskStackCallbacks Implementation ****/
 
     @Override
     public void onTaskViewClicked(final TaskStackView stackView, final TaskView tv,
             final TaskStack stack, final Task task, final boolean lockToTask,
-            final boolean boundsValid, final Rect bounds, int destinationStack) {
-
-        // Upfront the processing of the thumbnail
-        TaskViewTransform transform = new TaskViewTransform();
-        View sourceView;
-        int offsetX = 0;
-        int offsetY = 0;
-        float stackScroll = stackView.getScroller().getStackScroll();
-        if (tv == null) {
-            // If there is no actual task view, then use the stack view as the source view
-            // and then offset to the expected transform rect, but bound this to just
-            // outside the display rect (to ensure we don't animate from too far away)
-            sourceView = stackView;
-            offsetX = (int) transform.rect.left;
-            offsetY = getMeasuredHeight();
-        } else {
-            sourceView = tv.mThumbnailView;
-        }
-
-        // Compute the thumbnail to scale up from
-        final SystemServicesProxy ssp = Recents.getSystemServices();
-        boolean screenPinningRequested = false;
-        ActivityOptions opts = ActivityOptions.makeBasic();
-        ActivityOptions.OnAnimationStartedListener animStartedListener = null;
-        final IAppTransitionAnimationSpecsFuture transitionFuture;
-        if (task.thumbnail != null && task.thumbnail.getWidth() > 0 &&
-                task.thumbnail.getHeight() > 0) {
-            animStartedListener = new ActivityOptions.OnAnimationStartedListener() {
-                @Override
-                public void onAnimationStarted() {
-                    // If we are launching into another task, cancel the previous task's
-                    // window transition
-                    EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
-
-                    if (lockToTask) {
-                        // Request screen pinning after the animation runs
-                        postDelayed(new Runnable() {
-                            @Override
-                            public void run() {
-                                EventBus.getDefault().send(new ScreenPinningRequestEvent(
-                                        getContext(), ssp));
-                            }
-                        }, 350);
-                    }
-                }
-            };
-            transitionFuture = getAppTransitionFuture(stackView, tv, offsetX, offsetY, stackScroll,
-                    destinationStack);
-            screenPinningRequested = true;
-        } else {
-            transitionFuture = null;
-        }
-        if (boundsValid) {
-            opts.setBounds(bounds.isEmpty() ? null : bounds);
-        }
-        final ActivityOptions launchOpts = opts;
-        final boolean finalScreenPinningRequested = screenPinningRequested;
-        final OnAnimationStartedListener finalAnimStartedListener = animStartedListener;
-        final Runnable launchRunnable = new Runnable() {
-            @Override
-            public void run() {
-                if (task.isActive) {
-                    // Bring an active task to the foreground
-                    ssp.moveTaskToFront(task.key.id, launchOpts);
-                } else {
-                    if (ssp.startActivityFromRecents(getContext(), task.key.id, task.activityLabel,
-                            launchOpts)) {
-                        if (!finalScreenPinningRequested) {
-                            // If we have not requested this already to be run after the window
-                            // transition, then just run it now
-                            EventBus.getDefault().send(new ScreenPinningRequestEvent(
-                                    getContext(), ssp));
-                        }
-                    } else {
-                        // Dismiss the task and return the user to home if we fail to
-                        // launch the task
-                        EventBus.getDefault().send(new DismissTaskViewEvent(task, tv));
-                        if (mCb != null) {
-                            mCb.onTaskLaunchFailed();
-                        }
-
-                        // Keep track of failed launches
-                        MetricsLogger.count(getContext(), "overview_task_launch_failed", 1);
-                    }
-                }
-                if (transitionFuture != null) {
-                    IRemoteCallback.Stub callback = new IRemoteCallback.Stub() {
-                        @Override
-                        public void sendResult(Bundle data) throws RemoteException {
-                            post(new Runnable() {
-                                @Override
-                                public void run() {
-                                    if (finalAnimStartedListener != null) {
-                                        finalAnimStartedListener.onAnimationStarted();
-                                    }
-                                }
-                            });
-                        }
-                    };
-                    try {
-                        WindowManagerGlobal.getWindowManagerService()
-                                .overridePendingAppTransitionMultiThumbFuture(transitionFuture,
-                                        callback, true /* scaleUp */);
-                    } catch (RemoteException e) {
-                        Log.w(TAG, "Failed to override transition: " + e);
-                    }
-                }
-            }
-        };
-
-        // Keep track of the index of the task launch
-        int taskIndexFromFront = 0;
-        int taskIndex = stack.indexOfTask(task);
-        if (taskIndex > -1) {
-            taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
-        }
-        MetricsLogger.histogram(getContext(), "overview_task_launch_index", taskIndexFromFront);
-
-        // Launch the app right away if there is no task view, otherwise, animate the icon out first
-        if (tv == null) {
-            launchRunnable.run();
-        } else {
-            if (task.group != null && !task.group.isFrontMostTask(task)) {
-                // For affiliated tasks that are behind other tasks, we must animate the front cards
-                // out of view before starting the task transition
-                stackView.startLaunchTaskAnimation(tv, launchRunnable, lockToTask);
-            } else {
-                // Otherwise, we can start the task transition immediately
-                stackView.startLaunchTaskAnimation(tv, null, lockToTask);
-                launchRunnable.run();
-            }
-        }
+            final Rect bounds, int destinationStack) {
+        mTransitionHelper.launchTaskFromRecents(stack, task, stackView, tv, lockToTask, bounds,
+                destinationStack);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index 4a11b93..9c8829f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -79,7 +79,7 @@
     /** The TaskView callbacks */
     interface TaskStackViewCallbacks {
         public void onTaskViewClicked(TaskStackView stackView, TaskView tv, TaskStack stack, Task t,
-                boolean lockToTask, boolean boundsValid, Rect bounds, int destinationStack);
+                boolean lockToTask, Rect bounds, int destinationStack);
         public void onAllTaskViewsDismissed(ArrayList<Task> removedTasks);
         public void onTaskStackFilterTriggered();
         public void onTaskStackUnfilterTriggered();
@@ -1346,8 +1346,7 @@
         mUIDozeTrigger.stopDozing();
 
         if (mCb != null) {
-            mCb.onTaskViewClicked(this, tv, mStack, task, lockToTask, false, null,
-                    INVALID_STACK_ID);
+            mCb.onTaskViewClicked(this, tv, mStack, task, lockToTask, null, INVALID_STACK_ID);
         }
     }