Merge changes I91c58397,I7243f30b,I0f6e9dbf,I1eb0864a

* changes:
  Adding reveal transition to show app-overlay.
  Fixing issue with multiple focus timer animations running.
  Fixing several regressions with affiliated tasks.
  Starting the dismiss animation in parallel with the gesture.
diff --git a/packages/SystemUI/res/drawable/recents_info_dark.xml b/packages/SystemUI/res/drawable/recents_info_dark.xml
new file mode 100644
index 0000000..b1a2242
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recents_info_dark.xml
@@ -0,0 +1,24 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48.0dp"
+        android:height="48.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="@color/recents_task_bar_dark_icon_color"
+        android:pathData="M12.000000,2.000000C6.500000,2.000000 2.000000,6.500000 2.000000,12.000000s4.500000,10.000000 10.000000,10.000000c5.500000,0.000000 10.000000,-4.500000 10.000000,-10.000000S17.500000,2.000000 12.000000,2.000000zM13.000000,17.000000l-2.000000,0.000000l0.000000,-6.000000l2.000000,0.000000L13.000000,17.000000zM13.000000,9.000000l-2.000000,0.000000L11.000000,7.000000l2.000000,0.000000L13.000000,9.000000z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/recents_info_light.xml b/packages/SystemUI/res/drawable/recents_info_light.xml
new file mode 100644
index 0000000..bc58c3b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/recents_info_light.xml
@@ -0,0 +1,24 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="48.0dp"
+        android:height="48.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M12.000000,2.000000C6.500000,2.000000 2.000000,6.500000 2.000000,12.000000s4.500000,10.000000 10.000000,10.000000c5.500000,0.000000 10.000000,-4.500000 10.000000,-10.000000S17.500000,2.000000 12.000000,2.000000zM13.000000,17.000000l-2.000000,0.000000l0.000000,-6.000000l2.000000,0.000000L13.000000,17.000000zM13.000000,9.000000l-2.000000,0.000000L11.000000,7.000000l2.000000,0.000000L13.000000,9.000000z"
+        android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/packages/SystemUI/res/layout/recents_task_view_header.xml b/packages/SystemUI/res/layout/recents_task_view_header.xml
index 07ac39a..a94e781 100644
--- a/packages/SystemUI/res/layout/recents_task_view_header.xml
+++ b/packages/SystemUI/res/layout/recents_task_view_header.xml
@@ -64,12 +64,19 @@
         android:background="@drawable/recents_button_bg"
         android:visibility="invisible"
         android:src="@drawable/recents_dismiss_light" />
-    <ProgressBar
-        android:id="@+id/focus_timer_indicator"
-        style="?android:attr/progressBarStyleHorizontal"
-        android:layout_width="match_parent"
-        android:layout_height="5dp"
-        android:layout_gravity="bottom"
-        android:indeterminateOnly="false"
-        android:visibility="invisible" />
+
+    <!-- The progress indicator shows if auto-paging is enabled -->
+    <ViewStub android:id="@+id/focus_timer_indicator_stub"
+               android:inflatedId="@+id/focus_timer_indicator"
+               android:layout="@layout/recents_task_view_header_progress_bar"
+               android:layout_width="match_parent"
+               android:layout_height="5dp"
+               android:layout_gravity="bottom" />
+
+    <!-- The app overlay shows as the user long-presses on the app icon -->
+    <ViewStub android:id="@+id/app_overlay_stub"
+               android:inflatedId="@+id/app_overlay"
+               android:layout="@layout/recents_task_view_header_overlay"
+               android:layout_width="match_parent"
+               android:layout_height="match_parent" />
 </com.android.systemui.recents.views.TaskViewHeader>
diff --git a/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
new file mode 100644
index 0000000..5cdde9e
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_header_overlay.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:visibility="gone">
+    <com.android.systemui.recents.views.FixedSizeImageView
+        android:id="@+id/app_icon"
+        android:contentDescription="@string/recents_app_info_button_label"
+        android:layout_width="@dimen/recents_task_view_application_icon_size"
+        android:layout_height="@dimen/recents_task_view_application_icon_size"
+        android:layout_marginStart="8dp"
+        android:layout_gravity="center_vertical|start"
+        android:padding="8dp" />
+    <TextView
+        android:id="@+id/app_title"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical|start"
+        android:layout_marginStart="64dp"
+        android:layout_marginEnd="112dp"
+        android:textSize="16sp"
+        android:textColor="#ffffffff"
+        android:text="@string/recents_empty_message"
+        android:fontFamily="sans-serif-medium"
+        android:singleLine="true"
+        android:maxLines="2"
+        android:ellipsize="marquee"
+        android:fadingEdge="horizontal" />
+    <com.android.systemui.recents.views.FixedSizeImageView
+        android:id="@+id/app_info"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_marginEnd="4dp"
+        android:layout_gravity="center_vertical|end"
+        android:padding="12dp"
+        android:background="@drawable/recents_button_bg"
+        android:src="@drawable/recents_info_light" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml
new file mode 100644
index 0000000..e740458
--- /dev/null
+++ b/packages/SystemUI/res/layout/recents_task_view_header_progress_bar.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<ProgressBar
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    style="?android:attr/progressBarStyleHorizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:indeterminateOnly="false"
+    android:visibility="gone" />
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 6795da4..17ff195 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -35,7 +35,7 @@
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
          while the stack is focused. -->
-    <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-3</item>
     <item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e98ec82..7a87314 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -147,6 +147,9 @@
     <!-- The duration for animating the task decorations in after transitioning from an app. -->
     <integer name="recents_task_enter_from_app_duration">200</integer>
 
+    <!-- The duration for animating the task decorations in after transitioning from an app. -->
+    <integer name="recents_task_enter_from_affiliated_app_duration">125</integer>
+
     <!-- The duration for animating the task decorations out before transitioning to an app. -->
     <integer name="recents_task_exit_to_app_duration">125</integer>
 
@@ -194,7 +197,7 @@
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
          while the stack is focused. -->
-    <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-3</item>
     <item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
 
     <!-- Recents: The relative range of visible tasks from the current scroll position
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 47eb05a..91ca289 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -210,7 +210,7 @@
     <dimen name="recents_task_view_highlight">1dp</dimen>
 
     <!-- The amount to offset when animating into an affiliate group. -->
-    <dimen name="recents_task_view_affiliate_group_enter_offset">64dp</dimen>
+    <dimen name="recents_task_view_affiliate_group_enter_offset">32dp</dimen>
 
     <!-- The height of a task view bar. -->
     <dimen name="recents_task_bar_height">56dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index d28da41..fd4161f 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -367,6 +367,7 @@
         }
         anim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
+                updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
                 mCallback.onChildDismissed(view);
                 if (endAction != null) {
                     endAction.run();
@@ -381,9 +382,17 @@
                 updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
             }
         });
+        prepareDismissAnimation(animView, anim);
         anim.start();
     }
 
+    /**
+     * Called to update the dismiss animation.
+     */
+    protected void prepareDismissAnimation(View view, Animator anim) {
+        // Do nothing
+    }
+
     public void snapChild(final View view, float velocity) {
         final View animView = mCallback.getChildContentView(view);
         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
@@ -401,14 +410,14 @@
                 mCallback.onChildSnappedBack(animView);
             }
         });
-        updateSnapBackAnimation(anim);
+        prepareSnapBackAnimation(animView, anim);
         anim.start();
     }
 
     /**
      * Called to update the snap back animation.
      */
