Merge "No more waiting around for resume" into ub-launcher3-master
diff --git a/proguard.flags b/proguard.flags
index a315cdc..987fb6f 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -104,3 +104,8 @@
 -keep interface com.android.launcher3.model.nano.LauncherDumpProto.** {
   *;
 }
+
+# BUG(70852369): Surpress additional warnings after changing from Proguard to R8
+-dontwarn android.app.**
+-dontwarn android.view.**
+-dontwarn android.os.**
diff --git a/quickstep/res/layout/task_menu.xml b/quickstep/res/layout/task_menu.xml
new file mode 100644
index 0000000..6e3fb4f
--- /dev/null
+++ b/quickstep/res/layout/task_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2018 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.quickstep.TaskMenuView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/bg_popup_item_width"
+    android:layout_height="wrap_content"
+    android:visibility="invisible"
+    android:elevation="@dimen/deep_shortcuts_elevation"
+    android:orientation="vertical"
+    android:background="?attr/popupColorPrimary"
+    android:divider="@drawable/all_apps_divider"
+    android:showDividers="middle"
+    android:animateLayoutChanges="true">
+        <TextView
+            android:id="@+id/task_icon_and_name"
+            android:layout_width="match_parent"
+            android:layout_height="112dp"
+            android:textSize="14sp"
+            android:paddingTop="18dp"
+            android:drawablePadding="8dp"
+            android:gravity="center_horizontal"/>
+</com.android.quickstep.TaskMenuView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 4f85957..587261d 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -18,8 +18,10 @@
 
     <dimen name="task_thumbnail_top_margin">24dp</dimen>
     <dimen name="task_thumbnail_icon_size">48dp</dimen>
+    <dimen name="task_menu_background_radius">12dp</dimen>
 
     <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
     <dimen name="quickstep_fling_min_velocity">250dp</dimen>
 
+    <dimen name="workspace_overview_offset_x">-30dp</dimen>
 </resources>
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index f34aa85..f1da817 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -22,6 +22,7 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherState;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -49,10 +50,12 @@
         }
 
         RecentsView rv = launcher.getOverviewPanel();
+        float overlap = 0;
         if (rv.getCurrentPage() >= rv.getFirstTaskIndex()) {
             Utilities.scaleRectAboutCenter(pageRect, WORKSPACE_SCALE_ON_SCROLL);
+            overlap = launcher.getResources().getDimension(R.dimen.workspace_overview_offset_x);
         }
-        return getScaleAndTranslationForPageRect(launcher, pageRect);
+        return getScaleAndTranslationForPageRect(launcher, overlap, pageRect);
     }
 
     @Override
@@ -77,15 +80,23 @@
         return launcher.getOverviewPanel();
     }
 
-    public static float[] getScaleAndTranslationForPageRect(Launcher launcher, Rect pageRect) {
+    public static float[] getScaleAndTranslationForPageRect(Launcher launcher, float offsetX,
+            Rect pageRect) {
         Workspace ws = launcher.getWorkspace();
         float childWidth = ws.getNormalChildWidth();
 
         Rect insets = launcher.getDragLayer().getInsets();
         float scale = pageRect.width() / childWidth;
 
+        float translationX = offsetX / scale;
+        if (Utilities.isRtl(launcher.getResources())) {
+            translationX = -translationX;
+        }
+
         float halfHeight = ws.getHeight() / 2;
         float childTop = halfHeight - scale * (halfHeight - ws.getPaddingTop() - insets.top);
-        return new float[] {scale, pageRect.top - childTop};
+        float translationY = pageRect.top - childTop;
+
+        return new float[] {scale, translationX, translationY};
     }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
index 435d57e..410a36f 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
@@ -29,7 +29,6 @@
 import android.support.animation.SpringAnimation;
 import android.util.Log;
 import android.view.MotionEvent;
-import android.view.animation.Interpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Launcher;
@@ -42,7 +41,6 @@
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
-import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
@@ -50,6 +48,10 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.FloatRange;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsModel;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TouchInteractionService;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 
 import java.util.ArrayList;
 
@@ -86,6 +88,8 @@
     private static final int FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE = 1 << 0;
     private static final int FLAG_OVERVIEW_DISABLED_FLING = 1 << 1;
     private static final int FLAG_OVERVIEW_DISABLED_CANCEL_STATE = 1 << 2;
+    private static final int FLAG_RECENTS_PLAN_LOADING = 1 << 3;
+    private static final int FLAG_OVERVIEW_DISABLED = 1 << 4;
 
     private final Launcher mLauncher;
     private final SwipeDetector mDetector;
@@ -98,9 +102,10 @@
     private TaggedAnimatorSetBuilder mTaggedAnimatorSetBuilder;
     private AnimatorSet mQuickOverviewAnimation;
     private boolean mAnimatingToOverview;
-    private TwoStateAnimationController mTwoStateAnimationController;
+    private CroppedAnimationController mCroppedAnimationController;
 
     private AnimatorPlaybackController mCurrentAnimation;
+    private LauncherState mFromState;
     private LauncherState mToState;
 
     private float mStartProgress;
@@ -240,11 +245,27 @@
                     + MAX_PROGRESS_TO_OVERVIEW - MIN_PROGRESS_TO_OVERVIEW;
 
             // Build current animation
+            mFromState = mLauncher.getStateManager().getState();
             mToState = mLauncher.isInState(ALL_APPS) ? NORMAL : ALL_APPS;
             mTaggedAnimatorSetBuilder = new TaggedAnimatorSetBuilder();
             mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(
                     mToState, mTaggedAnimatorSetBuilder, maxAccuracy);
 
+            if (TouchInteractionService.isConnected()) {
+                // Load recents plan
+                RecentsModel recentsModel = RecentsModel.getInstance(mLauncher);
+                if (recentsModel.getLastLoadPlan() != null) {
+                    onRecentsPlanLoaded(recentsModel.getLastLoadPlan());
+                } else {
+                    mDragPauseDetector.addDisabledFlags(FLAG_RECENTS_PLAN_LOADING);
+                }
+                // Reload again so that we get the latest list
+                // TODO: Use callback instead of polling everytime
+                recentsModel.loadTasks(-1, this::onRecentsPlanLoaded);
+            } else {
+                mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED);
+            }
+
             mCurrentAnimation.getTarget().addListener(this);
             mStartProgress = 0;
             mProgressMultiplier = (mLauncher.isInState(ALL_APPS) ? 1 : -1) / range;
