Load some recent tasks

Bug: 69166452
Test: Build quickstep
Change-Id: Id4b0172256d6920616a6b9529d61abd1fe0c1a36
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index a76f4f9..ef50ac4 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/drawable/task_thumbnail_background.xml b/quickstep/res/drawable/task_thumbnail_background.xml
index 27efd6c..603380e 100644
--- a/quickstep/res/drawable/task_thumbnail_background.xml
+++ b/quickstep/res/drawable/task_thumbnail_background.xml
@@ -15,5 +15,5 @@
 -->
 <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
     <solid android:color="#FF000000" />
-    <corners android:radius="6dp" />
+    <corners android:radius="2dp" />
 </shape>
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index 521551c..a8b91c5 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -19,35 +19,8 @@
     android:theme="@style/HomeScreenElementTheme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingTop="20dp"
-    android:paddingBottom="20dp"
+    android:layout_gravity="center"
+    android:clipChildren="false"
     android:clipToPadding="false"
-    android:layout_gravity="center_horizontal|bottom"
-    android:gravity="top">
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:paddingStart="40dp"
-        android:paddingEnd="40dp"
-        android:orientation="horizontal">
-
-        <com.android.quickstep.SimpleTaskView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:background="#44FF0000"
-            android:layout_marginEnd="10dp"/>
-
-        <com.android.quickstep.SimpleTaskView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:background="#4400FF00"
-            android:layout_marginEnd="10dp"/>
-
-        <com.android.quickstep.SimpleTaskView
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:background="#440000FF" />
-    </LinearLayout>
-
-</com.android.quickstep.RecentsView>
\ No newline at end of file
+    android:alpha="0.0"
+    android:visibility="invisible" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 9d8aea7..fdf1adc 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -23,13 +23,14 @@
         android:id="@+id/snapshot"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:layout_marginTop="24dp"
         android:scaleType="matrix"
         android:background="@drawable/task_thumbnail_background"
         android:elevation="4dp" />
     <ImageView
         android:id="@+id/icon"
-        android:layout_width="36dp"
-        android:layout_height="36dp"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
         android:layout_gravity="top|center_horizontal"
         android:elevation="5dp"/>
 </com.android.quickstep.TaskView>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 9bdd7a3..26f5d5b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -46,12 +46,14 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        ((RecentsView) launcher.getOverviewPanel()).setViewVisible(true);