-    protected void updateSnapBackAnimation(Animator anim) {
+    protected void prepareSnapBackAnimation(View view, Animator anim) {
         // Do nothing
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index db55f28..3a3b19d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -183,7 +183,7 @@
         RecentsConfiguration config = Recents.getConfiguration();
         RecentsActivityLaunchState launchState = config.getLaunchState();
         if (!plan.hasTasks()) {
-            loader.preloadTasks(plan, launchState.launchedFromHome);
+            loader.preloadTasks(plan, -1, launchState.launchedFromHome);
         }
         RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options();
         loadOpts.runningTaskId = launchState.launchedToTaskId;
@@ -192,24 +192,14 @@
         loader.loadTasks(this, plan, loadOpts);
 
         TaskStack stack = plan.getTaskStack();
-        ArrayList<Task> tasks = stack.getStackTasks();
-        int taskCount = stack.getTaskCount();
         mRecentsView.setTaskStack(stack);
 
-        // Mark the task that is the launch target
-        int launchTaskIndexInStack = 0;
-        if (launchState.launchedToTaskId != -1) {
-            for (int j = 0; j < taskCount; j++) {
-                Task t = tasks.get(j);
-                if (t.key.id == launchState.launchedToTaskId) {
-                    t.isLaunchTarget = true;
-                    launchTaskIndexInStack = tasks.size() - j - 1;
-                    break;
-                }
-            }
-        }
-
         // Animate the SystemUI scrims into view
+        Task launchTarget = stack.getLaunchTarget();
+        int taskCount = stack.getTaskCount();
+        int launchTaskIndexInStack = launchTarget != null
+                ? stack.indexOfStackTask(launchTarget)
+                : 0;
         boolean hasStatusBarScrim = taskCount > 0;
         boolean animateStatusBarScrim = launchState.launchedFromHome;
         boolean hasNavBarScrim = (taskCount > 0) && !config.hasTransposedNavBar;
@@ -527,7 +517,7 @@
             launchOpts.loadThumbnails = false;
             launchOpts.onlyLoadForCache = true;
             RecentsTaskLoadPlan loadPlan = loader.createLoadPlan(this);
-            loader.preloadTasks(loadPlan, false);
+            loader.preloadTasks(loadPlan, -1, false);
             loader.loadTasks(this, loadPlan, launchOpts);
             EventBus.getDefault().send(new TaskStackUpdatedEvent(loadPlan.getTaskStack()));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
index 3151fd7..3866967 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsDebugFlags.java
@@ -37,6 +37,8 @@
         public static final boolean EnableSearchBar = false;
         // This disables the bitmap and icon caches
         public static final boolean DisableBackgroundCache = false;
+        // Enables the task affiliations
+        public static final boolean EnableAffiliatedTaskGroups = true;
         // Enables the simulated task affiliations
         public static final boolean EnableSimulatedTaskGroups = false;
         // Defines the number of mock task affiliations per group
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index 7c25d24..3eee087 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -118,7 +118,7 @@
 
                 // Load the next task only if we aren't svelte
                 RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-                loader.preloadTasks(plan, true);
+                loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
                 RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
                 // This callback is made when a new activity is launched and the old one is paused
                 // so ignore the current activity and try and preload the thumbnail for the
@@ -198,7 +198,7 @@
         // We can use a new plan since the caches will be the same.
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
         RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
         launchOpts.numVisibleTasks = loader.getIconCacheSize();
         launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
@@ -370,7 +370,7 @@
         sInstanceLoadPlan = loader.createLoadPlan(mContext);
         if (topTask != null && !ssp.isRecentsTopMost(topTask, topTaskHome)) {
             sInstanceLoadPlan.preloadRawTasks(topTaskHome.value);
-            loader.preloadTasks(sInstanceLoadPlan, topTaskHome.value);
+            loader.preloadTasks(sInstanceLoadPlan, topTask.id, topTaskHome.value);
             TaskStack stack = sInstanceLoadPlan.getTaskStack();
             if (stack.getTaskCount() > 0) {
                 // We try and draw the thumbnail transition bitmap in parallel before
@@ -399,7 +399,7 @@
         SystemServicesProxy ssp = Recents.getSystemServices();
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
         TaskStack focusedStack = plan.getTaskStack();
 
         // Return early if there are no tasks in the focused stack
@@ -451,7 +451,7 @@
         SystemServicesProxy ssp = Recents.getSystemServices();
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext);
-        loader.preloadTasks(plan, true /* isTopTaskHome */);
+        loader.preloadTasks(plan, -1, true /* isTopTaskHome */);
         TaskStack focusedStack = plan.getTaskStack();
 
         // Return early if there are no tasks in the focused stack
@@ -756,29 +756,18 @@
     private TaskViewTransform getThumbnailTransitionTransform(TaskStack stack,
             TaskStackView stackView, int runningTaskId, Task runningTaskOut) {
         // Find the running task in the TaskStack
-        Task task = null;
-        ArrayList<Task> tasks = stack.getStackTasks();
-        if (runningTaskId != -1) {
-            // Otherwise, try and find the task with the
-            int taskCount = tasks.size();
-            for (int i = taskCount - 1; i >= 0; i--) {
-                Task t = tasks.get(i);
-                if (t.key.id == runningTaskId) {
-                    task = t;
-                    runningTaskOut.copyFrom(t);
-                    break;
-                }
-            }
-        }
-        if (task == null) {
+        Task launchTask = stack.getLaunchTarget();
+        if (launchTask != null) {
+            runningTaskOut.copyFrom(launchTask);
+        } else {
             // If no task is specified or we can not find the task just use the front most one
-            task = tasks.get(tasks.size() - 1);
-            runningTaskOut.copyFrom(task);
+            launchTask = stack.getStackFrontMostTask();
+            runningTaskOut.copyFrom(launchTask);
         }
 
         // Get the transform for the running task
         stackView.getScroller().setStackScrollToInitialState();
-        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task,
+        mTmpTransform = stackView.getStackAlgorithm().getStackTransform(launchTask,
                 stackView.getScroller().getStackScroll(), mTmpTransform, null);
         return mTmpTransform;
     }
@@ -826,7 +815,7 @@
             sInstanceLoadPlan = loader.createLoadPlan(mContext);
         }
         if (mReloadTasks || mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) {
-            loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome);
+            loader.preloadTasks(sInstanceLoadPlan, topTask.id, isTopTaskHome);
         }
         TaskStack stack = sInstanceLoadPlan.getTaskStack();
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
index 80597bc..f6655c7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/history/RecentsHistoryAdapter.java
@@ -36,6 +36,7 @@
 import com.android.systemui.recents.model.RecentsTaskLoader;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.recents.views.TaskViewAnimation;
 
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -143,7 +144,6 @@
 
     private Context mContext;
     private LayoutInflater mLayoutInflater;
-    private final List<Task> mTasks = new ArrayList<>();
     private final List<Row> mRows = new ArrayList<>();
     private final SparseIntArray mTaskRowCount = new SparseIntArray();
     private TaskStack mStack;
@@ -268,8 +268,8 @@
 
     public void onTaskRemoved(Task task, int position) {
         // Since this is removed from the history, we need to update the stack as well to ensure
-        // that the model is correct
-        mStack.removeTask(task);
+        // that the model is correct. Since the stack is hidden, we can update it immediately.
+        mStack.removeTask(task, TaskViewAnimation.IMMEDIATE);
         removeTaskRow(position);
         if (mRows.isEmpty()) {
             dismissHistory();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java b/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java
new file mode 100644
index 0000000..72511de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/RectFEvaluator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 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.misc;
+
+import android.animation.TypeEvaluator;
+import android.graphics.RectF;
+
+/**
+ * This evaluator can be used to perform type interpolation between <code>RectF</code> values.
+ */
+public class RectFEvaluator implements TypeEvaluator<RectF> {
+
+    private RectF mRect = new RectF();
+
+    /**
+     * This function returns the result of linearly interpolating the start and
+     * end Rect values, with <code>fraction</code> representing the proportion
+     * between the start and end values. The calculation is a simple parametric
+     * calculation on each of the separate components in the Rect objects
+     * (left, top, right, and bottom).
+     *
+     * <p>The object returned will be the <code>reuseRect</code> passed into the constructor.</p>
+     *
+     * @param fraction   The fraction from the starting to the ending values
+     * @param startValue The start Rect
+     * @param endValue   The end Rect
+     * @return A linear interpolation between the start and end values, given the
+     *         <code>fraction</code> parameter.
+     */
+    @Override
+    public RectF evaluate(float fraction, RectF startValue, RectF endValue) {
+        float left = startValue.left + ((endValue.left - startValue.left) * fraction);
+        float top = startValue.top + ((endValue.top - startValue.top) * fraction);
+        float right = startValue.right + ((endValue.right - startValue.right) * fraction);
+        float bottom = startValue.bottom + ((endValue.bottom - startValue.bottom) * fraction);
+        mRect.set(left, top, right, bottom);
+        return mRect;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
index adc1e92..3f52ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -554,39 +555,41 @@
         }
     }
 
-    /** Returns the activity label */
-    public String getActivityLabel(ActivityInfo info) {
+    /**
+     * Returns the activity label, badging if necessary.
+     */
+    public String getBadgedActivityLabel(ActivityInfo info, int userId) {
         if (mPm == null) return null;
 
         // If we are mocking, then return a mock label
         if (RecentsDebugFlags.Static.EnableSystemServicesProxy) {
-            return "Recent Task";
+            return "Recent Task: " + userId;
         }
 
-        return info.loadLabel(mPm).toString();
+        return getBadgedLabel(info.loadLabel(mPm).toString(), userId);
     }
 
-    /** Returns the application label */
-    public String getApplicationLabel(Intent baseIntent, int userId) {
+    /**
+     * Returns the application label, badging if necessary.
+     */
+    public String getBadgedApplicationLabel(ApplicationInfo appInfo, int userId) {
         if (mPm == null) return null;
 
         // If we are mocking, then return a mock label
         if (RecentsDebugFlags.Static.EnableSystemServicesProxy) {
-            return "Recent Task";
+            return "Recent Task App: " + userId;
         }
 
-        ResolveInfo ri = mPm.resolveActivityAsUser(baseIntent, 0, userId);
-        CharSequence label = (ri != null) ? ri.loadLabel(mPm) : null;
-        return (label != null) ? label.toString() : null;
+        return getBadgedLabel(appInfo.loadLabel(mPm).toString(), userId);
     }
 
-    /** Returns the content description for a given task */
-    public String getContentDescription(Intent baseIntent, int userId, String activityLabel,
-            Resources res) {
-        String applicationLabel = getApplicationLabel(baseIntent, userId);
-        if (applicationLabel == null) {
-            return getBadgedLabel(activityLabel, userId);
-        }
+    /**
+     * Returns the content description for a given task, badging it if necessary.  The content
+     * description joins the app and activity labels.
+     */
+    public String getBadgedContentDescription(ActivityInfo info, int userId, Resources res) {
+        String activityLabel = info.loadLabel(mPm).toString();
+        String applicationLabel = info.applicationInfo.loadLabel(mPm).toString();
         String badgedApplicationLabel = getBadgedLabel(applicationLabel, userId);
         return applicationLabel.equals(activityLabel) ? badgedApplicationLabel
                 : res.getString(R.string.accessibility_recents_task_header,
@@ -610,6 +613,22 @@
     }
 
     /**
+     * Returns the application icon for the ApplicationInfo for a user, badging if
+     * necessary.
+     */
+    public Drawable getBadgedApplicationIcon(ApplicationInfo appInfo, int userId) {
+        if (mPm == null) return null;
+
+        // If we are mocking, then return a mock label
+        if (RecentsDebugFlags.Static.EnableSystemServicesProxy) {
+            return new ColorDrawable(0xFF666666);
+        }
+
+        Drawable icon = appInfo.loadIcon(mPm);
+        return getBadgedIcon(icon, userId);
+    }
+
+    /**
      * Returns the task description icon, loading and badging it if it necessary.
      */
     public Drawable getBadgedTaskDescriptionIcon(ActivityManager.TaskDescription taskDescription,
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 822ad77..9cdd703 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -25,7 +25,8 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArraySet;
-
+import android.util.SparseArray;
+import android.util.SparseIntArray;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
@@ -92,7 +93,7 @@
     }
 
     /**
-     * An optimization to preload the raw list of tasks.  The raw tasks are saved in least-recent
+     * An optimization to preload the raw list of tasks. The raw tasks are saved in least-recent
      * to most-recent order.
      */
     public synchronized void preloadRawTasks(boolean isTopTaskHome) {
@@ -107,7 +108,7 @@
     }
 
     /**
-     * Preloads the list of recent tasks from the system.  After this call, the TaskStack will
+     * Preloads the list of recent tasks from the system. After this call, the TaskStack will
      * have a list of all the recent tasks with their metadata, not including icons or
      * thumbnails which were not cached and have to be loaded.
      *
@@ -115,13 +116,16 @@
      * - least-recent to most-recent stack tasks
      * - least-recent to most-recent freeform tasks
      */
-    public synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) {
+    public synchronized void preloadPlan(RecentsTaskLoader loader, int topTaskId,
+            boolean isTopTaskHome) {
         Resources res = mContext.getResources();
         ArrayList<Task> allTasks = new ArrayList<>();
         if (mRawTasks == null) {
             preloadRawTasks(isTopTaskHome);
         }
 
+        SparseArray<Task.TaskKey> affiliatedTasks = new SparseArray<>();
+        SparseIntArray affiliatedTaskCounts = new SparseIntArray();
         String dismissDescFormat = mContext.getString(
                 R.string.accessibility_recents_item_will_be_dismissed);
         long lastStackActiveTime = Prefs.getLong(mContext,
@@ -131,6 +135,17 @@
         for (int i = 0; i < taskCount; i++) {
             ActivityManager.RecentTaskInfo t = mRawTasks.get(i);
 
+            // Affiliated tasks are returned in a specific order from ActivityManager but without a
+            // lastActiveTime since it hasn't yet been started. However, we later sort the task list
+            // by lastActiveTime, which rearranges the tasks. For now, we need to workaround this
+            // by updating the lastActiveTime of this task to the lastActiveTime of the task it is
+            // affiliated with, in the same order that we encounter it in the original list (just
+            // its index in the task group for the task it is affiliated with).
+            if (t.persistentId != t.affiliatedTaskId) {
+                t.lastActiveTime = affiliatedTasks.get(t.affiliatedTaskId).lastActiveTime +
+                        affiliatedTaskCounts.get(t.affiliatedTaskId, 0) + 1;
+            }
+
             // Compose the task key
             Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.stackId, t.baseIntent,
                     t.userId, t.firstActiveTime, t.lastActiveTime);
@@ -140,13 +155,14 @@
             boolean isFreeformTask = SystemServicesProxy.isFreeformStack(t.stackId);
             boolean isStackTask = isFreeformTask || (!isHistoricalTask(t) ||
                     (t.lastActiveTime >= lastStackActiveTime && i >= (taskCount - MIN_NUM_TASKS)));
+            boolean isLaunchTarget = taskKey.id == topTaskId;
             if (isStackTask && newLastStackActiveTime < 0) {
                 newLastStackActiveTime = t.lastActiveTime;
             }
 
             // Load the title, icon, and color
             String title = loader.getAndUpdateActivityTitle(taskKey, t.taskDescription);
-            String contentDescription = loader.getAndUpdateContentDescription(taskKey, title, res);
+            String contentDescription = loader.getAndUpdateContentDescription(taskKey, res);
             String dismissDescription = String.format(dismissDescFormat, contentDescription);
             Drawable icon = isStackTask
                     ? loader.getAndUpdateActivityIcon(taskKey, t.taskDescription, res, false)
@@ -157,9 +173,11 @@
             // Add the task to the stack
             Task task = new Task(taskKey, t.affiliatedTaskId, t.affiliatedTaskColor, icon,
                     thumbnail, title, contentDescription, dismissDescription, activityColor,
-                    !isStackTask, t.bounds, t.taskDescription);
+                    !isStackTask, isLaunchTarget, t.bounds, t.taskDescription);
 
             allTasks.add(task);
+            affiliatedTaskCounts.put(taskKey.id, affiliatedTaskCounts.get(taskKey.id, 0) + 1);
+            affiliatedTasks.put(taskKey.id, taskKey);
         }
         if (newLastStackActiveTime != -1) {
             Prefs.putLong(mContext, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
index 28338d83..44ad239 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
@@ -314,8 +314,8 @@
     }
 
     /** Preloads recents tasks using the specified plan to store the output. */
-    public void preloadTasks(RecentsTaskLoadPlan plan, boolean isTopTaskHome) {
-        plan.preloadPlan(this, isTopTaskHome);
+    public void preloadTasks(RecentsTaskLoadPlan plan, int topTaskId, boolean isTopTaskHome) {
+        plan.preloadPlan(this, topTaskId, isTopTaskHome);
     }
 
     /** Begins loading the heavy task data according to the specified options. */
@@ -436,7 +436,7 @@
         // All short paths failed, load the label from the activity info and cache it
         ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey);
         if (activityInfo != null) {
-            label = ssp.getActivityLabel(activityInfo);
+            label = ssp.getBadgedActivityLabel(activityInfo, taskKey.userId);
             mActivityLabelCache.put(taskKey, label);
             return label;
         }
@@ -449,8 +449,7 @@
      * Returns the cached task content description if the task key is not expired, updating the
      * cache if it is.
      */
-    String getAndUpdateContentDescription(Task.TaskKey taskKey, String activityLabel,
-            Resources res) {
+    String getAndUpdateContentDescription(Task.TaskKey taskKey, Resources res) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Return the cached content description if it exists
@@ -458,13 +457,11 @@
         if (label != null) {
             return label;
         }
-        // If the given activity label is empty, don't compute or cache the content description
-        if (activityLabel.isEmpty()) {
-            return "";
-        }
 
-        label = ssp.getContentDescription(taskKey.baseIntent, taskKey.userId, activityLabel, res);
-        if (label != null) {
+        // All short paths failed, load the label from the activity info and cache it
+        ActivityInfo activityInfo = getAndUpdateActivityInfo(taskKey);
+        if (activityInfo != null) {
+            label = ssp.getBadgedContentDescription(activityInfo, taskKey.userId, res);
             mContentDescriptionCache.put(taskKey, label);
             return label;
         }
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 29e7077..193bd17 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -154,7 +154,8 @@
     public Task(TaskKey key, int affiliationTaskId, int affiliationColor, Drawable icon,
                 Bitmap thumbnail, String title, String contentDescription,
                 String dismissDescription, int colorPrimary, boolean isHistorical,
-                Rect bounds, ActivityManager.TaskDescription taskDescription) {
+                boolean isLaunchTarget, Rect bounds,
+                ActivityManager.TaskDescription taskDescription) {
         boolean isInAffiliationGroup = (affiliationTaskId != key.id);
         boolean hasAffiliationGroupColor = isInAffiliationGroup && (affiliationColor != 0);
         this.key = key;
@@ -170,6 +171,7 @@
                 Color.WHITE) > 3f;
         this.bounds = bounds;
         this.taskDescription = taskDescription;
+        this.isLaunchTarget = isLaunchTarget;
         this.isHistorical = isHistorical;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 327cdf8..c73273e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -42,6 +42,7 @@
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.views.DropTarget;
+import com.android.systemui.recents.views.TaskViewAnimation;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -226,12 +227,12 @@
          * Notifies when a task has been removed from the stack.
          */
         void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask,
-            Task newFrontMostTask);
+            Task newFrontMostTask, TaskViewAnimation animation);
 
         /**
          * Notifies when a task has been removed from the history.
          */
-        void onHistoryTaskRemoved(TaskStack stack, Task removedTask);
+        void onHistoryTaskRemoved(TaskStack stack, Task removedTask, TaskViewAnimation animation);
     }
 
     /**
@@ -520,21 +521,24 @@
         }
     }
 
-    /** Removes a task */
-    public void removeTask(Task t) {
+    /**
+     * Removes a task from the stack, with an additional {@param animation} hint to the callbacks on
+     * how they should update themselves.
+     */
+    public void removeTask(Task t, TaskViewAnimation animation) {
         if (mStackTaskList.contains(t)) {
             boolean wasFrontMostTask = (getStackFrontMostTask() == t);
             removeTaskImpl(mStackTaskList, t);
             Task newFrontMostTask = getStackFrontMostTask();
             if (mCb != null) {
                 // Notify that a task has been removed
-                mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask);
+                mCb.onStackTaskRemoved(this, t, wasFrontMostTask, newFrontMostTask, animation);
             }
         } else if (mHistoryTaskList.contains(t)) {
             removeTaskImpl(mHistoryTaskList, t);
             if (mCb != null) {
                 // Notify that a task has been removed
-                mCb.onHistoryTaskRemoved(this, t);
+                mCb.onHistoryTaskRemoved(this, t, animation);
             }
         }
         mRawTaskList.remove(t);
@@ -564,7 +568,8 @@
             Task task = mRawTaskList.get(i);
             if (!newTasksMap.containsKey(task.key)) {
                 if (notifyStackChanges) {
-                    mCb.onStackTaskRemoved(this, task, i == (taskCount - 1), null);
+                    mCb.onStackTaskRemoved(this, task, i == (taskCount - 1), null,
+                            TaskViewAnimation.IMMEDIATE);
                 }
             }
             task.setGroup(null);
@@ -843,12 +848,17 @@
             for (int i = 0; i < taskCount; i++) {
                 Task t = tasks.get(i);
                 TaskGrouping group;
-                int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId :
-                        IndividualTaskIdOffset + t.key.id;
-                if (mAffinitiesGroups.containsKey(affiliation)) {
-                    group = getGroupWithAffiliation(affiliation);
+                if (RecentsDebugFlags.Static.EnableAffiliatedTaskGroups) {
+                    int affiliation = t.affiliationTaskId > 0 ? t.affiliationTaskId :
+                            IndividualTaskIdOffset + t.key.id;
+                    if (mAffinitiesGroups.containsKey(affiliation)) {
+                        group = getGroupWithAffiliation(affiliation);
+                    } else {
+                        group = new TaskGrouping(affiliation);
+                        addGroup(group);
+                    }
                 } else {
-                    group = new TaskGrouping(affiliation);
+                    group = new TaskGrouping(t.key.id);
                     addGroup(group);
                 }
                 group.addTask(t);
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 e727652..e448101 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -513,7 +513,7 @@
                     taskViewRect.right, taskViewRect.bottom);
 
             // Remove the task view after it is docked
-            mTaskStackView.updateLayout(false /* boundScroll */);
+            mTaskStackView.updateLayoutAlgorithm(false /* boundScroll */);
             stackLayout.getStackTransform(event.task, stackScroller.getStackScroll(), tmpTransform,
                     null);
             tmpTransform.alpha = 0;
@@ -529,11 +529,14 @@
                                     ssp.startTaskInDockedMode(getContext(), event.taskView,
                                             event.task.key.id, dockState.createMode);
 
-                                    mTaskStackView.getStack().removeTask(event.task);
+                                    // Animate the stack accordingly
+                                    TaskViewAnimation stackAnim = new TaskViewAnimation(
+                                            TaskStackView.DEFAULT_SYNC_STACK_DURATION,
+                                            mFastOutSlowInInterpolator);
+                                    mTaskStackView.getStack().removeTask(event.task, stackAnim);
                                 }
                             }));
 
-
             MetricsLogger.action(mContext,
                     MetricsLogger.ACTION_WINDOW_DOCK_DRAG_DROP);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
index 2930f4d..ccbb329 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
@@ -130,6 +130,7 @@
                     // Move the task view slightly lower so we can animate it in
                     RectF bounds = new RectF(mTmpTransform.rect);
                     bounds.offset(0, taskViewAffiliateGroupEnterOffset);
+                    tv.setClipViewInStack(false);
                     tv.setAlpha(0f);
                     tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top,
                             (int) bounds.right, (int) bounds.bottom);
@@ -165,6 +166,8 @@
 
         int taskViewEnterFromAppDuration = res.getInteger(
                 R.integer.recents_task_enter_from_app_duration);
+        int taskViewEnterFromAffiliatedAppDuration = res.getInteger(
+                R.integer.recents_task_enter_from_affiliated_app_duration);
         int taskViewEnterFromHomeDuration = res.getInteger(
                 R.integer.recents_task_enter_from_home_duration);
         int taskViewEnterFromHomeStaggerDelay = res.getInteger(
@@ -174,7 +177,7 @@
         List<TaskView> taskViews = mStackView.getTaskViews();
         int taskViewCount = taskViews.size();
         for (int i = taskViewCount - 1; i >= 0; i--) {
-            TaskView tv = taskViews.get(i);
+            final TaskView tv = taskViews.get(i);
             Task task = tv.getTask();
             boolean currentTaskOccludesLaunchTarget = false;
             if (launchTargetTask != null) {
@@ -195,8 +198,14 @@
                     // Animate the task up if it was occluding the launch target
                     if (currentTaskOccludesLaunchTarget) {
                         TaskViewAnimation taskAnimation = new TaskViewAnimation(
-                                taskViewEnterFromAppDuration, PhoneStatusBar.ALPHA_IN,
-                                postAnimationTrigger.decrementOnAnimationEnd());
+                                taskViewEnterFromAffiliatedAppDuration, PhoneStatusBar.ALPHA_IN,
+                                new AnimatorListenerAdapter() {
+                                    @Override
+                                    public void onAnimationEnd(Animator animation) {
+                                        postAnimationTrigger.decrement();
+                                        tv.setClipViewInStack(false);
+                                    }
+                                });
                         postAnimationTrigger.increment();
                         mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
                     }
@@ -286,7 +295,7 @@
             } else if (currentTaskOccludesLaunchTarget) {
                 // Animate this task out of view
                 TaskViewAnimation taskAnimation = new TaskViewAnimation(
-                        taskViewExitToAppDuration, mFastOutLinearInInterpolator,
+                        taskViewExitToAppDuration, PhoneStatusBar.ALPHA_OUT,
                         postAnimationTrigger.decrementOnAnimationEnd());
                 postAnimationTrigger.increment();
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 68ff63c..e99509c 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -373,7 +373,7 @@
      * Computes the minimum and maximum scroll progress values and the progress values for each task
      * in the stack.
      */
-    void update(TaskStack stack, ArraySet<Task> ignoreTasksSet) {
+    void update(TaskStack stack, ArraySet<Task.TaskKey> ignoreTasksSet) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Clear the progress map
@@ -393,7 +393,7 @@
         ArrayList<Task> stackTasks = new ArrayList<>();
         for (int i = 0; i < tasks.size(); i++) {
             Task task = tasks.get(i);
-            if (ignoreTasksSet.contains(task)) {
+            if (ignoreTasksSet.contains(task.key)) {
                 continue;
             }
             if (task.isFreeformTask()) {
@@ -434,19 +434,25 @@
             mFreeformLayoutAlgorithm.update(freeformTasks, this);
             mInitialScrollP = mMaxScrollP;
         } else {
+            Task launchTask = stack.getLaunchTarget();
+            int launchTaskIndex = launchTask != null
+                    ? stack.indexOfStackTask(launchTask)
+                    : mNumStackTasks - 1;
             if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
                 mInitialScrollP = mMinScrollP;
             } else if (getDefaultFocusState() > 0f) {
                 RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
                 if (launchState.launchedFromHome) {
-                    mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 1);
+                    mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP, launchTaskIndex));
                 } else {
-                    mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 2);
+                    mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP,
+                            launchTaskIndex - 1));
                 }
             } else {
                 float offsetPct = (float) (mTaskRect.height() / 2) / mStackRect.height();
                 float normX = mUnfocusedCurveInterpolator.getX(offsetPct);
-                mInitialScrollP = (mNumStackTasks - 1) - mUnfocusedRange.getAbsoluteX(normX);
+                mInitialScrollP = Math.max(mMinScrollP, Math.min(mMaxScrollP,
+                        launchTaskIndex - mUnfocusedRange.getAbsoluteX(normX)));
             }
         }
     }
@@ -553,7 +559,8 @@
 
             boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task);
             if (isFrontMostTaskInGroup) {
-                getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null);
+                getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null,
+                        false /* ignoreSingleTaskCase */);
                 float screenY = tmpTransform.rect.top;
                 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
                 if (hasVisibleThumbnail) {
@@ -596,13 +603,21 @@
                 return transformOut;
             }
             return getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut,