@@ -262,6 +283,14 @@
         }
     }
 
+    private void onRecentsPlanLoaded(RecentsTaskLoadPlan plan) {
+        RecentsView recentsView = mLauncher.getOverviewPanel();
+        recentsView.update(plan);
+        recentsView.initToPage(0);
+
+        mDragPauseDetector.clearDisabledFlags(FLAG_RECENTS_PLAN_LOADING);
+    }
+
     private float getShiftRange() {
         return mLauncher.getAllAppsController().getShiftRange();
     }
@@ -287,16 +316,11 @@
 
     @Override
     public void onDragEnd(float velocity, boolean fling) {
-        if (!fling && mDragPauseDetector.isEnabled() && mDragPauseDetector.isTriggered()) {
-            snapToOverview(velocity);
-            return;
-        }
-
         mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_FLING);
 
         final long animationDuration;
         final int logAction;
-        final LauncherState targetState;
+        LauncherState targetState;
         final float progress = mCurrentAnimation.getProgressFraction();
 
         if (fling) {
@@ -317,7 +341,7 @@
                 targetState = mToState;
                 animationDuration = SwipeDetector.calculateDuration(velocity, 1 - progress);
             } else {
-                targetState = mToState == ALL_APPS ? NORMAL : ALL_APPS;
+                targetState = mFromState;
                 animationDuration = SwipeDetector.calculateDuration(velocity, progress);
             }
         }
@@ -328,7 +352,13 @@
                 h.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
             }
         }
-        mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
+        mCurrentAnimation.setEndAction(() -> {
+            LauncherState finalState = targetState;
+            if (mDragPauseDetector.isTriggered() && targetState == NORMAL) {
+                finalState = OVERVIEW;
+            }
+            onSwipeInteractionCompleted(finalState, logAction);
+        });
 
         float nextFrameProgress = Utilities.boundToRange(
                 progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
@@ -341,7 +371,7 @@
     }
 
     private void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