+        RecentsView rv = launcher.getOverviewPanel();
+        rv.setOverviewStateEnabled(true);
     }
 
     @Override
     public void onStateDisabled(Launcher launcher) {
-        ((RecentsView) launcher.getOverviewPanel()).setViewVisible(false);
+        RecentsView rv = launcher.getOverviewPanel();
+        rv.setOverviewStateEnabled(false);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index caeef50..0810579 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -40,8 +40,15 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.states.InternalStateHandler;
+import com.android.launcher3.uioverrides.OverviewState;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
 
 @TargetApi(Build.VERSION_CODES.O)
 public class NavBarSwipeInteractionHandler extends InternalStateHandler implements FrameCallback {
@@ -66,13 +73,15 @@
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
 
+    private final Rect mStableInsets = new Rect();
     private final Rect mSourceRect = new Rect();
     private final Rect mTargetRect = new Rect();
     private final Rect mCurrentRect = new Rect();
     private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
 
     private final Bitmap mTaskSnapshot;
-    private final RunningTaskInfo mTaskInfo;
+    private final int mRunningTaskId;
+    private Future<RecentsTaskLoadPlan> mFutureLoadPlan;
 
     private Launcher mLauncher;
     private Choreographer mChoreographer;
@@ -94,9 +103,10 @@
     private boolean mTouchEnded = false;
     private float mEndVelocity;
 
-    NavBarSwipeInteractionHandler(Bitmap taskSnapShot, RunningTaskInfo taskInfo) {
+    NavBarSwipeInteractionHandler(Bitmap taskSnapShot, RunningTaskInfo runningTaskInfo) {
         mTaskSnapshot = taskSnapShot;
-        mTaskInfo = taskInfo;
+        mRunningTaskId = runningTaskInfo.id;
+        WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
     }
 
     @Override
@@ -109,22 +119,39 @@
     }
 
     @Override
-    public void onNewIntent(Launcher launcher) {
+    public void onCreate(Launcher launcher) {
         mLauncher = launcher;
-
-        // Go immediately
-        launcher.getStateManager().goToState(LauncherState.OVERVIEW, false);
-
-        // Optimization
-        launcher.getAppsView().setVisibility(View.GONE);
-
-        mDragView = new SnapshotDragView(launcher, mTaskSnapshot);
-        launcher.getDragLayer().addView(mDragView);
+        mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
+        mLauncher.getDragLayer().addView(mDragView);
         mDragView.setPivotX(0);
         mDragView.setPivotY(0);
-        mRecentsView = launcher.getOverviewPanel();
-        mRecentsView.scrollTo(0, 0);
-        mHotseat = launcher.getHotseat();
+        mRecentsView = mLauncher.getOverviewPanel();
+        mHotseat = mLauncher.getHotseat();
+
+        // Optimization
+        mLauncher.getAppsView().setVisibility(View.GONE);
+
+        // Launch overview
+        mRecentsView.update(consumeLastLoadPlan());
+        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, false /* animate */);
+    }
+
+    @Override
+    public void onNewIntent(Launcher launcher, boolean alreadyOnHome) {
+        mLauncher = launcher;
+        mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
+        mLauncher.getDragLayer().addView(mDragView);
+        mDragView.setPivotX(0);
+        mDragView.setPivotY(0);
+        mRecentsView = mLauncher.getOverviewPanel();
+        mHotseat = mLauncher.getHotseat();
+
+        // Optimization
+        mLauncher.getAppsView().setVisibility(View.GONE);
+
+        // Launch overview, animate if already on home
+        mRecentsView.update(consumeLastLoadPlan());
+        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
     }
 
     @BinderThread
@@ -167,19 +194,46 @@
             // Init target rect.
             View targetView = ((ViewGroup) mRecentsView.getChildAt(0)).getChildAt(0);
             dl.getViewRectRelativeToSelf(targetView, mTargetRect);
+            mTargetRect.right = mTargetRect.left + mTargetRect.width();
+            mTargetRect.bottom = mTargetRect.top + mTargetRect.height();
             mSourceRect.set(0, 0, dl.getWidth(), dl.getHeight());
         }
 
-        mCurrentShift = shift;
-        int hotseatHeight = mHotseat.getHeight();
-        mHotseat.setTranslationY((1 - shift) * hotseatHeight);
+        if (!mSourceRect.isEmpty()) {
+            mCurrentShift = shift;
+            int hotseatHeight = mHotseat.getHeight();
+            mHotseat.setTranslationY((1 - shift) * hotseatHeight);
 
-        mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
+            mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
 
-        mDragView.setTranslationX(mCurrentRect.left);
-        mDragView.setTranslationY(mCurrentRect.top);
-        mDragView.setScaleX((float) mCurrentRect.width() / mSourceRect.width());
-        mDragView.setScaleY((float) mCurrentRect.width() / mSourceRect.width());
+            float scale = (float) mCurrentRect.width() / mSourceRect.width();
+            mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
+            mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
+            mDragView.setScaleX(scale);
+            mDragView.setScaleY(scale);
+            mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
+            mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
+        }
+    }
+
+    void setLastLoadPlan(Future<RecentsTaskLoadPlan> futureLoadPlan) {
+        if (mFutureLoadPlan != null) {
+            mFutureLoadPlan.cancel(true);
+        }
+        mFutureLoadPlan = futureLoadPlan;
+    }
+
+    private RecentsTaskLoadPlan consumeLastLoadPlan() {
+        try {
+            if (mFutureLoadPlan != null) {
+                return mFutureLoadPlan.get();
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            e.printStackTrace();
+        } finally {
+            mFutureLoadPlan = null;
+        }
+        return null;
     }
 
     @UiThread
@@ -229,9 +283,8 @@
         mHotseat.setTranslationY(0);
         mLauncher.setOnResumeCallback(() -> mDragView.close(false));
 
-        // TODO: Task key should be received from Recents model
-        TaskKey taskKey = new TaskKey(mTaskInfo.id, 0, null, UserHandle.myUserId(), 0);
-        ActivityManagerWrapper.getInstance()
-                .startActivityFromRecentsAsync(taskKey, null, null, null);
+        // TODO: For now, assume that the task stack will have loaded in the bg, will update
+        // the lib api later for direct call
+        mRecentsView.launchTaskWithId(mRunningTaskId);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index d7559da..528b11d 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -19,17 +19,43 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.widget.HorizontalScrollView;
+import android.view.LayoutInflater;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.RecentsTaskLoader;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.TaskStack;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.ArrayList;
 
 /**
- * A placeholder view for recents
+ * A list of recent tasks.
  */
-public class RecentsView extends HorizontalScrollView implements Insettable {
+public class RecentsView extends PagedView {
+
+    private boolean mOverviewStateEnabled;
+    private boolean mTaskStackListenerRegistered;
+
+    private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+        @Override
+        public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
+            for (int i = 0; i < getChildCount(); i++) {
+                final TaskView taskView = (TaskView) getChildAt(i);
+                if (taskView.getTask().key.id == taskId) {
+                    taskView.getThumbnail().setThumbnail(snapshot);
+                    return;
+                }
+            }
+        }
+    };
 
     public RecentsView(Context context) {
         this(context, null);
@@ -41,24 +67,103 @@
 
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setAlpha(0);
-        setVisibility(INVISIBLE);
+        setWillNotDraw(false);
+        setPageSpacing((int) getResources().getDimension(R.dimen.recents_page_spacing));
     }
 
-    public void setViewVisible(boolean isVisible) { }
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        // TODO: These are rough calculations which currently use the stable insets
+        DeviceProfile profile = Launcher.getLauncher(getContext()).getDeviceProfile();
+        Rect stableInsets = new Rect();
+        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
+        Rect padding = profile.getWorkspacePadding(null);
+        float taskWidth = profile.getCurrentWidth() - stableInsets.left - stableInsets.right;
+        float taskHeight = profile.getCurrentHeight() - stableInsets.top - stableInsets.bottom;
+        float overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
+                - stableInsets.top;
+        float overviewWidth = taskWidth * overviewHeight / taskHeight;
+        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
+        setPadding(padding.left, padding.top, padding.right, padding.bottom);
+    }
 
     @Override