-                    frontTransform);
+                    frontTransform, false /* ignoreSingleTaskCase */);
         }
     }
 
-    /** Update/get the transform */
+    /**
+     * Update/get the transform.
+     *
+     * @param ignoreSingleTaskCase When set, will ensure that the transform computed does not take
+     *                             into account the special single-task case.  This is only used
+     *                             internally to ensure that we can calculate the transform for any
+     *                             position in the stack.
+     */
     public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
-            TaskViewTransform transformOut, TaskViewTransform frontTransform) {
+            TaskViewTransform transformOut, TaskViewTransform frontTransform,
+            boolean ignoreSingleTaskCase) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Compute the focused and unfocused offset
@@ -632,7 +647,7 @@
         int y;
         float z;
         float relP;
-        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
+        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) {
             // When there is exactly one task, then decouple the task from the stack and just move
             // in screen space
             p = (mMinScrollP - stackScroll) / mNumStackTasks;
@@ -762,8 +777,8 @@
                 mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin);
         float max = mUnfocusedRange.relativeMax +
                 mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax);
-        getStackTransform(min, 0f, mBackOfStackTransform, null);
-        getStackTransform(max, 0f, mFrontOfStackTransform, null);
+        getStackTransform(min, 0f, mBackOfStackTransform, null, true /* ignoreSingleTaskCase */);
+        getStackTransform(max, 0f, mFrontOfStackTransform, null, true /* ignoreSingleTaskCase */);
         mBackOfStackTransform.visible = true;
         mFrontOfStackTransform.visible = true;
     }
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 7583a19..809d4ee 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -30,6 +30,7 @@
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.MutableBoolean;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -77,7 +78,6 @@
 import com.android.systemui.recents.model.TaskStack;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
@@ -100,10 +100,12 @@
     private static final float SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
     private static final float HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
 
-    private static final int DEFAULT_SYNC_STACK_DURATION = 200;
+    public static final int DEFAULT_SYNC_STACK_DURATION = 200;
     private static final int DRAG_SCALE_DURATION = 175;
     private static final float DRAG_SCALE_FACTOR = 1.05f;
 