-        if (targetState == mToState) {
+        if (targetState != mFromState) {
             // Transition complete. log the action
             mLauncher.getUserEventDispatcher().logActionOnContainer(logAction,
                     mToState == ALL_APPS ? Direction.UP : Direction.DOWN,
@@ -354,33 +384,6 @@
         mLauncher.getStateManager().goToState(targetState, false /* animated */);
     }
 
-    private void snapToOverview(float velocity) {
-        mAnimatingToOverview = true;
-
-        final float progress = mCurrentAnimation.getProgressFraction();
-        float endProgress = mToState == NORMAL ? 1f : 0f;
-        long animationDuration = SwipeDetector.calculateDuration(
-                velocity, Math.abs(endProgress - progress));
-        float nextFrameProgress = Utilities.boundToRange(
-                progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
-
-        mCurrentAnimation.setEndAction(() -> {
-            // TODO: Add logging
-            clearState();
-            mLauncher.getStateManager().goToState(OVERVIEW, true /* animated */);
-        });
-
-        if (mTwoStateAnimationController != null) {
-            mTwoStateAnimationController.goBackToStart(endProgress);
-        }
-
-        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
-        anim.setFloatValues(nextFrameProgress, endProgress);
-        anim.setDuration(animationDuration);
-        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
-        anim.start();
-    }
-
     private void onDragPauseDetected() {
         final ValueAnimator twoStepAnimator = ValueAnimator.ofFloat(0, 1);
         twoStepAnimator.setDuration(mCurrentAnimation.getDuration());
@@ -409,33 +412,29 @@
         mQuickOverviewAnimation.start();
     }
 
-    private void onQuickOverviewAnimationComplete(ValueAnimator twoStepAnimator) {
+    private void onQuickOverviewAnimationComplete(ValueAnimator animator) {
         if (mAnimatingToOverview) {
             return;
         }
 
-        // The remaining state handlers are on the OVERVIEW state. Create two animations, one
-        // towards the NORMAL state and one towards ALL_APPS state and control them based on the
-        // swipe progress.
+        // For the remainder to the interaction, the user can either go to the ALL_APPS state or
+        // the OVERVIEW state.
+        // The remaining state handlers are on the OVERVIEW state. Create one animation towards the
+        // ALL_APPS state and only call it when the user moved above the current range.
         AnimationConfig config = new AnimationConfig();
         config.duration = (long) (2 * getShiftRange());
         config.userControlled = true;
 
-        LauncherState fromState = mToState == ALL_APPS ? NORMAL : ALL_APPS;
-        AnimatorSetBuilder builderToTargetState = new AnimatorSetBuilder();
-        AnimatorSetBuilder builderToSourceState = new AnimatorSetBuilder();
-
+        AnimatorSetBuilder builderToAllAppsState = new AnimatorSetBuilder();
         StateHandler[] handlers = mLauncher.getStateManager().getStateHandlers();
         for (int i = OTHER_HANDLERS_START_INDEX; i < handlers.length; i++) {
-            handlers[i].setStateWithAnimation(mToState, builderToTargetState, config);
-            handlers[i].setStateWithAnimation(fromState, builderToSourceState, config);
+            handlers[i].setStateWithAnimation(ALL_APPS, builderToAllAppsState, config);
         }
 
-        mTwoStateAnimationController = new TwoStateAnimationController(
-                AnimatorPlaybackController.wrap(builderToSourceState.build(), config.duration),
-                AnimatorPlaybackController.wrap(builderToTargetState.build(), config.duration),
-                twoStepAnimator.getAnimatedFraction());
-        twoStepAnimator.addUpdateListener(mTwoStateAnimationController);
+        mCroppedAnimationController = new CroppedAnimationController(
+                AnimatorPlaybackController.wrap(builderToAllAppsState.build(), config.duration),
+                new FloatRange(animator.getAnimatedFraction(), mToState == ALL_APPS ? 1 : 0));
+        animator.addUpdateListener(mCroppedAnimationController);
     }
 
     private void clearState() {
@@ -450,69 +449,49 @@
             mQuickOverviewAnimation.cancel();
             mQuickOverviewAnimation = null;
         }
-        mTwoStateAnimationController = null;
+        mCroppedAnimationController = null;
         mAnimatingToOverview = false;
 
         mDetector.finishedScrolling();
     }
 
     /**
-     * {@link AnimatorUpdateListener} which interpolates two animations based the progress
+     * {@link AnimatorUpdateListener} which controls another animation for a fraction of range
      */
-    private static class TwoStateAnimationController implements AnimatorUpdateListener {
+    private static class CroppedAnimationController implements AnimatorUpdateListener {
 
-        private final AnimatorPlaybackController mControllerTowardsStart;
-        private final AnimatorPlaybackController mControllerTowardsEnd;
+        private final AnimatorPlaybackController mTarget;
+        private final FloatRange mRange;
 
-        private Interpolator mInterpolator = Interpolators.LINEAR;
-        private float mStartFraction;
-        private float mLastFraction;
-
-        TwoStateAnimationController(AnimatorPlaybackController controllerTowardsStart,
-                AnimatorPlaybackController controllerTowardsEnd, float startFraction) {
-            mControllerTowardsStart = controllerTowardsStart;
-            mControllerTowardsEnd = controllerTowardsEnd;
-            mLastFraction = mStartFraction = startFraction;
+        CroppedAnimationController(AnimatorPlaybackController target, FloatRange range) {
+            mTarget = target;
+            mRange = range;
         }
 
+
         @Override
         public void onAnimationUpdate(ValueAnimator valueAnimator) {
-            mLastFraction = mInterpolator.getInterpolation(valueAnimator.getAnimatedFraction());
-            if (mLastFraction > mStartFraction) {
-                if (mStartFraction >= 1) {
-                    mControllerTowardsEnd.setPlayFraction(0);
-                } else {
-                    mControllerTowardsEnd.setPlayFraction(
-                            (mLastFraction - mStartFraction) / (1 - mStartFraction));
-                }
-            } else {
-                if (mStartFraction <= 0) {
-                    mControllerTowardsStart.setPlayFraction(0);
-                } else {
-                    mControllerTowardsStart.setPlayFraction(
-                            (mStartFraction - mLastFraction) / mStartFraction);
-                }
-            }
-        }
+            float fraction = valueAnimator.getAnimatedFraction();
 
-        /**
-         * Changes the interpolator such that from this point ({@link #mLastFraction}), the
-         * animation run towards {@link #mStartFraction}. This allows us to animate the UI back
-         * to the original point.
-         * @param endFraction expected end point for this animation. Should either be 0 or 1.
-         */
-        public void goBackToStart(float endFraction) {
-            if (mLastFraction == mStartFraction || mLastFraction == endFraction) {
-                mInterpolator = (v) -> mStartFraction;
-            } else if (mLastFraction > mStartFraction && endFraction < mStartFraction) {
-                mInterpolator = (v) -> Math.max(v, mStartFraction);
-            } else if (mLastFraction < mStartFraction && endFraction > mStartFraction) {
-                mInterpolator = (v) -> Math.min(mStartFraction, v);
+            if (mRange.start < mRange.end) {
+                if (fraction <= mRange.start) {
+                    mTarget.setPlayFraction(0);
+                } else if (fraction >= mRange.end) {
+                    mTarget.setPlayFraction(1);
+                } else {
+                    mTarget.setPlayFraction((fraction - mRange.start) / (mRange.end - mRange.start));
+                }
+            } else if (mRange.start > mRange.end) {
+                if (fraction >= mRange.start) {
+                    mTarget.setPlayFraction(0);
+                } else if (fraction <= mRange.end) {
+                    mTarget.setPlayFraction(1);
+                } else {
+                    mTarget.setPlayFraction((fraction - mRange.start) / (mRange.end - mRange.start));
+                }
             } else {
-                final float start = mLastFraction;
-                final float range = endFraction - mLastFraction;
-                mInterpolator = (v) ->
-                        SwipeDetector.interpolate(start, mStartFraction, (v - start) / range);
+                // mRange.start == mRange.end
+                mTarget.setPlayFraction(0);
             }
         }
     }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java b/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java
index 990d286..9f7cffe 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java
@@ -39,7 +39,7 @@
 public class WorkspaceCard extends FrameLayout implements PageCallbacks, OnClickListener {
 
     private final Rect mTempRect = new Rect();
-    private final float[] mEvaluatedFloats = new float[2];
+    private final float[] mEvaluatedFloats = new float[3];
     private final FloatArrayEvaluator mEvaluator = new FloatArrayEvaluator(mEvaluatedFloats);
 
     // UI related information
@@ -151,21 +151,25 @@
 
     @Override
     public int onPageScroll(ScrollState scrollState) {
-        setTranslationX(scrollState.distanceFromScreenCenter);
-
         float factor = scrollState.linearInterpolation;
         float scale = factor * WORKSPACE_SCALE_ON_SCROLL + (1 - factor);
         setScaleX(scale);
         setScaleY(scale);
 
+        float translateX = scrollState.distanceFromScreenCenter;
         if (mIsWorkspaceScrollingEnabled) {
             initUiData();
 
             mEvaluator.evaluate(factor, mScaleAndTranslatePage0, mScaleAndTranslatePage1);
             mWorkspace.setScaleX(mEvaluatedFloats[0]);
             mWorkspace.setScaleY(mEvaluatedFloats[0]);
-            mWorkspace.setTranslationY(mEvaluatedFloats[1]);
+            mWorkspace.setTranslationX(mEvaluatedFloats[1]);
+            mWorkspace.setTranslationY(mEvaluatedFloats[2]);
+            translateX += mEvaluatedFloats[1];
         }
+
+        setTranslationX(translateX);
+
         return SCROLL_TYPE_WORKSPACE;
     }
 
@@ -174,13 +178,15 @@
             return;
         }
 
+        float overlap = getResources().getDimension(R.dimen.workspace_overview_offset_x);
+
         RecentsView.getPageRect(mLauncher, mTempRect);
         mScaleAndTranslatePage0 = OverviewState
-                .getScaleAndTranslationForPageRect(mLauncher, mTempRect);
+                .getScaleAndTranslationForPageRect(mLauncher, 0, mTempRect);
         Rect scaledDown = new Rect(mTempRect);
         Utilities.scaleRectAboutCenter(scaledDown, WORKSPACE_SCALE_ON_SCROLL);
         mScaleAndTranslatePage1 = OverviewState
-                .getScaleAndTranslationForPageRect(mLauncher, scaledDown);
+                .getScaleAndTranslationForPageRect(mLauncher, overlap, scaledDown);
         mUIDataValid = true;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index 095b445..09fd8f0 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.states.InternalStateHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.views.AllAppsScrim;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
@@ -97,6 +98,7 @@
     private RecentsView mRecentsView;
     private RecentsViewStateController mStateController;
     private Hotseat mHotseat;
+    private AllAppsScrim mAllAppsScrim;
     private RecentsTaskLoadPlan mLoadPlan;
 
     private boolean mLauncherReady;
@@ -182,6 +184,7 @@
         mRecentsView = mLauncher.getOverviewPanel();
         mStateController = mRecentsView.getStateController();
         mHotseat = mLauncher.getHotseat();
+        mAllAppsScrim = mLauncher.findViewById(R.id.all_apps_scrim);
 
         // Optimization
         mLauncher.getAppsView().setVisibility(View.GONE);
@@ -222,7 +225,9 @@
         float shift = mCurrentShift.value * mActivityMultiplier.value;
         int hotseatSize = getHotseatSize();
 
-        mHotseat.setTranslationY((1 - shift) * hotseatSize);
+        float hotseatTranslation = (1 - shift) * hotseatSize;
+        mHotseat.setTranslationY(hotseatTranslation);
+        mAllAppsScrim.setTranslationY(hotseatTranslation);
 
         mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
 
@@ -324,6 +329,7 @@
     private void cleanupLauncher() {
         // TODO: These should be done as part of ActivityOptions#OnAnimationStarted
         mHotseat.setTranslationY(0);
+        mAllAppsScrim.setTranslationY(0);
         mLauncher.setOnResumeCallback(() -> mDragView.close(false));
     }
 
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
new file mode 100644
index 0000000..112f156
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Looper;
+import android.os.UserHandle;
+
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.PreloadOptions;
+import com.android.systemui.shared.recents.model.RecentsTaskLoader;
+import com.android.systemui.shared.system.BackgroundExecutor;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+/**
+ * Singleton class to load and manage recents model.
+ */
+public class RecentsModel {
+
+    // We do not need any synchronization for this variable as its only written on UI thread.
+    private static RecentsModel INSTANCE;
+
+    public static RecentsModel getInstance(final Context context) {
+        if (INSTANCE == null) {
+            if (Looper.myLooper() == Looper.getMainLooper()) {
+                INSTANCE = new RecentsModel(context.getApplicationContext());
+            } else {
+                try {
+                    return new MainThreadExecutor().submit(
+                            () -> RecentsModel.getInstance(context)).get();
+                } catch (InterruptedException|ExecutionException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+        return INSTANCE;
+    }
+
+    private final Context mContext;
+    private final RecentsTaskLoader mRecentsTaskLoader;
+    private final MainThreadExecutor mMainThreadExecutor;
+
+    private RecentsTaskLoadPlan mLastLoadPlan;
+    private RecentsModel(Context context) {
+        mContext = context;
+
+        Resources res = context.getResources();
+        mRecentsTaskLoader = new RecentsTaskLoader(mContext,
+                res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize),
+                res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
+        mRecentsTaskLoader.startLoader(mContext);
+
+        mMainThreadExecutor = new MainThreadExecutor();
+    }
+
+    public RecentsTaskLoader getRecentsTaskLoader() {
+        return mRecentsTaskLoader;
+    }
+
+    /**
+     * Preloads the task plan
+     * @param taskId The running task id or -1
+     * @param callback The callback to receive the task plan once its complete or null. This is
+     *                always called on the UI thread.
+     */
+    public void loadTasks(int taskId, Consumer<RecentsTaskLoadPlan> callback) {
+        BackgroundExecutor.get().submit(() -> {
+            // Preload the plan
+            RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext);
+            PreloadOptions opts = new PreloadOptions();
+            opts.loadTitles = false;
+            loadPlan.preloadPlan(opts, mRecentsTaskLoader, taskId, UserHandle.myUserId());
+            // Set the load plan on UI thread
+            mMainThreadExecutor.execute(() -> {
+                mLastLoadPlan = loadPlan;
+                if (callback != null) {
+                    callback.accept(loadPlan);
+                }
+            });
+        });
+    }
+
+    public RecentsTaskLoadPlan getLastLoadPlan() {
+        return mLastLoadPlan;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 6161858..00901c6 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -146,7 +146,8 @@
     }
 
     public void update(RecentsTaskLoadPlan loadPlan) {
-        final RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+        final RecentsTaskLoader loader = RecentsModel.getInstance(getContext())
+                .getRecentsTaskLoader();
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
         if (stack == null) {
             removeAllViews();
diff --git a/quickstep/src/com/android/quickstep/TaskMenuView.java b/quickstep/src/com/android/quickstep/TaskMenuView.java
new file mode 100644
index 0000000..70542c2
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskMenuView.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Contains options for a recent task when long-pressing its icon.
+ */
+public class TaskMenuView extends AbstractFloatingView {
+
+    private static final Rect sTempRect = new Rect();
+
+    /** Note that these will be shown in order from top to bottom, if available for the task. */
+    private static final TaskSystemShortcut[] MENU_OPTIONS = new TaskSystemShortcut[] {
+            new TaskSystemShortcut.Widgets(),
+            new TaskSystemShortcut.AppInfo(),
+            new TaskSystemShortcut.Install()
+    };
+
+    private static final long OPEN_CLOSE_DURATION = 220;
+
+    private Launcher mLauncher;
+    private TextView mTaskIconAndName;
+    private AnimatorSet mOpenCloseAnimator;
+
+    public TaskMenuView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        mLauncher = Launcher.getLauncher(context);
+        setClipToOutline(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                float r = getResources().getDimensionPixelSize(R.dimen.task_menu_background_radius);
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), r);
+            }
+        });
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mTaskIconAndName = findViewById(R.id.task_icon_and_name);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            DragLayer dl = mLauncher.getDragLayer();
+            if (!dl.isEventOverView(this, ev)) {
+                // TODO: log this once we have a new container type for it?
+                close(true);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (animate) {
+            animateClose();
+        } else {
+            closeComplete();
+        }
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // TODO
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_TASK_MENU) != 0;
+    }
+
+    public static boolean showForTask(TaskView taskView) {
+        Launcher launcher = Launcher.getLauncher(taskView.getContext());
+        final TaskMenuView taskMenuView = (TaskMenuView) launcher.getLayoutInflater().inflate(
+                        R.layout.task_menu, launcher.getDragLayer(), false);
+        return taskMenuView.populateAndShowForTask(taskView);
+    }
+
+    private boolean populateAndShowForTask(TaskView taskView) {
+        if (isAttachedToWindow()) {
+            return false;
+        }
+        mLauncher.getDragLayer().addView(this);
+        addMenuOptions(taskView.getTask());
+        orientAroundTaskView(taskView);
+        post(this::animateOpen);
+        return true;
+    }
+
+    private void addMenuOptions(Task task) {
+        Drawable icon = task.icon.getConstantState().newDrawable();
+        int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
+        icon.setBounds(0, 0, iconSize, iconSize);
+        mTaskIconAndName.setCompoundDrawables(null, icon, null, null);
+        mTaskIconAndName.setText(TaskUtils.getTitle(mLauncher, task));
+
+        LayoutInflater inflater = mLauncher.getLayoutInflater();
+        for (TaskSystemShortcut menuOption : MENU_OPTIONS) {
+            OnClickListener onClickListener = menuOption.getOnClickListener(mLauncher, task);
+            if (onClickListener != null) {
+                DeepShortcutView menuOptionView = (DeepShortcutView) inflater.inflate(
+                        R.layout.system_shortcut, this, false);
+                menuOptionView.getIconView().setBackgroundResource(menuOption.iconResId);
+                menuOptionView.getBubbleText().setText(menuOption.labelResId);
+                menuOptionView.setOnClickListener(onClickListener);
+                addView(menuOptionView);
+            }
+        }
+    }
+
+    private void orientAroundTaskView(TaskView taskView) {
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        mLauncher.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
+        Rect insets = mLauncher.getDragLayer().getInsets();
+        setX(sTempRect.left + (sTempRect.width() - getMeasuredWidth()) / 2 - insets.left);
+        setY(sTempRect.top - mTaskIconAndName.getPaddingTop() - insets.top);
+    }
+
+    private void animateOpen() {
+        animateOpenOrClosed(false);
+        mIsOpen = true;
+    }
+
+    private void animateClose() {
+        animateOpenOrClosed(true);
+    }
+
+    private void animateOpenOrClosed(boolean closing) {
+        if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
+            return;
+        }
+        mOpenCloseAnimator = LauncherAnimUtils.createAnimatorSet();
+        mOpenCloseAnimator.play(createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, closing));
+        mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationStart(Animator animation) {
+                setVisibility(VISIBLE);
+            }
+
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                if (closing) {
+                    closeComplete();
+                }
+            }
+        });
+        mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
+        mOpenCloseAnimator.setDuration(OPEN_CLOSE_DURATION);
+        mOpenCloseAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
+        mOpenCloseAnimator.start();
+    }
+
+    private void closeComplete() {
+        mIsOpen = false;
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
+        int iconSize = getResources().getDimensionPixelSize(R.dimen.task_thumbnail_icon_size);
+        float fromRadius = iconSize / 2;
+        float toRadius = getResources().getDimensionPixelSize(
+                R.dimen.task_menu_background_radius);
+        Point iconCenter = new Point(getWidth() / 2, mTaskIconAndName.getPaddingTop() + iconSize / 2);
+        Rect fromRect = new Rect(iconCenter.x, iconCenter.y, iconCenter.x, iconCenter.y);
+        Rect toRect = new Rect(0, 0, getWidth(), getHeight());
+        return new RoundedRectRevealOutlineProvider(fromRadius, toRadius, fromRect, toRect) {
+            @Override
+            public boolean shouldRemoveElevationDuringAnimation() {
+                return true;
+            }
+        };
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
new file mode 100644
index 0000000..1ba7ce4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.UserHandle;
+import android.view.View;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.popup.SystemShortcut;
+import com.android.launcher3.util.InstantAppResolver;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Represents a system shortcut that can be shown for a recent task.
+ */
+public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut {
+
+    protected T mSystemShortcut;
+
+    protected TaskSystemShortcut(T systemShortcut) {
+        super(systemShortcut.iconResId, systemShortcut.labelResId);
+        mSystemShortcut = systemShortcut;
+    }
+
+    @Override
+    public View.OnClickListener getOnClickListener(Launcher launcher, ItemInfo itemInfo) {
+        return null;
+    }
+
+    public View.OnClickListener getOnClickListener(final Launcher launcher, final Task task) {
+        ShortcutInfo dummyInfo = new ShortcutInfo();
+        dummyInfo.intent = new Intent();
+        ComponentName component = task.getTopComponent();
+        dummyInfo.intent.setComponent(component);
+        dummyInfo.user = UserHandle.getUserHandleForUid(task.key.userId);
+        dummyInfo.title = TaskUtils.getTitle(launcher, task);
+
+        return getOnClickListenerForTask(launcher, task, dummyInfo);
+    }
+
+    protected View.OnClickListener getOnClickListenerForTask(final Launcher launcher,
+            final Task task, final ItemInfo dummyInfo) {
+        return mSystemShortcut.getOnClickListener(launcher, dummyInfo);
+    }
+
+
+    public static class Widgets extends TaskSystemShortcut<SystemShortcut.Widgets> {
+        public Widgets() {
+            super(new SystemShortcut.Widgets());
+        }
+    }
+
+    public static class AppInfo extends TaskSystemShortcut<SystemShortcut.AppInfo> {
+        public AppInfo() {
+            super(new SystemShortcut.AppInfo());
+        }
+    }
+
+    public static class Install extends TaskSystemShortcut<SystemShortcut.Install> {
+        public Install() {
+            super(new SystemShortcut.Install());
+        }
+
+        @Override
+        protected View.OnClickListener getOnClickListenerForTask(Launcher launcher, Task task,
+                ItemInfo itemInfo) {
+            if (InstantAppResolver.newInstance(launcher).isInstantApp(launcher,
+                        task.getTopComponent().getPackageName())) {
+                return mSystemShortcut.createOnClickListener(launcher, itemInfo);
+            }
+            return null;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 4a9bfea..3d4d451 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -173,4 +173,10 @@
         }
         invalidate();
     }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        updateThumbnailMatrix();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java
new file mode 100644
index 0000000..a95e7c1
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+import com.android.launcher3.Launcher;
+import com.android.systemui.shared.recents.model.Task;
+
+/**
+ * Contains helpful methods for retrieving data from {@link Task}s.
+ * TODO: remove this once we switch to getting the icon and label from IconCache.
+ */
+public class TaskUtils {
+    private static final String TAG = "TaskUtils";
+
+    public static CharSequence getTitle(Launcher launcher, Task task) {
+        PackageManager pm = launcher.getPackageManager();
+        try {
+            return pm.getPackageInfo(task.getTopComponent().getPackageName(), 0)
+                    .applicationInfo.loadLabel(pm);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Failed to get title for task " + task, e);
+        }
+        return "";
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index 6b37ada..94d85ee 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -208,12 +208,14 @@
     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
         mSnapshotView.setThumbnail(thumbnailData);
         mIconView.setImageDrawable(task.icon);
+        mIconView.setOnLongClickListener(icon -> TaskMenuView.showForTask(this));
     }
 
     @Override
     public void onTaskDataUnloaded() {
         mSnapshotView.setThumbnail(null);
         mIconView.setImageDrawable(null);
+        mIconView.setOnLongClickListener(null);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index f457a59..4321791 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -28,10 +28,8 @@
 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;
@@ -39,7 +37,6 @@
 import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.UserHandle;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.Display;
@@ -52,14 +49,9 @@
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
-import com.android.launcher3.MainThreadExecutor;
-import com.android.launcher3.R;
 import com.android.launcher3.util.TraceHelper;
 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.PreloadOptions;
-import com.android.systemui.shared.recents.model.RecentsTaskLoader;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.WindowManagerWrapper;
@@ -74,8 +66,6 @@
 
     private static final String TAG = "TouchInteractionService";
 
-    private static RecentsTaskLoader sRecentsTaskLoader;
-
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
         @Override
@@ -93,12 +83,18 @@
             = this::handleTouchDownOnOtherActivity;
     private final Consumer<MotionEvent> mNoOpTouchConsumer = (ev) -> {};
 
+    private static boolean sConnected = false;
+
+    public static boolean isConnected() {
+        return sConnected;
+    }
+
     private ActivityManagerWrapper mAM;
     private RunningTaskInfo mRunningTask;
+    private RecentsModel mRecentsModel;
     private Intent mHomeIntent;
     private ComponentName mLauncher;
     private MotionEventQueue mEventQueue;
-    private MainThreadExecutor mMainThreadExecutor;
 
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
@@ -117,6 +113,7 @@
     public void onCreate() {
         super.onCreate();
         mAM = ActivityManagerWrapper.getInstance();
+        mRecentsModel = RecentsModel.getInstance(this);
 
         mHomeIntent = new Intent(Intent.ACTION_MAIN)
                 .addCategory(Intent.CATEGORY_HOME)
@@ -126,16 +123,15 @@
         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);
-        }
-
-        mMainThreadExecutor = new MainThreadExecutor();
         mEventQueue = new MotionEventQueue(Choreographer.getInstance(), this::handleMotionEvent);
+        mRecentsModel.loadTasks(-1, null);
+        sConnected = true;
+    }
+
+    @Override
+    public void onDestroy() {
+        sConnected = false;
+        super.onDestroy();
     }
 
     @Override
@@ -144,10 +140,6 @@
         return mMyBinder;
     }
 
-    public static RecentsTaskLoader getRecentsTaskLoader() {
-        return sRecentsTaskLoader;
-    }
-
     private void handleMotionEvent(MotionEvent ev) {
         if (ev.getActionMasked() == ACTION_DOWN) {
             mRunningTask = mAM.getRunningTask();
@@ -255,12 +247,9 @@
         final NavBarSwipeInteractionHandler handler =
                 new NavBarSwipeInteractionHandler(mRunningTask, this);
 
-        // Preload and start the recents activity on a background thread
-        final Context context = this;
-        final RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(context);
-        final int taskId = mRunningTask.id;
         TraceHelper.partitionSection("TouchInt", "Thershold crossed ");
 
+        // Start the recents activity on a background thread
         BackgroundExecutor.get().submit(() -> {
             // Get the snap shot before
             handler.setTaskSnapshot(getCurrentTaskSnapshot());
@@ -275,15 +264,10 @@
                     ActivityOptions.makeCustomAnimation(this, 0, 0), UserHandle.myUserId(),
                     null, null);
              */
-
-            // Preload the plan
-            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
-            PreloadOptions opts = new PreloadOptions();
-            opts.loadTitles = false;
-            loadPlan.preloadPlan(opts, loader, taskId, UserHandle.myUserId());
-            // Set the load plan on UI thread
-            mMainThreadExecutor.execute(() -> handler.setRecentsTaskLoadPlan(loadPlan));
         });
+
+        // Preload the plan
+        mRecentsModel.loadTasks(mRunningTask.id, handler::setRecentsTaskLoadPlan);
         mInteractionHandler = handler;
     }
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 1ae7cbf..36eb34b 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -199,10 +199,6 @@
     <dimen name="popup_arrow_horizontal_center_start">28dp</dimen>
     <!-- popup_padding_end + deep_shortcut_drag_handle_size / 2 -->
     <dimen name="popup_arrow_horizontal_center_end">24dp</dimen>
-    <!-- popup_arrow_center_start - popup_arrow_width / 2-->
-    <dimen name="popup_arrow_horizontal_offset_start">23dp</dimen>
-    <!-- popup_arrow_center_end - popup_arrow_width / 2-->
-    <dimen name="popup_arrow_horizontal_offset_end">19dp</dimen>
     <dimen name="popup_arrow_corner_radius">2dp</dimen>
     <!-- popup_padding_start + icon_size + 10dp -->
     <dimen name="deep_shortcuts_text_padding_start">56dp</dimen>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index da464c0..9a6be0b 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -43,7 +43,8 @@
             TYPE_WIDGET_RESIZE_FRAME,
             TYPE_WIDGETS_FULL_SHEET,
             TYPE_QUICKSTEP_PREVIEW,
-            TYPE_ON_BOARD_POPUP
+            TYPE_ON_BOARD_POPUP,
+            TYPE_TASK_MENU
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -54,10 +55,11 @@
     public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4;
     public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 5;
     public static final int TYPE_ON_BOARD_POPUP = 1 << 6;
+    public static final int TYPE_TASK_MENU = 1 << 7;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
             | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
-            | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP;
+            | TYPE_QUICKSTEP_PREVIEW | TYPE_ON_BOARD_POPUP | TYPE_TASK_MENU;
 
     // Type of popups which should be kept open during launcher rebind
     public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 2beaca1..a1f5879 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -137,7 +137,7 @@
     }
 
     public float[] getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new float[] {1, 0};
+        return new float[] {1, 0, 0};
     }
 
     public float getHoseatAlpha(Launcher launcher) {
diff --git a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
index 80818f2..9ed86ed 100644
--- a/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
+++ b/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
@@ -123,10 +123,8 @@
      * Starts a transition animation for the workspace.
      */
     private void setWorkspaceProperty(LauncherState state, PropertySetter propertySetter) {
-        float[] scaleAndTranslationY = state.getWorkspaceScaleAndTranslation(mLauncher);
-        mNewScale = scaleAndTranslationY[0];
-        final float finalWorkspaceTranslationY = scaleAndTranslationY[1];
-
+        float[] scaleAndTranslation = state.getWorkspaceScaleAndTranslation(mLauncher);
+        mNewScale = scaleAndTranslation[0];
         PageAlphaProvider pageAlphaProvider = state.getWorkspacePageAlphaProvider(mLauncher);
         final int childCount = mWorkspace.getChildCount();
         for (int i = 0; i < childCount; i++) {
@@ -135,8 +133,10 @@
         }
 
         propertySetter.setFloat(mWorkspace, SCALE_PROPERTY, mNewScale, Interpolators.ZOOM_IN);
+        propertySetter.setFloat(mWorkspace, View.TRANSLATION_X,
+                scaleAndTranslation[1], Interpolators.ZOOM_IN);
         propertySetter.setFloat(mWorkspace, View.TRANSLATION_Y,
-                finalWorkspaceTranslationY, Interpolators.ZOOM_IN);
+                scaleAndTranslation[2], Interpolators.ZOOM_IN);
 
         float hotseatAlpha = state.getHoseatAlpha(mLauncher);
         propertySetter.setViewAlpha(mWorkspace.createHotseatAlphaAnimator(hotseatAlpha),
diff --git a/src/com/android/launcher3/anim/RevealOutlineAnimation.java b/src/com/android/launcher3/anim/RevealOutlineAnimation.java
index 1312da9..c6b62fa 100644
--- a/src/com/android/launcher3/anim/RevealOutlineAnimation.java
+++ b/src/com/android/launcher3/anim/RevealOutlineAnimation.java
@@ -34,7 +34,6 @@
         final float elevation = revealView.getElevation();
 
         va.addListener(new AnimatorListenerAdapter() {
-            private boolean mWasCanceled = false;
             private boolean mIsClippedToOutline;
             private ViewOutlineProvider mOldOutlineProvider;
 
@@ -49,18 +48,11 @@
                 }
             }
 
-            @Override
-            public void onAnimationCancel(Animator animation) {
-                mWasCanceled = true;
-            }
-
             public void onAnimationEnd(Animator animation) {
-                if (!mWasCanceled) {
-                    revealView.setOutlineProvider(mOldOutlineProvider);
-                    revealView.setClipToOutline(mIsClippedToOutline);
-                    if (shouldRemoveElevationDuringAnimation()) {
-                        revealView.setTranslationZ(0);
-                    }
+                revealView.setOutlineProvider(mOldOutlineProvider);
+                revealView.setClipToOutline(mIsClippedToOutline);
+                if (shouldRemoveElevationDuringAnimation()) {
+                    revealView.setTranslationZ(0);
                 }
             }
 
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index a166dff..6481183 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -341,15 +341,17 @@
         updateDividers();
 
         // Add the arrow.
-        final int arrowHorizontalOffset = getResources().getDimensionPixelSize(isAlignedWithStart()
-                ? R.dimen.popup_arrow_horizontal_offset_start
-                : R.dimen.popup_arrow_horizontal_offset_end);
+        final Resources res = getResources();
+        final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart()
+                ? R.dimen.popup_arrow_horizontal_center_start
+                : R.dimen.popup_arrow_horizontal_center_end);
+        final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2;
         mLauncher.getDragLayer().addView(mArrow);
         DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
         if (mIsLeftAligned) {
-            mArrow.setX(getX() + arrowHorizontalOffset);
+            mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth);
         } else {
-            mArrow.setX(getX() + getMeasuredWidth() - arrowHorizontalOffset);
+            mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth);
         }
 
         if (Gravity.isVertical(mGravity)) {
@@ -435,9 +437,6 @@
             x = rightAlignedX;
         }
         mIsLeftAligned = x == leftAlignedX;
-        if (mIsRtl) {
-            x -= dragLayer.getWidth() - width;
-        }
 
         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
         int iconWidth = mOriginalIcon.getWidth()
@@ -529,8 +528,7 @@
 
         // enforce contained is within screen
         DragLayer dragLayer = mLauncher.getDragLayer();
-        if (getTranslationX() + l < 0 ||
-                getTranslationX() + r > dragLayer.getWidth()) {
+        if (getTranslationX() + l < 0 || getTranslationX() + r > dragLayer.getWidth()) {
             // If we are still off screen, center horizontally too.
             mGravity |= Gravity.CENTER_HORIZONTAL;
         }
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index c398aaa..42aa12b 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,5 +1,8 @@
 package com.android.launcher3.popup;
 
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Action;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
+
 import android.content.Intent;
 import android.graphics.Rect;
 import android.os.Bundle;
@@ -19,9 +22,6 @@
 
 import java.util.List;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Action;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
-
 /**
  * Represents a system shortcut for a given app. The shortcut should have a static label and
  * icon, and an onClickListener that depends on the item that the shortcut services.
@@ -110,14 +110,15 @@
             if (!enabled) {
                 return null;
             }
-            return new View.OnClickListener() {
-                @Override
-                public void onClick(View view) {
-                    Intent intent = PackageManagerHelper.getMarketIntent(itemInfo
-                            .getTargetComponent().getPackageName());
-                    launcher.startActivitySafely(view, intent, itemInfo);
-                    AbstractFloatingView.closeAllOpenViews(launcher);
-                }
+            return createOnClickListener(launcher, itemInfo);
+        }
+
+        public View.OnClickListener createOnClickListener(Launcher launcher, ItemInfo itemInfo) {
+            return view -> {
+                Intent intent = PackageManagerHelper.getMarketIntent(itemInfo
+                        .getTargetComponent().getPackageName());
+                launcher.startActivitySafely(view, intent, itemInfo);
+                AbstractFloatingView.closeAllOpenViews(launcher);
             };
         }
     }
diff --git a/src/com/android/launcher3/shortcuts/ShortcutKey.java b/src/com/android/launcher3/shortcuts/ShortcutKey.java
index e86bfb2..704d82f 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutKey.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutKey.java
@@ -1,6 +1,7 @@
 package com.android.launcher3.shortcuts;
 
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.os.UserHandle;
 
@@ -17,6 +18,10 @@
         super(new ComponentName(packageName, id), user);
     }
 
+    public ShortcutKey(Context context, String componentKeyStr) {
+        super(context, componentKeyStr);
+    }
+
     public String getId() {
         return componentName.getClassName();
     }
diff --git a/src/com/android/launcher3/states/SpringLoadedState.java b/src/com/android/launcher3/states/SpringLoadedState.java
index 995cdaa..da656db 100644
--- a/src/com/android/launcher3/states/SpringLoadedState.java
+++ b/src/com/android/launcher3/states/SpringLoadedState.java
@@ -70,7 +70,7 @@
         float myCenter = ws.getTop() + halfHeight;
         float cellTopFromCenter = halfHeight - ws.getChildAt(0).getTop();
         float actualCellTop = myCenter - cellTopFromCenter * scale;
-        return new float[] { scale, (desiredCellTop - actualCellTop) / scale};
+        return new float[] { scale, 0, (desiredCellTop - actualCellTop) / scale};
     }
 
     @Override
diff --git a/src/com/android/launcher3/util/InstantAppResolver.java b/src/com/android/launcher3/util/InstantAppResolver.java
index 99ce7ca..601a5ab 100644
--- a/src/com/android/launcher3/util/InstantAppResolver.java
+++ b/src/com/android/launcher3/util/InstantAppResolver.java
@@ -18,8 +18,11 @@
 
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.util.Log;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 
@@ -44,6 +47,17 @@
         return false;
     }
 
+    public boolean isInstantApp(Launcher launcher, String packageName) {
+        PackageManager packageManager = launcher.getPackageManager();
+        try {
+            return isInstantApp(packageManager.getPackageInfo(packageName, 0).applicationInfo);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e("InstantAppResolver", "Failed to determine whether package is instant app "
+                    + packageName, e);
+        }
+        return false;
+    }
+
     public List<ApplicationInfo> getInstantApps() {
         return Collections.emptyList();
     }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
index d67156f..485e97b 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/AllAppsState.java
@@ -68,7 +68,7 @@
 
     @Override
     public float[] getWorkspaceScaleAndTranslation(Launcher launcher) {
-        return new float[] { 1f,
+        return new float[] { 1f, 0,
                 -launcher.getAllAppsController().getShiftRange()
                         * AllAppsTransitionController.PARALLAX_COEFFICIENT};
     }
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
index 73f208e..19967ae 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
@@ -59,7 +59,7 @@
         int workspaceOffsetTopEdge =
                 workspaceTop + ((workspaceBottom - workspaceTop) - scaledHeight) / 2;
         int overviewOffsetTopEdge = overviewTop + (overviewBottom - overviewTop - scaledHeight) / 2;
-        return new float[] {SCALE_FACTOR, -workspaceOffsetTopEdge + overviewOffsetTopEdge };
+        return new float[] {SCALE_FACTOR, 0, -workspaceOffsetTopEdge + overviewOffsetTopEdge };
     }
 
     @Override