-    public void setInsets(Rect insets) {
-        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
-        lp.topMargin = insets.top;
-        lp.bottomMargin = insets.bottom;
-        lp.leftMargin = insets.left;
-        lp.rightMargin = insets.right;
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        updateTaskStackListenerState();
+    }
 
-        DeviceProfile dp = Launcher.getLauncher(getContext()).getDeviceProfile();
-        if (!dp.isVerticalBarLayout()) {
-             lp.bottomMargin += dp.hotseatBarSizePx + getResources().getDimensionPixelSize(
-                     R.dimen.dynamic_grid_min_page_indicator_size);
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        updateTaskStackListenerState();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        updateTaskStackListenerState();
+    }
+
+    public void setOverviewStateEnabled(boolean enabled) {
+        mOverviewStateEnabled = enabled;
+        updateTaskStackListenerState();
+    }
+
+    public void update(RecentsTaskLoadPlan loadPlan) {
+        final RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+        setCurrentPage(0);
+        TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
+        if (stack == null) {
+            removeAllViews();
+            return;
+        }
+
+        // Ensure there are as many views as there are tasks in the stack (adding and trimming as
+        // necessary)
+        final LayoutInflater inflater = LayoutInflater.from(getContext());
+        final ArrayList<Task> tasks = stack.getTasks();
+        for (int i = getChildCount(); i < tasks.size(); i++) {
+            final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false);
+            addView(taskView);
+        }
+        while (getChildCount() > tasks.size()) {
+            final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1);
+            removeView(taskView);
+            loader.unloadTaskData(taskView.getTask());
+        }
+
+        // Rebind all task views
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            final Task task = tasks.get(i);
+            final TaskView taskView = (TaskView) getChildAt(tasks.size() - i - 1);
+            taskView.bind(task);
+            loader.loadTaskData(task);
+        }
+    }
+
+    public void launchTaskWithId(int taskId) {
+        for (int i = 0; i < getChildCount(); i++) {
+            final TaskView taskView = (TaskView) getChildAt(i);
+            if (taskView.getTask().key.id == taskId) {
+                taskView.launchTask(false /* animate */);
+                return;
+            }
+        }
+    }
+
+    private void updateTaskStackListenerState() {
+        boolean registerStackListener = mOverviewStateEnabled && isAttachedToWindow()
+                && getWindowVisibility() == VISIBLE;
+        if (registerStackListener != mTaskStackListenerRegistered) {
+            if (registerStackListener) {
+                ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
+            } else {
+                ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+            }
+            mTaskStackListenerRegistered = registerStackListener;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SnapshotDragView.java b/quickstep/src/com/android/quickstep/SnapshotDragView.java
index 791fe9f..2ef3942 100644
--- a/quickstep/src/com/android/quickstep/SnapshotDragView.java
+++ b/quickstep/src/com/android/quickstep/SnapshotDragView.java
@@ -23,6 +23,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
+import com.android.systemui.shared.recents.view.AnimateableViewBounds;
 
 /**
  * Floating view which shows the task snapshot allowing it to be dragged and placed.
@@ -31,12 +32,20 @@
 
     private final Launcher mLauncher;
     private final Bitmap mSnapshot;
+    private final AnimateableViewBounds mViewBounds;
 
     public SnapshotDragView(Launcher launcher, Bitmap snapshot) {
         super(launcher, null);
         mLauncher = launcher;
         mSnapshot = snapshot;
+        mViewBounds = new AnimateableViewBounds(this, 0);
         setWillNotDraw(false);
+        setClipToOutline(true);
+        setOutlineProvider(mViewBounds);
+    }
+
+    AnimateableViewBounds getViewBounds() {
+        return mViewBounds;
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 96c93c2..55d22e0 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -22,20 +22,17 @@
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
-import android.graphics.ColorMatrixColorFilter;
 import android.graphics.LightingColorFilter;
 import android.graphics.Matrix;
-import android.graphics.Outline;
 import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.Shader;
 import android.util.AttributeSet;
-import android.view.Display;
-import android.view.View;
-import android.view.ViewOutlineProvider;
 import android.widget.FrameLayout;
 
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
 /**
@@ -95,6 +92,14 @@
         }
     }
 
+    /**
+     * Sets the alpha of the dim layer on top of this view.
+     */
+    public void setDimAlpha(float dimAlpha) {
+        mDimAlpha = dimAlpha;
+        updateThumbnailPaintFilter();
+    }
+
     @Override
     protected void onDraw(Canvas canvas) {
         int viewWidth = getMeasuredWidth();
@@ -105,43 +110,40 @@
                 (int) (mThumbnailRect.height() * mThumbnailScale));
 
         if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
-            int topOffset = 0;
             // Draw the background, there will be some small overdraw with the thumbnail
             if (thumbnailWidth < viewWidth) {
                 // Portrait thumbnail on a landscape task view
-                canvas.drawRect(Math.max(0, thumbnailWidth), topOffset, viewWidth, viewHeight,
+                canvas.drawRect(Math.max(0, thumbnailWidth), 0, viewWidth, viewHeight,
                         mBgFillPaint);
             }
             if (thumbnailHeight < viewHeight) {
                 // Landscape thumbnail on a portrait task view
-                canvas.drawRect(0, Math.max(topOffset, thumbnailHeight), viewWidth, viewHeight,
+                canvas.drawRect(0, Math.max(0, thumbnailHeight), viewWidth, viewHeight,
                         mBgFillPaint);
             }
 
             // Draw the thumbnail
-            canvas.drawRect(0, topOffset, thumbnailWidth, thumbnailHeight, mDrawPaint);
+            canvas.drawRect(0, 0, thumbnailWidth, thumbnailHeight, mDrawPaint);
         } else {
             canvas.drawRect(0, 0, viewWidth, viewHeight, mBgFillPaint);
         }
     }
 
-    void updateThumbnailPaintFilter() {
-        int mul = (int) ((1.0f - mDimAlpha) * 255);
+    private void updateThumbnailPaintFilter() {
+        int mul = (int) (mDimAlpha * 255);
         if (mBitmapShader != null) {
-            mLightingColorFilter = new LightingColorFilter(Color.WHITE,
-                    Color.argb(255, mul, mul, mul));
+            mLightingColorFilter = new LightingColorFilter(Color.argb(255, mul, mul, mul), 0);
             mDrawPaint.setColorFilter(mLightingColorFilter);
             mDrawPaint.setColor(0xFFffffff);
             mBgFillPaint.setColorFilter(mLightingColorFilter);
         } else {
-            int grey = mul;
             mDrawPaint.setColorFilter(null);
-            mDrawPaint.setColor(Color.argb(255, grey, grey, grey));
+            mDrawPaint.setColor(Color.argb(255, mul, mul, mul));
         }
         invalidate();
     }
 
-    public void updateThumbnailMatrix() {
+    private void updateThumbnailMatrix() {
         mThumbnailScale = 1f;
         if (mBitmapShader != null && mThumbnailData != null) {
             if (getMeasuredWidth() == 0) {
@@ -152,8 +154,7 @@
                 float invThumbnailScale = 1f / mThumbnailScale;
                 final Configuration configuration =
                         getContext().getApplicationContext().getResources().getConfiguration();
-                final Point displaySize = new Point();
-                getDisplay().getRealSize(displaySize);
+                final DeviceProfile profile = Launcher.getLauncher(getContext()).getDeviceProfile();
                 if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
                     if (mThumbnailData.orientation == Configuration.ORIENTATION_PORTRAIT) {
                         // If we are in the same orientation as the screenshot, just scale it to the
@@ -163,23 +164,17 @@
                         // Scale the landscape thumbnail up to app size, then scale that to the task
                         // view size to match other portrait screenshots
                         mThumbnailScale = invThumbnailScale *
-                                ((float) getMeasuredWidth() / displaySize.x);
+                                ((float) getMeasuredWidth() / profile.getCurrentWidth());
                     }
                 } else {
                     // Otherwise, scale the screenshot to fit 1:1 in the current orientation
                     mThumbnailScale = invThumbnailScale;
                 }
             }
-            mMatrix.setTranslate(-mThumbnailData.insets.left * mThumbnailScale,
-                    -mThumbnailData.insets.top * mThumbnailScale);
+            mMatrix.setTranslate(-mThumbnailData.insets.left, -mThumbnailData.insets.top);
             mMatrix.postScale(mThumbnailScale, mThumbnailScale);
             mBitmapShader.setLocalMatrix(mMatrix);
         }
         invalidate();
     }
-
-    public void setDimAlpha(float dimAlpha) {
-        mDimAlpha = dimAlpha;
-        updateThumbnailPaintFilter();
-    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index ea584f0..f6408a8 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -16,17 +16,26 @@
 
 package com.android.quickstep;
 
+import android.app.ActivityOptions;
 import android.content.Context;
+import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import com.android.launcher3.R;
+import com.android.launcher3.uioverrides.OverviewState;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskCallbacks;
 import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
+import com.android.systemui.shared.recents.view.RecentsTransition;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * A task in the Recents view.
  */
@@ -48,10 +57,7 @@
         super(context, attrs, defStyleAttr);
         setWillNotDraw(false);
         setOnClickListener((view) -> {
-            if (mTask != null) {
-                ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
-                        null, null, null);
-            }
+            launchTask(true /* animate */);
         });
     }
 
@@ -65,20 +71,63 @@
      * Updates this task view to the given {@param task}.
      */
     public void bind(Task task) {
+        if (mTask != null) {
+            mTask.removeCallback(this);
+        }
         mTask = task;
         task.addCallback(this);
     }
 
+    public Task getTask() {
+        return mTask;
+    }
+
+    public TaskThumbnailView getThumbnail() {
+        return mSnapshotView;
+    }
+
+    public void launchTask(boolean animate) {
+        if (mTask != null) {
+            final ActivityOptions opts;
+            if (animate) {
+                // Calculate the bounds of the thumbnail to animate from
+                final Rect bounds = new Rect();
+                final int[] pos = new int[2];
+                mSnapshotView.getLocationInWindow(pos);
+                bounds.set(pos[0], pos[1],
+                        pos[0] + mSnapshotView.getWidth(),
+                        pos[1] + mSnapshotView.getHeight());
+                AppTransitionAnimationSpecsFuture animFuture =
+                        new AppTransitionAnimationSpecsFuture(getHandler()) {
+                            @Override
+                            public List<AppTransitionAnimationSpecCompat> composeSpecs() {
+                                ArrayList<AppTransitionAnimationSpecCompat> specs =
+                                        new ArrayList<>();
+                                specs.add(new AppTransitionAnimationSpecCompat(mTask.key.id, null,
+                                        bounds));
+                                return specs;
+                            }
+                        };
+                opts = RecentsTransition.createAspectScaleAnimation(
+                        getContext(), getHandler(), true /* scaleUp */, animFuture, null);
+            } else {
+                opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0);
+            }
+            ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
+                    opts, null, null);
+        }
+    }
+
     @Override
     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
         mSnapshotView.setThumbnail(thumbnailData);