+    private static final ArraySet<Task.TaskKey> EMPTY_TASK_SET = new ArraySet<>();
+
     TaskStack mStack;
     TaskStackLayoutAlgorithm mLayoutAlgorithm;
     TaskStackViewScroller mStackScroller;
@@ -116,7 +118,8 @@
 
     ArrayList<TaskView> mTaskViews = new ArrayList<>();
     ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
-    TaskViewAnimation mDeferredTaskViewUpdateAnimation = null;
+    ArraySet<Task.TaskKey> mIgnoreTasks = new ArraySet<>();
+    TaskViewAnimation mDeferredTaskViewLayoutAnimation = null;
 
     DozeTrigger mUIDozeTrigger;
     Task mFocusedTask;
@@ -137,7 +140,6 @@
     int[] mTmpVisibleRange = new int[2];
     Rect mTmpRect = new Rect();
     ArrayMap<Task.TaskKey, TaskView> mTmpTaskViewMap = new ArrayMap<>();
-    ArraySet<Task> mTmpTaskSet = new ArraySet<>();
     List<TaskView> mTmpTaskViews = new ArrayList<>();
     TaskViewTransform mTmpTransform = new TaskViewTransform();
     LayoutInflater mInflater;
@@ -345,30 +347,82 @@
     }
 
     /**
-     * Gets the stack transforms of a list of tasks, and returns the visible range of tasks.
-     * This call ignores freeform tasks.
+     * Adds a task to the ignored set.
      */
-    private boolean updateStackTransforms(ArrayList<TaskViewTransform> taskTransforms,
-            ArrayList<Task> tasks, float stackScroll,
-            int[] visibleRangeOut, ArraySet<Task> ignoreTasksSet) {
-        int taskTransformCount = taskTransforms.size();
+    void addIgnoreTask(Task task) {
+        mIgnoreTasks.add(task.key);
+    }
+
+    /**
+     * Removes a task from the ignored set.
+     */
+    void removeIgnoreTask(Task task) {
+        mIgnoreTasks.remove(task.key);
+    }
+
+    /**
+     * Returns whether the specified {@param task} is ignored.
+     */
+    boolean isIgnoredTask(Task task) {
+        return mIgnoreTasks.contains(task.key);
+    }
+
+    /**
+     * Computes the task transforms at the current stack scroll for all visible tasks. If a valid
+     * target stack scroll is provided (ie. is different than {@param curStackScroll}), then the
+     * visible range includes all tasks at the target stack scroll. This is useful for ensure that
+     * all views necessary for a transition or animation will be visible at the start.
+     *
+     * This call ignores freeform tasks.
+     *
+     * @param taskTransforms The set of task view transforms to reuse, this list will be sized to
+     *                       match the size of {@param tasks}
+     * @param tasks The set of tasks for which to generate transforms
+     * @param curStackScroll The current stack scroll
+     * @param targetStackScroll The stack scroll that we anticipate we are going to be scrolling to.
+     *                          The range of the union of the visible views at the current and
+     *                          target stack scrolls will be returned.
+     * @param ignoreTasksSet The set of tasks to skip for purposes of calculaing the visible range.
+     *                       Transforms will still be calculated for the ignore tasks.
+     */
+    boolean computeVisibleTaskTransforms(ArrayList<TaskViewTransform> taskTransforms,
+            ArrayList<Task> tasks, float curStackScroll, float targetStackScroll,
+            int[] visibleRangeOut, ArraySet<Task.TaskKey> ignoreTasksSet) {
         int taskCount = tasks.size();
         int frontMostVisibleIndex = -1;
         int backMostVisibleIndex = -1;
+        boolean useTargetStackScroll = Float.compare(curStackScroll, targetStackScroll) != 0;
 
         // We can reuse the task transforms where possible to reduce object allocation
         Utilities.matchTaskListSize(tasks, taskTransforms);
 
         // Update the stack transforms
         TaskViewTransform frontTransform = null;
+        TaskViewTransform frontTransformAtTarget = null;
+        TaskViewTransform transform = null;
+        TaskViewTransform transformAtTarget = null;
         for (int i = taskCount - 1; i >= 0; i--) {
             Task task = tasks.get(i);
-            if (ignoreTasksSet.contains(task)) {
-                continue;
+
+            // Calculate the current and (if necessary) the target transform for the task
+            transform = mLayoutAlgorithm.getStackTransform(task, curStackScroll,
+                    taskTransforms.get(i), frontTransform);
+            if (useTargetStackScroll && !transform.visible) {
+                // If we have a target stack scroll and the task is not currently visible, then we
+                // just update the transform at the new scroll
+                // TODO: Optimize this
+                transformAtTarget = mLayoutAlgorithm.getStackTransform(task,
+                        targetStackScroll, new TaskViewTransform(), frontTransformAtTarget);
+                if (transformAtTarget.visible) {
+                    transform.copyFrom(transformAtTarget);
+                }
             }
 
-            TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
-                    taskTransforms.get(i), frontTransform);
+            // For ignore tasks, only calculate the stack transform and skip the calculation of the
+            // visible stack indices
+            if (ignoreTasksSet.contains(task.key)) {
+                continue;
+            }
 
             // For freeform tasks, only calculate the stack transform and skip the calculation of
             // the visible stack indices
@@ -392,7 +446,9 @@
                     break;
                 }
             }
+
             frontTransform = transform;
+            frontTransformAtTarget = transformAtTarget;
         }
         if (visibleRangeOut != null) {
             visibleRangeOut[0] = frontMostVisibleIndex;
@@ -402,33 +458,48 @@
     }
 
     /**
-     * Updates the children {@link TaskView}s to match the tasks in the current {@link TaskStack}.
-     * This call does not update the {@link TaskView}s to their position in the layout except when
-     * they are initially picked up from the pool, when they will be placed in a suitable initial
-     * position.
+     * Binds the visible {@link TaskView}s at the given target scroll.
+     *
+     * @see #bindVisibleTaskViews(float, ArraySet<Task.TaskKey>)
      */
-    private void bindTaskViewsWithStack(ArraySet<Task> ignoreTasksSet) {
-        final float stackScroll = mStackScroller.getStackScroll();
+    void bindVisibleTaskViews(float targetStackScroll) {
+        bindVisibleTaskViews(targetStackScroll, mIgnoreTasks);
+    }
+
+    /**
+     * Synchronizes the set of children {@link TaskView}s to match the visible set of tasks in the
+     * current {@link TaskStack}. This call does not continue on to update their position to the
+     * computed {@link TaskViewTransform}s of the visible range, but only ensures that they will
+     * be added/removed from the view hierarchy and placed in the correct Z order and initial
+     * position (if not currently on screen).
+     *
+     * @param targetStackScroll If provided, will ensure that the set of visible {@link TaskView}s
+     *                          includes those visible at the current stack scroll, and all at the
+     *                          target stack scroll.
+     * @param ignoreTasksSet The set of tasks to ignore in this rebinding of the visible
+     *                       {@link TaskView}s
+     */
+    void bindVisibleTaskViews(float targetStackScroll, ArraySet<Task.TaskKey> ignoreTasksSet) {
         final int[] visibleStackRange = mTmpVisibleRange;
 
         // Get all the task transforms
         final ArrayList<Task> tasks = mStack.getStackTasks();
-        final boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms,
-                tasks, stackScroll, visibleStackRange, ignoreTasksSet);
+        final boolean isValidVisibleRange = computeVisibleTaskTransforms(mCurrentTaskTransforms,
+                tasks, mStackScroller.getStackScroll(), targetStackScroll, visibleStackRange,
+                ignoreTasksSet);
 
         // Return all the invisible children to the pool
-        final List<TaskView> taskViews = getTaskViews();
-        final int taskViewCount = taskViews.size();
-        int lastFocusedTaskIndex = -1;
         mTmpTaskViewMap.clear();
-        mTmpTaskViewMap.ensureCapacity(tasks.size());
+        List<TaskView> taskViews = getTaskViews();
+        int lastFocusedTaskIndex = -1;
+        int taskViewCount = taskViews.size();
         for (int i = taskViewCount - 1; i >= 0; i--) {
-            final TaskView tv = taskViews.get(i);
-            final Task task = tv.getTask();
-            final int taskIndex = mStack.indexOfStackTask(task);
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            int taskIndex = mStack.indexOfStackTask(task);
 
             // Skip ignored tasks
-            if (ignoreTasksSet.contains(task)) {
+            if (ignoreTasksSet.contains(task.key)) {
                 continue;
             }
 
@@ -445,13 +516,13 @@
         }
 
         // Pick up all the newly visible children
-        int lastVisStackIndex = isValidVisibleStackRange ? visibleStackRange[1] : 0;
-        for (int i = mStack.getTaskCount() - 1; i >= lastVisStackIndex; i--) {
-            final Task task = tasks.get(i);
-            final TaskViewTransform transform = mCurrentTaskTransforms.get(i);
+        int lastVisStackIndex = isValidVisibleRange ? visibleStackRange[1] : 0;
+        for (int i = tasks.size() - 1; i >= lastVisStackIndex; i--) {
+            Task task = tasks.get(i);
+            TaskViewTransform transform = mCurrentTaskTransforms.get(i);
 
             // Skip ignored tasks
-            if (ignoreTasksSet.contains(task)) {
+            if (ignoreTasksSet.contains(task.key)) {
                 continue;
             }
 
@@ -502,26 +573,34 @@
     }
 
     /**
-     * Cancels any existing {@link TaskView} animations, and updates each {@link TaskView} to its
-     * current position as defined by the {@link TaskStackLayoutAlgorithm}.
+     * Relayout the the visible {@link TaskView}s to their current transforms as specified by the
+     * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any
+     * animations that are current running on those task views, and will ensure that the children
+     * {@link TaskView}s will match the set of visible tasks in the stack.
      *
-     * @param ignoreTasks the set of tasks to ignore in the relayout
+     * @see #relayoutTaskViews(TaskViewAnimation, ArraySet<Task.TaskKey>)
      */
-    private void updateTaskViewsToLayout(TaskViewAnimation animation, Task... ignoreTasks) {
-        // Keep track of the ignore tasks
-        ArraySet<Task> ignoreTasksSet = mTmpTaskSet;
-        ignoreTasksSet.clear();
-        ignoreTasksSet.ensureCapacity(ignoreTasks.length);
-        Collections.addAll(ignoreTasksSet, ignoreTasks);
+    void relayoutTaskViews(TaskViewAnimation animation) {
+        relayoutTaskViews(animation, mIgnoreTasks);
+    }
 
+    /**
+     * Relayout the the visible {@link TaskView}s to their current transforms as specified by the
+     * {@link TaskStackLayoutAlgorithm} with the given {@param animation}. This call cancels any
+     * animations that are current running on those task views, and will ensure that the children
+     * {@link TaskView}s will match the set of visible tasks in the stack.
+     *
+     * @param ignoreTasksSet the set of tasks to ignore in the relayout
+     */
+    void relayoutTaskViews(TaskViewAnimation animation, ArraySet<Task.TaskKey> ignoreTasksSet) {
         // If we had a deferred animation, cancel that
-        mDeferredTaskViewUpdateAnimation = null;
+        mDeferredTaskViewLayoutAnimation = null;
 
         // Cancel all task view animations
         cancelAllTaskViewAnimations();
 
-        // Fetch the current set of TaskViews
-        bindTaskViewsWithStack(ignoreTasksSet);
+        // Synchronize the current set of TaskViews
+        bindVisibleTaskViews(mStackScroller.getStackScroll(), ignoreTasksSet);
 
         // Animate them to their final transforms with the given animation
         List<TaskView> taskViews = getTaskViews();
@@ -531,7 +610,7 @@
             final int taskIndex = mStack.indexOfStackTask(tv.getTask());
             final TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex);
 
-            if (ignoreTasksSet.contains(tv.getTask())) {
+            if (ignoreTasksSet.contains(tv.getTask().key)) {
                 continue;
             }
 
@@ -542,8 +621,8 @@
     /**
      * Posts an update to synchronize the {@link TaskView}s with the stack on the next frame.
      */
-    private void updateTaskViewsToLayoutOnNextFrame(TaskViewAnimation animation) {
-        mDeferredTaskViewUpdateAnimation = animation;
+    void relayoutTaskViewsOnNextFrame(TaskViewAnimation animation) {
+        mDeferredTaskViewLayoutAnimation = animation;
         postInvalidateOnAnimation();
     }
 
@@ -558,13 +637,62 @@
     }
 
     /**
-     * Cancels all {@link TaskView} animations.
+     * Returns the current task transforms of all tasks, falling back to the stack layout if there
+     * is no {@link TaskView} for the task.
      */
-    private void cancelAllTaskViewAnimations() {
+    public void getCurrentTaskTransforms(ArrayList<Task> tasks,
+            ArrayList<TaskViewTransform> transformsOut) {
+        Utilities.matchTaskListSize(tasks, transformsOut);
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            Task task = tasks.get(i);
+            TaskViewTransform transform = transformsOut.get(i);
+            TaskView tv = getChildViewForTask(task);
+            if (tv != null) {
+                transform.fillIn(tv);
+            } else {
+                mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(),
+                        transform, null);
+            }
+            transform.visible = true;
+        }
+    }
+
+    /**
+     * Returns the task transforms for all the tasks in the stack if the stack was at the given
+     * {@param stackScroll}.
+     */
+    public void getLayoutTaskTransforms(float stackScroll, ArrayList<Task> tasks,
+            ArrayList<TaskViewTransform> transformsOut) {
+        Utilities.matchTaskListSize(tasks, transformsOut);
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            Task task = tasks.get(i);
+            TaskViewTransform transform = transformsOut.get(i);
+            mLayoutAlgorithm.getStackTransform(task, stackScroll, transform, null);
+            transform.visible = true;
+        }
+    }
+
+    /**
+     * Cancels all {@link TaskView} animations.
+     *
+     * @see #cancelAllTaskViewAnimations(ArraySet<Task.TaskKey>)
+     */
+    void cancelAllTaskViewAnimations() {
+        cancelAllTaskViewAnimations(mIgnoreTasks);
+    }
+
+    /**
+     * Cancels all {@link TaskView} animations.
+     *
+     * @param ignoreTasksSet The set of tasks to continue running their animations.
+     */
+    void cancelAllTaskViewAnimations(ArraySet<Task.TaskKey> ignoreTasksSet) {
         List<TaskView> taskViews = getTaskViews();
         for (int i = taskViews.size() - 1; i >= 0; i--) {
             final TaskView tv = taskViews.get(i);
-            tv.cancelTransformAnimation();
+            if (!ignoreTasksSet.contains(tv.getTask().key)) {
+                tv.cancelTransformAnimation();
+            }
         }
     }
 
@@ -577,11 +705,22 @@
         // Update the clip on each task child
         List<TaskView> taskViews = getTaskViews();
         TaskView tmpTv = null;
+        TaskView prevVisibleTv = null;
         int taskViewCount = taskViews.size();
         for (int i = 0; i < taskViewCount; i++) {
             TaskView tv = taskViews.get(i);
             TaskView frontTv = null;
             int clipBottom = 0;
+
+            if (mIgnoreTasks.contains(tv.getTask().key)) {
+                // For each of the ignore tasks, update the translationZ of its TaskView to be
+                // between the translationZ of the tasks immediately underneath it
+                if (prevVisibleTv != null) {
+                    tv.setTranslationZ(Math.max(tv.getTranslationZ(),
+                            prevVisibleTv.getTranslationZ() + 0.1f));
+                }
+            }
+
             if (i < (taskViewCount - 1) && tv.shouldClipViewInStack()) {
                 // Find the next view to clip against
                 for (int j = i + 1; j < taskViewCount; j++) {
@@ -609,33 +748,37 @@
             if (!config.useHardwareLayers) {
                 tv.mThumbnailView.updateThumbnailVisibility(clipBottom - tv.getPaddingBottom());
             }
+            prevVisibleTv = tv;
         }
         mTaskViewsClipDirty = false;
     }
 
     /**
+     * Updates the layout algorithm min and max virtual scroll bounds.
+     *
+     * @see #updateLayoutAlgorithm(boolean, ArraySet<Task.TaskKey>)
+     */
+    void updateLayoutAlgorithm(boolean boundScrollToNewMinMax) {
+        updateLayoutAlgorithm(boundScrollToNewMinMax, mIgnoreTasks);
+    }
+
+    /**
      * Updates the min and max virtual scroll bounds.
      *
-     * @param ignoreTasks the set of tasks to ignore in the relayout
+     * @param ignoreTasksSet the set of tasks to ignore in the relayout
      */
-    void updateLayout(boolean boundScrollToNewMinMax, Task... ignoreTasks) {
-        // Keep track of the ingore tasks
-        ArraySet<Task> ignoreTasksSet = mTmpTaskSet;
-        ignoreTasksSet.clear();
-        ignoreTasksSet.ensureCapacity(ignoreTasks.length);
-        Collections.addAll(ignoreTasksSet, ignoreTasks);
-
+    void updateLayoutAlgorithm(boolean boundScrollToNewMinMax,
+            ArraySet<Task.TaskKey> ignoreTasksSet) {
         // Compute the min and max scroll values
         mLayoutAlgorithm.update(mStack, ignoreTasksSet);
 
-        // Update the freeform workspace
+        // Update the freeform workspace background
         SystemServicesProxy ssp = Recents.getSystemServices();
         if (ssp.hasFreeformWorkspaceSupport()) {
             mTmpRect.set(mLayoutAlgorithm.mFreeformRect);
             mFreeformWorkspaceBackground.setBounds(mTmpRect);
         }
 
-        // Debug logging
         if (boundScrollToNewMinMax) {
             mStackScroller.boundScroll();
         }
@@ -671,8 +814,6 @@
 
         // Reset the last focused task state if changed
         if (mFocusedTask != null) {
-            resetFocusedTask(mFocusedTask);
-
             // Cancel the timer indicator, if applicable
             if (showTimerIndicator) {
                 final TaskView tv = getChildViewForTask(mFocusedTask);
@@ -680,6 +821,8 @@
                     tv.getHeaderView().cancelFocusTimerIndicator();
                 }
             }
+
+            resetFocusedTask(mFocusedTask);
         }
 
         boolean willScroll = false;
@@ -937,10 +1080,10 @@
             // Notify accessibility
             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
         }
-        if (mDeferredTaskViewUpdateAnimation != null) {
-            updateTaskViewsToLayout(mDeferredTaskViewUpdateAnimation);
+        if (mDeferredTaskViewLayoutAnimation != null) {
+            relayoutTaskViews(mDeferredTaskViewLayoutAnimation);
             mTaskViewsClipDirty = true;
-            mDeferredTaskViewUpdateAnimation = null;
+            mDeferredTaskViewLayoutAnimation = null;
         }
         if (mTaskViewsClipDirty) {
             clipTaskViews();
@@ -948,30 +1091,16 @@
     }
 
     /**
-     * Computes the stack and task rects.
-     *
-     * @param ignoreTasks the set of tasks to ignore in the relayout
-     */
-    public void computeRects(Rect taskStackBounds, boolean boundScroll, Task... ignoreTasks) {
-        // Compute the rects in the stack algorithm
-        mLayoutAlgorithm.initialize(taskStackBounds,
-                TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
-
-        // Update the scroll bounds
-        updateLayout(boundScroll, ignoreTasks);
-    }
-
-    /**
      * This is ONLY used from the Recents component to update the dummy stack view for purposes
      * of getting the task rect to animate to.
      */
     public void updateLayoutForStack(TaskStack stack) {
         mStack = stack;
-        updateLayout(false);
+        updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET);
     }
 
     /**
-     * Computes the maximum number of visible tasks and thumbnails.  Requires that
+     * Computes the maximum number of visible tasks and thumbnails. Requires that
      * updateLayoutForStack() is called first.
      */
     public TaskStackLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() {
@@ -1002,16 +1131,18 @@
         int width = MeasureSpec.getSize(widthMeasureSpec);
         int height = MeasureSpec.getSize(heightMeasureSpec);
 
-        // Compute our stack/task rects
-        computeRects(mStackBounds, false);
+        // Compute the rects in the stack algorithm
+        mLayoutAlgorithm.initialize(mStackBounds,
+                TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
+        updateLayoutAlgorithm(false /* boundScroll */, EMPTY_TASK_SET);
 
         // If this is the first layout, then scroll to the front of the stack, then update the
         // TaskViews with the stack so that we can lay them out
         if (mAwaitingFirstLayout) {
             mStackScroller.setStackScrollToInitialState();
         }
-        mTmpTaskSet.clear();
-        bindTaskViewsWithStack(mTmpTaskSet);
+        // Rebind all the views, including the ignore ones
+        bindVisibleTaskViews(mStackScroller.getStackScroll(), EMPTY_TASK_SET);
 
         // Measure each of the TaskViews
         mTmpTaskViews.clear();
@@ -1066,7 +1197,8 @@
                 mStackScroller.boundScroll();
             }
         }
-        updateTaskViewsToLayout(TaskViewAnimation.IMMEDIATE);
+        // Relayout all of the task views including the ignored ones
+        relayoutTaskViews(TaskViewAnimation.IMMEDIATE, EMPTY_TASK_SET);
         clipTaskViews();
 
         if (mAwaitingFirstLayout || !mEnterAnimationComplete) {
@@ -1088,9 +1220,12 @@
 
         // Set the task focused state without requesting view focus, and leave the focus animations
         // until after the enter-animation
+        Task launchTask = mStack.getLaunchTarget();
         RecentsConfiguration config = Recents.getConfiguration();
         RecentsActivityLaunchState launchState = config.getLaunchState();
-        int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
+        int focusedTaskIndex = launchTask != null
+                ? mStack.indexOfStackTask(launchTask)
+                : launchState.getInitialFocusTaskIndex(mStack.getTaskCount());
         if (focusedTaskIndex != -1) {
             setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
                     false /* requestViewFocus */);
@@ -1106,8 +1241,29 @@
     }
 
     public boolean isTouchPointInView(float x, float y, TaskView tv) {
-        return (tv.getLeft() <= x && x <= tv.getRight()) &&
-                (tv.getTop() <= y && y <= tv.getBottom());
+        mTmpRect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom());
+        mTmpRect.offset((int) tv.getTranslationX(), (int) tv.getTranslationY());
+        return mTmpRect.contains((int) x, (int) y);
+    }
+
+    /**
+     * Returns a non-ignored task in the {@param tasks} list that can be used as an achor when
+     * calculating the scroll position before and after a layout change.
+     */
+    public Task findAnchorTask(List<Task> tasks, MutableBoolean isFrontMostTask) {
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            Task task = tasks.get(i);
+
+            // Ignore deleting tasks
+            if (mIgnoreTasks.contains(task.key)) {
+                if (i == tasks.size() - 1) {
+                    isFrontMostTask.value = true;
+                }
+                continue;
+            }
+            return task;
+        }
+        return null;
     }
 
     @Override
@@ -1152,70 +1308,38 @@
     @Override
     public void onStackTaskAdded(TaskStack stack, Task newTask) {
         // Update the min/max scroll and animate other task views into their new positions
-        updateLayout(true);
+        updateLayoutAlgorithm(true /* boundScroll */);
 
         // Animate all the tasks into place
-        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+        relayoutTaskViews(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
                 mFastOutSlowInInterpolator));
     }
 
+    /**
+     * We expect that the {@link TaskView} associated with the removed task is already hidden.
+     */
     @Override
     public void onStackTaskRemoved(TaskStack stack, Task removedTask, boolean wasFrontMostTask,
-            Task newFrontMostTask) {
+            Task newFrontMostTask, TaskViewAnimation animation) {
         if (mFocusedTask == removedTask) {
             resetFocusedTask(removedTask);
         }
 
-        if (!removedTask.isFreeformTask()) {
-            // Remove the view associated with this task, we can't rely on updateTransforms
-            // to work here because the task is no longer in the list
-            TaskView tv = getChildViewForTask(removedTask);
-            if (tv != null) {
-                mViewPool.returnViewToPool(tv);
-            }
-
-            // Get the stack scroll of the task to anchor to (since we are removing something, the
-            // front most task will be our anchor task)
-            Task anchorTask = mStack.getStackFrontMostTask();
-            float prevAnchorTaskScroll = 0;
-            if (anchorTask != null) {
-                prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
-            }
-
-            // Update the min/max scroll and animate other task views into their new positions
-            updateLayout(true);
-
-            if (wasFrontMostTask) {
-                // Since the max scroll progress is offset from the bottom of the stack, just scroll
-                // to ensure that the new front most task is now fully visible
-                mStackScroller.setStackScroll(mLayoutAlgorithm.mMaxScrollP);
-            } else if (anchorTask != null) {
-                // Otherwise, offset the scroll by the movement of the anchor task
-                float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask);
-                float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll);
-                if (mLayoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) {
-                    // If we are focused, we don't want the front task to move, but otherwise, we
-                    // allow the back task to move up, and the front task to move back
-                    stackScrollOffset /= 2;
-                }
-                mStackScroller.setStackScroll(mStackScroller.getStackScroll() + stackScrollOffset);
-                mStackScroller.boundScroll();
-            }
-        } else {
-            // Remove the view associated with this task, we can't rely on updateTransforms
-            // to work here because the task is no longer in the list
-            TaskView tv = getChildViewForTask(removedTask);
-            if (tv != null) {
-                mViewPool.returnViewToPool(tv);
-            }
-
-            // Update the min/max scroll and animate other task views into their new positions
-            updateLayout(true);
+        // Remove the view associated with this task, we can't rely on updateTransforms
+        // to work here because the task is no longer in the list
+        TaskView tv = getChildViewForTask(removedTask);
+        if (tv != null) {
+            mViewPool.returnViewToPool(tv);
         }
 
-        // Animate all the tasks into place
-        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
-                mFastOutSlowInInterpolator));
+        // Remove the task from the ignored set
+        removeIgnoreTask(removedTask);
+
+        // If requested, relayout with the given animation
+        if (animation != null) {
+            updateLayoutAlgorithm(true /* boundScroll */);
+            relayoutTaskViews(animation);
+        }
 
         // Update the new front most task's action button
         if (mScreenPinningEnabled && newFrontMostTask != null) {
@@ -1232,7 +1356,8 @@
     }
 
     @Override
-    public void onHistoryTaskRemoved(TaskStack stack, Task removedTask) {
+    public void onHistoryTaskRemoved(TaskStack stack, Task removedTask,
+            TaskViewAnimation animation) {
         // To be implemented
     }
 
@@ -1316,7 +1441,10 @@
 
     @Override
     public void onTaskViewClipStateChanged(TaskView tv) {
-        clipTaskViews();
+        if (!mTaskViewsClipDirty) {
+            mTaskViewsClipDirty = true;
+            invalidate();
+        }
     }
 
     /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/
@@ -1324,7 +1452,9 @@
     @Override
     public void onScrollChanged(float prevScroll, float curScroll, TaskViewAnimation animation) {
         mUIDozeTrigger.poke();
-        updateTaskViewsToLayoutOnNextFrame(animation);
+        if (animation != null) {
+            relayoutTaskViewsOnNextFrame(animation);
+        }
 
         if (shouldShowHistoryButton() &&
                 prevScroll > SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD &&
@@ -1354,7 +1484,7 @@
                     tv.dismissTask();
                 } else {
                     // Otherwise, remove the task from the stack immediately
-                    mStack.removeTask(t);
+                    mStack.removeTask(t, TaskViewAnimation.IMMEDIATE);
                 }
             }
         }
@@ -1402,7 +1532,7 @@
     }
 
     public final void onBusEvent(TaskViewDismissedEvent event) {
-        removeTaskViewFromStack(event.taskView);
+        removeTaskViewFromStack(event.taskView, event.task);
         EventBus.getDefault().send(new DeleteTaskDataEvent(event.task));
     }
 