-        mSnapshotView.setDimAlpha(1f);
         mIconView.setImageDrawable(task.icon);
     }
 
     @Override
     public void onTaskDataUnloaded() {
-        // Do nothing
+        mSnapshotView.setThumbnail(null);
+        mIconView.setImageDrawable(null);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index dcfa53b..1218176 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -23,8 +23,10 @@
 import android.app.ActivityOptions;
 import android.app.Service;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Point;
 import android.graphics.PointF;
@@ -32,6 +34,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.Log;
 import android.view.Display;
 import android.view.MotionEvent;
@@ -39,9 +42,17 @@
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.R;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.Options;
+import com.android.systemui.shared.recents.model.RecentsTaskLoader;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+
+import java.util.concurrent.Future;
 
 /**
  * Service connected by system-UI for handling touch interaction.
@@ -50,6 +61,8 @@
 
     private static final String TAG = "TouchInteractionService";
 
+    private static RecentsTaskLoader sRecentsTaskLoader;
+
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
         @Override
@@ -68,6 +81,7 @@
     private Intent mHomeIntent;
     private ComponentName mLauncher;
 
+
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     private int mActivePointerId = INVALID_POINTER_ID;
@@ -89,6 +103,14 @@
         ResolveInfo info = getPackageManager().resolveActivity(mHomeIntent, 0);
         mLauncher = new ComponentName(getPackageName(), info.activityInfo.name);
         mHomeIntent.setComponent(mLauncher);
+
+        Resources res = getResources();
+        if (sRecentsTaskLoader == null) {
+            sRecentsTaskLoader = new RecentsTaskLoader(this,
+                    res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize),
+                    res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
+            sRecentsTaskLoader.startLoader(this);
+        }
     }
 
     @Override
@@ -97,6 +119,10 @@
         return mMyBinder;
     }
 
+    public static RecentsTaskLoader getRecentsTaskLoader() {
+        return sRecentsTaskLoader;
+    }
+
     private void handleMotionEvent(MotionEvent ev) {
         if (ev.getActionMasked() != MotionEvent.ACTION_DOWN && mVelocityTracker == null) {
             return;
@@ -170,16 +196,46 @@
     }
 
     private void startTouchTracking() {
-        mInteractionHandler = new NavBarSwipeInteractionHandler(getCurrentTaskSnapshot(), mRunningTask);
+        // Create the shared handler
+        mInteractionHandler = new NavBarSwipeInteractionHandler(getCurrentTaskSnapshot(),
+                mRunningTask);
 
-        Bundle extras = new Bundle();
-        extras.putBinder(EXTRA_STATE_HANDLER, mInteractionHandler);
-        Intent homeIntent = new Intent(mHomeIntent).putExtras(extras);
+        // Preload and start the recents activity on a background thread
+        final Context context = this;
+        final int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().id;
+        final RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(context);
+        Future<RecentsTaskLoadPlan> loadPlanFuture = BackgroundExecutor.get().submit(() -> {
+            // Preload the plan
+            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+            loadPlan.preloadPlan(loader, runningTaskId, UserHandle.myUserId());
 
-        // TODO: Call ActivityManager#startRecentsActivity instead, so that the system knows that
-        // recents was started and not Home.
-        startActivity(homeIntent,
-                ActivityOptions.makeCustomAnimation(this, 0, 0).toBundle());
+            // Pass the
+            Bundle extras = new Bundle();
+            extras.putBinder(EXTRA_STATE_HANDLER, mInteractionHandler);
+
+            // Start the activity
+            Intent homeIntent = new Intent(mHomeIntent);
+            homeIntent.putExtras(extras);
+            startActivity(homeIntent, ActivityOptions.makeCustomAnimation(this, 0, 0).toBundle());
+            /*
+            ActivityManagerWrapper.getInstance().startRecentsActivity(null, options,
+                    ActivityOptions.makeCustomAnimation(this, 0, 0), UserHandle.myUserId(),
+                    null, null);
+             */
+
+            // Kick off loading of the plan while the activity is starting
+            Options loadOpts = new Options();
+            loadOpts.runningTaskId = runningTaskId;
+            loadOpts.loadIcons = true;
+            loadOpts.loadThumbnails = true;
+            loadOpts.numVisibleTasks = 2;
+            loadOpts.numVisibleTaskThumbnails = 2;
+            loadOpts.onlyLoadForCache = false;
+            loadOpts.onlyLoadPausedActivities = false;
+            loader.loadTasks(loadPlan, loadOpts);
+        }, loadPlan);
+
+        mInteractionHandler.setLastLoadPlan(loadPlanFuture);
     }
 
     private void endInteraction() {
diff --git a/res/values/config.xml b/res/values/config.xml
index 54328e1..0ff0d75 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -134,4 +134,8 @@
     <item type="id" name="search_container_hotseat" />
     <item type="id" name="search_container_all_apps" />
 
+<!-- Recents -->
+    <integer name="config_recentsMaxThumbnailCacheSize">6</integer>
+    <integer name="config_recentsMaxIconCacheSize">12</integer>
+
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 957ec19..59736d8 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -232,4 +232,7 @@
     <dimen name="horizontal_ellipsis_offset">19dp</dimen>
     <dimen name="popup_item_divider_height">0.5dp</dimen>
     <dimen name="swipe_helper_falsing_threshold">70dp</dimen>
+
+<!-- Recents -->
+    <dimen name="recents_page_spacing">10dp</dimen>
 </resources>
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 6030e08..6ddaf29 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -27,7 +27,6 @@
 import android.util.DisplayMetrics;
 import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.FrameLayout;
 
@@ -681,13 +680,13 @@
         }
     }
 