@@ -1419,7 +1549,10 @@
         // Poke the doze trigger on user interaction
         mUIDozeTrigger.poke();
         if (event.showTimerIndicator && mFocusedTask != null) {
-            getChildViewForTask(mFocusedTask).getHeaderView().cancelFocusTimerIndicator();
+            TaskView tv = getChildViewForTask(mFocusedTask);
+            if (tv != null) {
+                tv.getHeaderView().cancelFocusTimerIndicator();
+            }
         }
     }
 
@@ -1455,23 +1588,29 @@
     }
 
     public final void onBusEvent(DragDropTargetChangedEvent event) {
+        TaskViewAnimation animation = new TaskViewAnimation(250, mFastOutSlowInInterpolator);
         if (event.dropTarget instanceof TaskStack.DockState) {
             // Calculate the new task stack bounds that matches the window size that Recents will
             // have after the drop
+            addIgnoreTask(event.task);
             final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget;
             mStackBounds.set(dockState.getDockedTaskStackBounds(getMeasuredWidth(),
                     getMeasuredHeight(), mDividerSize, mLayoutAlgorithm.mSystemInsets,
                     getResources()));
-            computeRects(mStackBounds, true /* boundScroll */, event.task /* ignoreTask */);
-            updateTaskViewsToLayout(new TaskViewAnimation(250, mFastOutSlowInInterpolator),
-                    event.task /* ignoreTask */);
+            mLayoutAlgorithm.initialize(mStackBounds,
+                    TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
+            updateLayoutAlgorithm(true /* boundScroll */);
         } else {
-            // Restore the pre-drag task stack bounds
+            // Restore the pre-drag task stack bounds, but ensure that we don't layout the dragging
+            // task view, so add it back to the ignore set after updating the layout
             mStackBounds.set(mStableStackBounds);
-            computeRects(mStackBounds, true /* boundScroll */);
-            updateTaskViewsToLayout(new TaskViewAnimation(250, mFastOutSlowInInterpolator),
-                    event.task /* ignoreTask */);
+            removeIgnoreTask(event.task);
+            mLayoutAlgorithm.initialize(mStackBounds,
+                    TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
+            updateLayoutAlgorithm(true /* boundScroll */);
+            addIgnoreTask(event.task);
         }
+        relayoutTaskViews(animation);
     }
 
     public final void onBusEvent(final DragEndEvent event) {
@@ -1487,14 +1626,14 @@
 
         if (hasChangedStacks) {
             // Move the task to the right position in the stack (ie. the front of the stack if
-            // freeform or the front of the stack if fullscreen).  Note, we MUST move the tasks
+            // freeform or the front of the stack if fullscreen). Note, we MUST move the tasks
             // before we update their stack ids, otherwise, the keys will have changed.
             if (event.dropTarget == mFreeformWorkspaceDropTarget) {
                 mStack.moveTaskToStack(event.task, FREEFORM_WORKSPACE_STACK_ID);
             } else if (event.dropTarget == mStackDropTarget) {
                 mStack.moveTaskToStack(event.task, FULLSCREEN_WORKSPACE_STACK_ID);
             }
-            updateLayout(true);
+            updateLayoutAlgorithm(true /* boundScroll */);
 
             // Move the task to the new stack in the system after the animation completes
             event.addPostAnimationCallback(new Runnable() {
@@ -1522,11 +1661,12 @@
         mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(),
                 mTmpTransform, null);
         event.getAnimationTrigger().increment();
-        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+        relayoutTaskViews(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
                 mFastOutSlowInInterpolator));
         updateTaskViewToTransform(event.taskView, mTmpTransform,
                 new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, mFastOutSlowInInterpolator,
                         event.getAnimationTrigger().decrementOnAnimationEnd()));
+        removeIgnoreTask(event.task);
     }
 
     public final void onBusEvent(StackViewScrolledEvent event) {
@@ -1593,15 +1733,14 @@
      * Removes the task from the stack, and updates the focus to the next task in the stack if the
      * removed TaskView was focused.
      */
-    private void removeTaskViewFromStack(TaskView tv) {
-        Task task = tv.getTask();
-
+    private void removeTaskViewFromStack(TaskView tv, Task task) {
         // Announce for accessibility
         tv.announceForAccessibility(getContext().getString(
-                R.string.accessibility_recents_item_dismissed, tv.getTask().title));
+                R.string.accessibility_recents_item_dismissed, task.title));
 
         // Remove the task from the stack
-        mStack.removeTask(task);
+        mStack.removeTask(task, new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+                mFastOutSlowInInterpolator));
     }
 
     /**
@@ -1622,7 +1761,7 @@
     }
 
     /**
-     * Returns the insert index for the task in the current set of task views.  If the given task
+     * Returns the insert index for the task in the current set of task views. If the given task
      * is already in the task view list, then this method returns the insert index assuming it
      * is first removed at the previous index.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
index 32f02ac..5335b14 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -151,6 +151,7 @@
 
     /** Animates the stack scroll into bounds */
     ObjectAnimator animateBoundScroll() {
+        // TODO: Take duration for snap back
         float curScroll = getStackScroll();
         float newScroll = getBoundedStackScroll(curScroll);
         if (Float.compare(newScroll, curScroll) != 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index 4813c19..e9f6f39 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -21,13 +21,16 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.util.Log;
+import android.util.ArrayMap;
+import android.util.MutableBoolean;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewParent;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
 import com.android.systemui.SwipeHelper;
@@ -37,19 +40,25 @@
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.ui.StackViewScrolledEvent;
 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
+import com.android.systemui.recents.misc.RectFEvaluator;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
+import com.android.systemui.recents.model.Task;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 
+import java.util.ArrayList;
 import java.util.List;
 
-/* Handles touch events for a TaskStackView. */
+/**
+ * Handles touch events for a TaskStackView.
+ */
 class TaskStackViewTouchHandler implements SwipeHelper.Callback {
 
-    private static final String TAG = "TaskStackViewTouchHandler";
-    private static final boolean DEBUG = false;
+    private static final int INACTIVE_POINTER_ID = -1;
 
-    private static int INACTIVE_POINTER_ID = -1;
+    private static final RectFEvaluator RECT_EVALUATOR = new RectFEvaluator();
+    private static final Interpolator STACK_TRANSFORM_INTERPOLATOR =
+            new PathInterpolator(0.73f, 0.33f, 0.42f, 0.85f);
 
     Context mContext;
     TaskStackView mSv;
@@ -74,6 +83,15 @@
     final int mWindowTouchSlop;
 
     private final StackViewScrolledEvent mStackViewScrolledEvent = new StackViewScrolledEvent();
+
+    // The current and final set of task transforms, sized to match the list of tasks in the stack
+    private ArrayList<Task> mCurrentTasks = new ArrayList<>();
+    private ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
+    private ArrayList<TaskViewTransform> mFinalTaskTransforms = new ArrayList<>();
+    private ArrayMap<View, Animator> mSwipeHelperAnimations = new ArrayMap<>();
+    private TaskViewTransform mTmpTransform = new TaskViewTransform();
+    private float mTargetStackScroll;
+
     SwipeHelper mSwipeHelper;
     boolean mInterceptedBySwipeHelper;
 
@@ -97,8 +115,14 @@
             }
 
             @Override
-            protected void updateSnapBackAnimation(Animator anim) {
+            protected void prepareDismissAnimation(View v, Animator anim) {
+                mSwipeHelperAnimations.put(v, anim);
+            }
+
+            @Override
+            protected void prepareSnapBackAnimation(View v, Animator anim) {
                 anim.setInterpolator(mSv.mFastOutSlowInInterpolator);
+                mSwipeHelperAnimations.put(v, anim);
             }
         };
         mSwipeHelper.setDisableHardwareLayers(true);
@@ -119,21 +143,6 @@
         }
     }
 
-    /** Returns the view at the specified coordinates */
-    TaskView findViewAtPoint(int x, int y) {
-        List<TaskView> taskViews = mSv.getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = taskViewCount - 1; i >= 0; i--) {
-            TaskView tv = taskViews.get(i);
-            if (tv.getVisibility() == View.VISIBLE) {
-                if (mSv.isTouchPointInView(x, y, tv)) {
-                    return tv;
-                }
-            }
-        }
-        return null;
-    }
-
     /** Touch preprocessing for handling below */
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         // Pass through to swipe helper if we are swiping
@@ -179,6 +188,15 @@
                 mScroller.stopBoundScrollAnimation();
                 Utilities.cancelAnimationWithoutCallbacks(mScrollFlingAnimator);
 
+                // Finish any existing task animations from the delete
+                mSv.cancelAllTaskViewAnimations();
+                // Finish any of the swipe helper animations
+                ArrayMap<View, Animator> existingAnimators = new ArrayMap<>(mSwipeHelperAnimations);
+                for (int i = 0; i < existingAnimators.size(); i++) {
+                    existingAnimators.get(existingAnimators.keyAt(i)).end();
+                }
+                mSwipeHelperAnimations.clear();
+
                 // Initialize the velocity tracker
                 initOrResetVelocityTracker();
                 mVelocityTracker.addMovement(ev);
@@ -214,9 +232,6 @@
                     float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y);
                     float curScrollP = mDownScrollP + deltaP;
                     mScroller.setStackScroll(curScrollP);
-                    if (DEBUG) {
-                        Log.d(TAG, "scroll: " + curScrollP);
-                    }
                     mStackViewScrolledEvent.updateY(y - mLastY);
                     EventBus.getDefault().send(mStackViewScrolledEvent);
                 }
@@ -343,12 +358,19 @@
 
     @Override
     public View getChildAtPosition(MotionEvent ev) {
-        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
+        TaskView tv = findViewAtPoint((int) ev.getX(), (int) ev.getY());
+        if (tv != null && canChildBeDismissed(tv)) {
+            return tv;
+        }
+        return null;
     }
 
     @Override
     public boolean canChildBeDismissed(View v) {
-        return true;
+        // Disallow dismissing an already dismissed task
+        TaskView tv = (TaskView) v;
+        return !mSwipeHelperAnimations.containsKey(v) &&
+                (mSv.getStack().indexOfStackTask(tv.getTask()) != -1);
     }
 
     @Override