-    private int getCurrentWidth() {
+    public int getCurrentWidth() {
         return isLandscape
                 ? Math.max(widthPx, heightPx)
                 : Math.min(widthPx, heightPx);
     }
 
-    private int getCurrentHeight() {
+    public int getCurrentHeight() {
         return isLandscape
                 ? Math.min(widthPx, heightPx)
                 : Math.max(widthPx, heightPx);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index b1b3452..60c00fb 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -63,7 +63,6 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
-import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcelable;
@@ -362,6 +361,8 @@
 
         restoreState(savedInstanceState);
 
+        InternalStateHandler.handleCreate(this, getIntent());
+
         // We only load the page synchronously if the user rotates (or triggers a
         // configuration change) while launcher is in the foreground
         int currentScreen = PagedView.INVALID_RESTORE_PAGE;
@@ -1347,7 +1348,6 @@
         // Check this condition before handling isActionMain, as this will get reset.
         boolean shouldMoveToDefaultScreen = alreadyOnHome && isInState(NORMAL)
                 && AbstractFloatingView.getTopOpenView(this) == null;
-
         boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
         if (isActionMain) {
             if (mWorkspace == null) {
@@ -1407,7 +1407,7 @@
                 });
             }
         }
-        InternalStateHandler.handleIntent(this, intent);
+        InternalStateHandler.handleNewIntent(this, intent, alreadyOnHome);
 
         TraceHelper.endSection("NEW_INTENT");
     }
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
index a90ed36..c6feefc 100644
--- a/src/com/android/launcher3/states/InternalStateHandler.java
+++ b/src/com/android/launcher3/states/InternalStateHandler.java
@@ -29,15 +29,30 @@
 
     public static final String EXTRA_STATE_HANDLER = "launcher.state_handler";
 