@@ -364,34 +386,113 @@
         if (parent != null) {
             parent.requestDisallowInterceptTouchEvent(true);
         }
+
+        // Add this task to the set of tasks we are deleting
+        mSv.addIgnoreTask(tv.getTask());
+
+        // Determine if we are animating the other tasks while dismissing this task
+        mCurrentTasks = mSv.getStack().getStackTasks();
+        MutableBoolean isFrontMostTask = new MutableBoolean(false);
+        Task anchorTask = mSv.findAnchorTask(mCurrentTasks, isFrontMostTask);
+        TaskStackViewScroller stackScroller = mSv.getScroller();
+        if (anchorTask != null) {
+            // Get the current set of task transforms
+            mSv.getCurrentTaskTransforms(mCurrentTasks, mCurrentTaskTransforms);
+
+            // Get the stack scroll of the task to anchor to (since we are removing something, the
+            // front most task will be our anchor task)
+            float prevAnchorTaskScroll = 0;
+            boolean pullStackForward = mCurrentTasks.size() > 0;
+            if (pullStackForward) {
+                prevAnchorTaskScroll = mSv.getStackAlgorithm().getStackScrollForTask(anchorTask);
+            }
+
+            // Calculate where the views would be without the deleting tasks
+            mSv.updateLayoutAlgorithm(false /* boundScroll */);
+
+            float newStackScroll = stackScroller.getStackScroll();
+            if (isFrontMostTask.value) {
+                // Bound the stack scroll to pull tasks forward if necessary
+                newStackScroll = stackScroller.getBoundedStackScroll(newStackScroll);
+            } else if (pullStackForward) {
+                // Otherwise, offset the scroll by the movement of the anchor task
+                float anchorTaskScroll = mSv.getStackAlgorithm().getStackScrollForTask(anchorTask);
+                float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll);
+                if (mSv.getStackAlgorithm().getFocusState() !=
+                        TaskStackLayoutAlgorithm.STATE_FOCUSED) {
+                    // If we are focused, we don't want the front task to move, but otherwise, we
+                    // allow the back task to move up, and the front task to move back
+                    stackScrollOffset /= 2;
+                }
+                newStackScroll = stackScroller.getBoundedStackScroll(stackScroller.getStackScroll()
+                        + stackScrollOffset);
+            }
+
+            // Pick up the newly visible views, not including the deleting tasks
+            mSv.bindVisibleTaskViews(newStackScroll);
+
+            // Get the final set of task transforms (with task removed)
+            mSv.getLayoutTaskTransforms(newStackScroll, mCurrentTasks, mFinalTaskTransforms);
+
+            // Set the target to scroll towards upon dismissal
+            mTargetStackScroll = newStackScroll;
+
+            /*
+             * Post condition: All views that will be visible as a part of the gesture are retrieved
+             *                 and at their initial positions.  The stack is still at the current
+             *                 scroll, but the layout is updated without the task currently being
+             *                 dismissed.
+             */
+        }
     }
 
     @Override
     public boolean updateSwipeProgress(View v, boolean dismissable, float swipeProgress) {
+        updateTaskViewTransforms(getDismissFraction(v));
         return true;
     }
 
+    /**
+     * Called after the {@link TaskView} is finished animating away.
+     */
     @Override
     public void onChildDismissed(View v) {
         TaskView tv = (TaskView) v;
+
         // Re-enable clipping with the stack (we will reuse this view)
         tv.setClipViewInStack(true);
         // Re-enable touch events from this task view
         tv.setTouchEnabled(true);
+        // Update the scroll to the final scroll position from onBeginDrag()
+        mSv.getScroller().setStackScroll(mTargetStackScroll, null);
         // Remove the task view from the stack
         EventBus.getDefault().send(new TaskViewDismissedEvent(tv.getTask(), tv));
+        // Stop tracking this deletion animation
+        mSwipeHelperAnimations.remove(v);
         // Keep track of deletions by keyboard
         MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source",
                 Constants.Metrics.DismissSourceSwipeGesture);
     }
 
+    /**
+     * Called after the {@link TaskView} is finished animating back into the list.
+     * onChildDismissed() calls.
+     */
     @Override
     public void onChildSnappedBack(View v) {
         TaskView tv = (TaskView) v;
+
         // Re-enable clipping with the stack
         tv.setClipViewInStack(true);
         // Re-enable touch events from this task view
         tv.setTouchEnabled(true);
+
+        // Stop tracking this deleting task, and update the layout to include this task again.  The
+        // stack scroll does not need to be reset, since the scroll has not actually changed in
+        // onBeginDrag().
+        mSv.removeIgnoreTask(tv.getTask());
+        mSv.updateLayoutAlgorithm(false /* boundScroll */);
+        mSwipeHelperAnimations.remove(v);
     }
 
     @Override
@@ -414,4 +515,59 @@
         return 0;
     }
 
+    /**
+     * Interpolates the non-deleting tasks to their final transforms from their current transforms.
+     */
+    private void updateTaskViewTransforms(float dismissFraction) {
+        List<TaskView> taskViews = mSv.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+
+            if (mSv.isIgnoredTask(task)) {
+                continue;
+            }
+
+            int taskIndex = mCurrentTasks.indexOf(task);
+            TaskViewTransform fromTransform = mCurrentTaskTransforms.get(taskIndex);
+            TaskViewTransform toTransform = mFinalTaskTransforms.get(taskIndex);
+
+            mTmpTransform.copyFrom(fromTransform);
+            // We only really need to interpolate the bounds, progress and translation
+            mTmpTransform.rect.set(RECT_EVALUATOR.evaluate(dismissFraction, fromTransform.rect,
+                    toTransform.rect));
+            mTmpTransform.p = fromTransform.p + (toTransform.p - fromTransform.p) * dismissFraction;
+            mTmpTransform.translationZ = fromTransform.translationZ +
+                    (toTransform.translationZ - fromTransform.translationZ) * dismissFraction;
+
+            mSv.updateTaskViewToTransform(tv, mTmpTransform, TaskViewAnimation.IMMEDIATE);
+        }
+    }
+
+    /** Returns the view at the specified coordinates */
+    private TaskView findViewAtPoint(int x, int y) {
+        List<Task> tasks = mSv.getStack().getStackTasks();
+        int taskCount = tasks.size();
+        for (int i = taskCount - 1; i >= 0; i--) {
+            TaskView tv = mSv.getChildViewForTask(tasks.get(i));
+            if (tv != null && tv.getVisibility() == View.VISIBLE) {
+                if (mSv.isTouchPointInView(x, y, tv)) {
+                    return tv;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the fraction which we should interpolate the other task views based on the dismissal
+     * of this given task.
+     *
+     * TODO: We can interpolate this to adjust when the other tasks should respond to the dismissal
+     */
+    private float getDismissFraction(View v) {
+        float fraction = Math.min(1f, Math.abs(v.getTranslationX() / mSv.getWidth()));
+        return STACK_TRANSFORM_INTERPOLATOR.getInterpolation(fraction);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 9b72702..1f8216f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -162,6 +162,7 @@
 
     /** Resets this TaskView for reuse. */
     void reset() {
+        mHeaderView.reset();
         resetViewProperties();
         resetNoUserInteractionState();
         setClipViewInStack(false);
@@ -459,16 +460,14 @@
     public void showActionButton(boolean fadeIn, int fadeInDuration) {
         mActionButtonView.setVisibility(View.VISIBLE);
 
-        if (fadeIn) {
-            if (mActionButtonView.getAlpha() < 1f) {
-                mActionButtonView.animate()
-                        .alpha(1f)
-                        .scaleX(1f)
-                        .scaleY(1f)
-                        .setDuration(fadeInDuration)
-                        .setInterpolator(PhoneStatusBar.ALPHA_IN)
-                        .start();
-            }
+        if (fadeIn && mActionButtonView.getAlpha() < 1f) {
+            mActionButtonView.animate()
+                    .alpha(1f)
+                    .scaleX(1f)
+                    .scaleY(1f)
+                    .setDuration(fadeInDuration)
+                    .setInterpolator(PhoneStatusBar.ALPHA_IN)
+                    .start();
         } else {
             mActionButtonView.setScaleX(1f);
             mActionButtonView.setScaleY(1f);
@@ -484,29 +483,27 @@
      */
     public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
             final Animator.AnimatorListener animListener) {
-        if (fadeOut) {
-            if (mActionButtonView.getAlpha() > 0f) {
-                if (scaleDown) {
-                    float toScale = 0.9f;
-                    mActionButtonView.animate()
-                            .scaleX(toScale)
-                            .scaleY(toScale);
-                }
+        if (fadeOut && mActionButtonView.getAlpha() > 0f) {
+            if (scaleDown) {
+                float toScale = 0.9f;
                 mActionButtonView.animate()
-                        .alpha(0f)
-                        .setDuration(fadeOutDuration)
-                        .setInterpolator(PhoneStatusBar.ALPHA_OUT)
-                        .withEndAction(new Runnable() {
-                            @Override
-                            public void run() {
-                                if (animListener != null) {
-                                    animListener.onAnimationEnd(null);
-                                }
-                                mActionButtonView.setVisibility(View.INVISIBLE);
-                            }
-                        })
-                        .start();
+                        .scaleX(toScale)
+                        .scaleY(toScale);
             }
+            mActionButtonView.animate()
+                    .alpha(0f)
+                    .setDuration(fadeOutDuration)
+                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
+                    .withEndAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            if (animListener != null) {
+                                animListener.onAnimationEnd(null);
+                            }
+                            mActionButtonView.setVisibility(View.INVISIBLE);
+                        }
+                    })
+                    .start();
         } else {
             mActionButtonView.setAlpha(0f);
             mActionButtonView.setVisibility(View.INVISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
index e7717ac..2563190 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
@@ -16,22 +16,27 @@
 
 package com.android.systemui.recents.views;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.annotation.Nullable;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
 import android.graphics.Paint;
-import android.graphics.PorterDuff;
 import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.RippleDrawable;
-import android.support.v4.graphics.ColorUtils;
 import android.os.CountDownTimer;
+import android.support.v4.graphics.ColorUtils;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.ViewStub;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
@@ -39,10 +44,10 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 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.RecentsDebugFlags;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.LaunchTaskEvent;
 import com.android.systemui.recents.events.ui.ShowApplicationInfoEvent;
@@ -59,6 +64,8 @@
         implements View.OnClickListener, View.OnLongClickListener {
 
     private static final float HIGHLIGHT_LIGHTNESS_INCREMENT = 0.125f;
+    private static final float OVERLAY_LIGHTNESS_INCREMENT = -0.0625f;
+    private static final int OVERLAY_REVEAL_DURATION = 250;
     private static final long FOCUS_INDICATOR_INTERVAL_MS = 30;
 
     /**
@@ -69,8 +76,6 @@
         private Paint mHighlightPaint = new Paint();
         private Paint mBackgroundPaint = new Paint();
 
-        private float[] mTmpHSL = new float[3];
-
         public HighlightColorDrawable() {
             mBackgroundPaint.setColor(Color.argb(255, 0, 0, 0));
             mBackgroundPaint.setAntiAlias(true);
@@ -122,11 +127,16 @@
     Task mTask;
 
     // Header views
-    ImageView mMoveTaskButton;
-    ImageView mDismissButton;
     ImageView mIconView;
     TextView mTitleView;
-    int mMoveTaskTargetStackId = INVALID_STACK_ID;
+    ImageView mMoveTaskButton;
+    ImageView mDismissButton;
+    ViewStub mAppOverlayViewStub;
+    FrameLayout mAppOverlayView;
+    ImageView mAppIconView;
+    ImageView mAppInfoView;
+    TextView mAppTitleView;
+    ViewStub mFocusTimerIndicatorStub;
     ProgressBar mFocusTimerIndicator;
 
     // Header drawables
@@ -140,21 +150,26 @@
     Drawable mDarkFreeformIcon;
     Drawable mLightFullscreenIcon;
     Drawable mDarkFullscreenIcon;
+    Drawable mLightInfoIcon;
+    Drawable mDarkInfoIcon;
     int mTaskBarViewLightTextColor;
     int mTaskBarViewDarkTextColor;
+    int mMoveTaskTargetStackId = INVALID_STACK_ID;
 
     // Header background
     private HighlightColorDrawable mBackground;
+    private HighlightColorDrawable mOverlayBackground;
+    private float[] mTmpHSL = new float[3];
 
     // Header dim, which is only used when task view hardware layers are not used
     private Paint mDimLayerPaint = new Paint();
 
     Interpolator mFastOutSlowInInterpolator;
     Interpolator mFastOutLinearInInterpolator;
+    Interpolator mLinearOutSlowInInterpolator;
 
-    long mFocusIndicatorProgress;
     private CountDownTimer mFocusTimerCountDown;
-    long mFocusTimerDuration;
+    private long mFocusTimerDuration;
 
     public TaskViewHeader(Context context) {
         this(context, null);
@@ -184,36 +199,45 @@
         mDarkFreeformIcon = context.getDrawable(R.drawable.recents_move_task_freeform_dark);
         mLightFullscreenIcon = context.getDrawable(R.drawable.recents_move_task_fullscreen_light);
         mDarkFullscreenIcon = context.getDrawable(R.drawable.recents_move_task_fullscreen_dark);
+        mLightInfoIcon = context.getDrawable(R.drawable.recents_info_light);
+        mDarkInfoIcon = context.getDrawable(R.drawable.recents_info_dark);
 
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_slow_in);
         mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_linear_in);
+        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.linear_out_slow_in);
 
         // Configure the background and dim
         mBackground = new HighlightColorDrawable();
         mBackground.setColorAndDim(Color.argb(255, 0, 0, 0), 0f);
         setBackground(mBackground);
+        mOverlayBackground = new HighlightColorDrawable();
         mDimLayerPaint.setColor(Color.argb(255, 0, 0, 0));
         mDimLayerPaint.setAntiAlias(true);
         mFocusTimerDuration = res.getInteger(R.integer.recents_auto_advance_duration);
     }
 
+    /**
+     * Resets this header along with the TaskView.
+     */
+    public void reset() {
+        hideAppOverlay(true /* immediate */);
+    }
+
     @Override
     protected void onFinishInflate() {
         // Initialize the icon and description views
         mIconView = (ImageView) findViewById(R.id.icon);
+        mIconView.setClickable(false);
         mIconView.setOnLongClickListener(this);
         mTitleView = (TextView) findViewById(R.id.title);
         mDismissButton = (ImageView) findViewById(R.id.dismiss_task);
         mDismissButton.setOnClickListener(this);
         mMoveTaskButton = (ImageView) findViewById(R.id.move_task);
-        mFocusTimerIndicator = (ProgressBar) findViewById(R.id.focus_timer_indicator);
-
-        // Hide the backgrounds if they are ripple drawables
-        if (mIconView.getBackground() instanceof RippleDrawable) {
-            mIconView.setBackground(null);
-        }
+        mFocusTimerIndicatorStub = (ViewStub) findViewById(R.id.focus_timer_indicator_stub);
+        mAppOverlayViewStub = (ViewStub) findViewById(R.id.app_overlay_stub);
     }
 
     /**
@@ -228,6 +252,7 @@
 
         mTaskViewRect.set(0, 0, width, height);
         boolean updateMoveTaskButton = mMoveTaskButton.getVisibility() != View.GONE;
+        boolean isFreeformTask = (mTask != null) && mTask.isFreeformTask();
         int appIconWidth = mIconView.getMeasuredWidth();
         int activityDescWidth = (mTask != null)
                 ? (int) mTitleView.getPaint().measureText(mTask.title)
@@ -239,19 +264,20 @@
 
         // Priority-wise, we show the activity icon first, the dismiss icon if there is room, the
         // move-task icon if there is room, and then finally, the activity label if there is room
-        if (width < (appIconWidth + dismissIconWidth)) {
+        if (isFreeformTask && width < (appIconWidth + dismissIconWidth)) {
             mTitleView.setVisibility(View.INVISIBLE);
             if (updateMoveTaskButton) {
                 mMoveTaskButton.setVisibility(View.INVISIBLE);
             }
             mDismissButton.setVisibility(View.INVISIBLE);
-        } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth)) {
+        } else if (isFreeformTask && width < (appIconWidth + dismissIconWidth +
+                moveTaskIconWidth)) {
             mTitleView.setVisibility(View.INVISIBLE);
             if (updateMoveTaskButton) {
                 mMoveTaskButton.setVisibility(View.INVISIBLE);
             }
             mDismissButton.setVisibility(View.VISIBLE);
-        } else if (width < (appIconWidth + dismissIconWidth + moveTaskIconWidth +
+        } else if (isFreeformTask && width < (appIconWidth + dismissIconWidth + moveTaskIconWidth +
                 activityDescWidth)) {
             mTitleView.setVisibility(View.INVISIBLE);
             if (updateMoveTaskButton) {
@@ -273,11 +299,6 @@
     }
 
     @Override
-    protected boolean verifyDrawable(Drawable who) {
-        return super.verifyDrawable(who) || (who == mBackground);
-    }
-
-    @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
 
@@ -288,6 +309,10 @@
 
     /** Starts the focus timer. */
     public void startFocusTimerIndicator() {
+        if (mFocusTimerIndicator == null) {
+            return;
+        }
+
         mFocusTimerIndicator.setVisibility(View.VISIBLE);
         mFocusTimerIndicator.setMax((int) mFocusTimerDuration);
         if (mFocusTimerCountDown == null) {
@@ -308,7 +333,11 @@
 
     /** Cancels the focus timer. */
     public void cancelFocusTimerIndicator() {
-        if (mFocusTimerCountDown != null && mFocusTimerIndicator != null) {
+        if (mFocusTimerIndicator == null) {
+            return;
+        }
+
+        if (mFocusTimerCountDown != null) {
             mFocusTimerCountDown.cancel();
             mFocusTimerIndicator.setProgress(0);
             mFocusTimerIndicator.setVisibility(View.INVISIBLE);
@@ -337,6 +366,10 @@
     private void updateBackgroundColor(float dimAlpha) {
         if (mTask != null) {
             mBackground.setColorAndDim(mTask.colorPrimary, dimAlpha);
+            // TODO: Consider using the saturation of the color to adjust the lightness as well
+            ColorUtils.colorToHSL(mTask.colorPrimary, mTmpHSL);
+            mTmpHSL[2] = Math.min(1f, mTmpHSL[2] + OVERLAY_LIGHTNESS_INCREMENT * (1.0f - dimAlpha));
+            mOverlayBackground.setColorAndDim(ColorUtils.HSLToColor(mTmpHSL), dimAlpha);
             mDimLayerPaint.setAlpha((int) (dimAlpha * 255));
         }
     }
@@ -382,10 +415,15 @@
             mMoveTaskButton.setOnClickListener(this);
         }
 
-        mFocusTimerIndicator.getProgressDrawable()
-                .setColorFilter(
-                        getSecondaryColor(t.colorPrimary, t.useLightOnPrimaryColor),
-                        PorterDuff.Mode.SRC_IN);
+        if (Recents.getDebugFlags().isFastToggleIndicatorEnabled()) {
+            if (mFocusTimerIndicator == null) {
+                mFocusTimerIndicator = (ProgressBar) mFocusTimerIndicatorStub.inflate();
+            }
+            mFocusTimerIndicator.getProgressDrawable()
+                    .setColorFilter(
+                            getSecondaryColor(t.colorPrimary, t.useLightOnPrimaryColor),
+                            PorterDuff.Mode.SRC_IN);
+        }
 
         // In accessibility, a single click on the focused app info button will show it
         if (ssp.isTouchExplorationEnabled()) {
@@ -447,8 +485,11 @@
     @Override
     public void onClick(View v) {
         if (v == mIconView) {
-            // In accessibility, a single click on the focused app info button will show it
-            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+            SystemServicesProxy ssp = Recents.getSystemServices();
+            if (ssp.isTouchExplorationEnabled()) {
+                // In accessibility, a single click on the focused app info button will show it
+                EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+            }
         } else if (v == mDismissButton) {
             TaskView tv = Utilities.findParent(this, TaskView.class);
             tv.dismissTask();
@@ -463,15 +504,92 @@
                     : new Rect();
             EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds,
                     mMoveTaskTargetStackId, false));
+        } else if (v == mAppInfoView) {
+            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+        } else if (v == mAppIconView) {
+            hideAppOverlay(false /* immediate */);
         }
     }
 
     @Override
     public boolean onLongClick(View v) {
         if (v == mIconView) {
-            EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask));
+            showAppOverlay();
+            return true;
+        } else if (v == mAppIconView) {
+            hideAppOverlay(false /* immediate */);
             return true;
         }
         return false;
     }
+
+    /**
+     * Shows the application overlay.
+     */
+    private void showAppOverlay() {
+        // Skip early if the task is invalid
+        SystemServicesProxy ssp = Recents.getSystemServices();
+        ComponentName cn = mTask.key.getComponent();
+        int userId = mTask.key.userId;
+        ActivityInfo activityInfo = ssp.getActivityInfo(cn, userId);
+        if (activityInfo == null) {
+            return;
+        }
+
+        // Inflate the overlay if necessary
+        if (mAppOverlayView == null) {
+            mAppOverlayView = (FrameLayout) mAppOverlayViewStub.inflate();
+            mAppOverlayView.setBackground(mOverlayBackground);
+            mAppIconView = (ImageView) mAppOverlayView.findViewById(R.id.app_icon);
+            mAppIconView.setOnClickListener(this);
+            mAppIconView.setOnLongClickListener(this);
+            mAppInfoView = (ImageView) mAppOverlayView.findViewById(R.id.app_info);
+            mAppInfoView.setOnClickListener(this);
+            mAppTitleView = (TextView) mAppOverlayView.findViewById(R.id.app_title);
+        }
+
+        // Update the overlay contents for the current app
+        mAppTitleView.setText(ssp.getBadgedApplicationLabel(activityInfo.applicationInfo, userId));
+        mAppIconView.setImageDrawable(ssp.getBadgedApplicationIcon(activityInfo.applicationInfo, userId));
+        mAppInfoView.setImageDrawable(mTask.useLightOnPrimaryColor
+                ? mLightInfoIcon
+                : mDarkInfoIcon);
+        mAppOverlayView.setVisibility(View.VISIBLE);
+
+        int x = mIconView.getLeft() + mIconView.getWidth() / 2;
+        int y = mIconView.getTop() + mIconView.getHeight() / 2;
+        Animator revealAnim = ViewAnimationUtils.createCircularReveal(mAppOverlayView, x, y, 0,
+                getWidth());
+        revealAnim.setDuration(OVERLAY_REVEAL_DURATION);
+        revealAnim.setInterpolator(mLinearOutSlowInInterpolator);
+        revealAnim.start();
+    }
+
+    /**
+     * Hide the application overlay.
+     */
+    private void hideAppOverlay(boolean immediate) {
+        // Skip if we haven't even loaded the overlay yet
+        if (mAppOverlayView == null) {
+            return;
+        }
+
+        if (immediate) {
+            mAppOverlayView.setVisibility(View.GONE);
+        } else {
+            int x = mIconView.getLeft() + mIconView.getWidth() / 2;
+            int y = mIconView.getTop() + mIconView.getHeight() / 2;
+            Animator revealAnim = ViewAnimationUtils.createCircularReveal(mAppOverlayView, x, y,
+                    getWidth(), 0);
+            revealAnim.setDuration(OVERLAY_REVEAL_DURATION);
+            revealAnim.setInterpolator(mLinearOutSlowInInterpolator);
+            revealAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAppOverlayView.setVisibility(View.GONE);
+                }
+            });
+            revealAnim.start();
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
index 538c248..85b7c82 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
@@ -88,12 +88,39 @@
     public float alpha = 1f;
 
     public boolean visible = false;
-    float p = 0f;
+
+    // This is the relative task progress of this task, relative to the stack scroll at which this
+    // transform was computed
+    public float p = 0f;
 
     // This is a window-space rect used for positioning the task in the stack and freeform workspace
     public RectF rect = new RectF();
 
     /**
+     * Fills int this transform from the state of the given TaskView.
+     */
+    public void fillIn(TaskView tv) {
+        translationZ = tv.getTranslationZ();
+        scale = tv.getScaleX();
+        alpha = tv.getAlpha();
+        visible = true;
+        p = tv.getTaskProgress();
+        rect.set(tv.getLeft(), tv.getTop(), tv.getRight(), tv.getBottom());
+    }
+
+    /**
+     * Copies the transform state from another {@link TaskViewTransform}.
+     */
+    public void copyFrom(TaskViewTransform other) {
+        translationZ = other.translationZ;
+        scale = other.scale;
+        alpha = other.alpha;
+        visible = other.visible;
+        p = other.p;
+        rect.set(other.rect);
+    }
+
+    /**
      * Resets the current transform.
      */
     public void reset() {