-    public abstract void onNewIntent(Launcher launcher);
+    public abstract void onCreate(Launcher launcher);
+    public abstract void onNewIntent(Launcher launcher, boolean alreadyOnHome);
 
-    public static void handleIntent(Launcher launcher, Intent intent) {
-        IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
-        if (stateBinder instanceof InternalStateHandler) {
-            InternalStateHandler handler = (InternalStateHandler) stateBinder;
-            launcher.setOnResumeCallback(handler);
-            handler.onNewIntent(launcher);
+    public static void handleCreate(Launcher launcher, Intent intent) {
+        if (intent.getExtras() != null) {
+            IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
+            if (stateBinder instanceof InternalStateHandler) {
+                InternalStateHandler handler = (InternalStateHandler) stateBinder;
+                launcher.setOnResumeCallback(handler);
+                handler.onCreate(launcher);
+            }
+            intent.getExtras().remove(EXTRA_STATE_HANDLER);
         }
-        intent.getExtras().remove(EXTRA_STATE_HANDLER);
+    }
+
+    public static void handleNewIntent(Launcher launcher, Intent intent, boolean alreadyOnHome) {
+        if (intent.getExtras() != null) {
+            IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
+            if (stateBinder instanceof InternalStateHandler) {
+                InternalStateHandler handler = (InternalStateHandler) stateBinder;
+                launcher.setOnResumeCallback(handler);
+                handler.onNewIntent(launcher, alreadyOnHome);
+            }
+            intent.getExtras().remove(EXTRA_STATE_HANDLER);
+        }
     }
 }