Merge "Fix work tab is gone when leaving search mode" into ub-launcher3-master
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 08c740c..02b4379 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -23,6 +23,7 @@
 
     <uses-sdk android:targetSdkVersion="23" android:minSdkVersion="21"/>
 
+    <uses-permission android:name="android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS" />
     <application
         android:backupAgent="com.android.launcher3.LauncherBackupAgent"
         android:fullBackupOnly="true"
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index 9069698..18ddeee 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
deleted file mode 100644
index f1f48ac..0000000
--- a/quickstep/res/drawable/task_thumbnail_background.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 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.
--->
-<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
-    <corners android:radius="2dp" />
-</shape>
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index 78238fa..9f4f8a1 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -26,32 +26,6 @@
 
     <com.android.launcher3.uioverrides.WorkspaceCard
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:paddingTop="@dimen/task_thumbnail_top_margin" >
-
-        <View
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:id="@+id/workspace_click_target" />
-
-        <FrameLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:id="@+id/widget_button"
-            android:background="@drawable/bg_workspace_card_button" >
-
-            <TextView
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/widget_button_text"
-                android:drawableStart="@drawable/ic_widget"
-                android:textColor="?attr/workspaceTextColor"
-                android:drawableTint="?attr/workspaceTextColor"
-                android:gravity="center"
-                android:layout_gravity="center"
-                android:drawablePadding="20dp" />
-        </FrameLayout>
-
-    </com.android.launcher3.uioverrides.WorkspaceCard>
+        android:layout_height="match_parent" />
 
 </com.android.quickstep.RecentsView>
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index 839d934..91b6aa3 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
      Copyright (C) 2017 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,23 +13,20 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.quickstep.TaskView
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.quickstep.TaskView xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:elevation="4dp">
 
     <com.android.quickstep.TaskThumbnailView
         android:id="@+id/snapshot"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/task_thumbnail_top_margin"
-        android:scaleType="matrix"
-        android:background="@drawable/task_thumbnail_background"
-        android:elevation="4dp" />
+        android:layout_marginTop="@dimen/task_thumbnail_top_margin" />
+
     <ImageView
         android:id="@+id/icon"
         android:layout_width="@dimen/task_thumbnail_icon_size"
         android:layout_height="@dimen/task_thumbnail_icon_size"
-        android:layout_gravity="top|center_horizontal"
-        android:elevation="5dp"/>
+        android:layout_gravity="top|center_horizontal" />
 </com.android.quickstep.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 222a3f4..9ef8e82 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -19,6 +19,9 @@
     <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="task_corner_radius">2dp</dimen>
+    <dimen name="task_fade_length">20dp</dimen>
+
 
     <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
     <dimen name="quickstep_fling_min_velocity">250dp</dimen>
@@ -27,4 +30,7 @@
 
     <!-- TODO: This can be calculated using other resource values -->
     <dimen name="all_apps_search_box_full_height">90dp</dimen>
+
+    <dimen name="drag_layer_trans_y">25dp</dimen>
+
 </resources>
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManager.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManager.java
new file mode 100644
index 0000000..8ee1b85
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManager.java
@@ -0,0 +1,286 @@
+/*
+ * 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.launcher3;
+
+import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
+import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
+import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+
+import com.android.launcher3.InsettableFrameLayout.LayoutParams;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.systemui.shared.system.ActivityOptionsCompat;
+import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
+import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
+import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
+import com.android.systemui.shared.system.TransactionCompat;
+
+/**
+ * Manages the opening app animations from Launcher.
+ */
+public class LauncherAppTransitionManager {
+
+    private static final int REFRESH_RATE_MS = 16;
+
+    private final DragLayer mDragLayer;
+    private final Launcher mLauncher;
+    private final DeviceProfile mDeviceProfile;
+
+    private final float mDragLayerTransY;
+
+    private ImageView mFloatingView;
+
+    public LauncherAppTransitionManager(Launcher launcher) {
+        mLauncher = launcher;
+        mDragLayer = launcher.getDragLayer();
+        mDeviceProfile = launcher.getDeviceProfile();
+
+        mDragLayerTransY =
+                launcher.getResources().getDimensionPixelSize(R.dimen.drag_layer_trans_y);
+    }
+
+    public Bundle getActivityLauncherOptions(View v) {
+        RemoteAnimationRunnerCompat runner = new RemoteAnimationRunnerCompat() {
+            @Override
+            public void onAnimationStart(RemoteAnimationTargetCompat[] targets,
+                    Runnable finishedCallback) {
+                // Post at front of queue ignoring sync barriers to make sure it gets processed
+                // before the next frame.
+                postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
+                    AnimatorSet both = new AnimatorSet();
+                    both.play(getLauncherAnimators(v));
+                    both.play(getAppWindowAnimators(v, targets));
+                    both.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            // Reset launcher to normal state
+                            v.setVisibility(View.VISIBLE);
+                            ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
+
+                            mDragLayer.setAlpha(1f);
+                            mDragLayer.setTranslationY(0f);
+                            finishedCallback.run();
+                        }
+                    });
+                    both.start();
+                    // Because t=0 has the app icon in its original spot, we can skip the first
+                    // frame and have the same movement one frame earlier.
+                    both.setCurrentPlayTime(REFRESH_RATE_MS);
+                });
+            }
+
+            @Override
+            public void onAnimationCancelled() {
+            }
+        };
+
+        return ActivityOptionsCompat.makeRemoteAnimation(
+                new RemoteAnimationAdapterCompat(runner, 500, 380)).toBundle();
+    }
+
+    private AnimatorSet getLauncherAnimators(View v) {
+        AnimatorSet launcherAnimators = new AnimatorSet();
+        launcherAnimators.play(getHideLauncherAnimator());
+        launcherAnimators.play(getAppIconAnimator(v));
+        return launcherAnimators;
+    }
+
+    private AnimatorSet getHideLauncherAnimator() {
+        AnimatorSet hideLauncher = new AnimatorSet();
+
+        // Animate Launcher so that it moves downwards and fades out.
+        ObjectAnimator dragLayerAlpha = ObjectAnimator.ofFloat(mDragLayer, View.ALPHA, 1f, 0f);
+        dragLayerAlpha.setDuration(217);
+        dragLayerAlpha.setInterpolator(Interpolators.LINEAR);
+        ObjectAnimator dragLayerTransY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, 0,
+                mDragLayerTransY);
+        dragLayerTransY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+        dragLayerTransY.setDuration(350);
+
+        hideLauncher.play(dragLayerAlpha);
+        hideLauncher.play(dragLayerTransY);
+        return hideLauncher;
+    }
+
+    private AnimatorSet getAppIconAnimator(View v) {
+        // Create a copy of the app icon
+        mFloatingView = new ImageView(mLauncher);
+        Bitmap iconBitmap = ((FastBitmapDrawable) ((BubbleTextView) v).getIcon()).getBitmap();
+        mFloatingView.setImageDrawable(new FastBitmapDrawable(iconBitmap));
+
+        // Position the copy of the app icon exactly on top of the original
+        Rect rect = new Rect();
+        mDragLayer.getDescendantRectRelativeToSelf(v, rect);
+        int viewLocationLeft = rect.left;
+        int viewLocationTop = rect.top;
+
+        ((BubbleTextView) v).getIconBounds(rect);
+        LayoutParams lp = new LayoutParams(rect.width(), rect.height());
+        lp.ignoreInsets = true;
+        lp.leftMargin = viewLocationLeft + rect.left;
+        lp.topMargin = viewLocationTop + rect.top;
+        mFloatingView.setLayoutParams(lp);
+
+        // Swap the two views in place.
+        ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView);
+        v.setVisibility(View.INVISIBLE);
+
+        AnimatorSet appIconAnimatorSet = new AnimatorSet();
+        // Animate the app icon to the center
+        float centerX = mDeviceProfile.widthPx / 2;
+        float centerY = mDeviceProfile.heightPx / 2;
+        float dX = centerX - lp.leftMargin - (lp.width / 2);
+        float dY = centerY - lp.topMargin - (lp.height / 2);
+        ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX);
+        ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY);
+
+        // Adjust the duration to change the "curve" of the app icon to the center.
+        boolean isBelowCenterY = lp.topMargin < centerY;
+        x.setDuration(isBelowCenterY ? 500 : 233);
+        y.setDuration(isBelowCenterY ? 233 : 500);
+        appIconAnimatorSet.play(x);
+        appIconAnimatorSet.play(y);
+
+        // Scale the app icon to take up the entire screen. This simplifies the math when
+        // animating the app window position / scale.
+        float maxScaleX = mDeviceProfile.widthPx / (float) rect.width();
+        float maxScaleY = mDeviceProfile.heightPx / (float) rect.height();
+        float scale = Math.max(maxScaleX, maxScaleY);
+        ObjectAnimator sX = ObjectAnimator.ofFloat(mFloatingView, View.SCALE_X, 1f, scale);
+        ObjectAnimator sY = ObjectAnimator.ofFloat(mFloatingView, View.SCALE_Y, 1f, scale);
+        sX.setDuration(500);
+        sY.setDuration(500);
+        appIconAnimatorSet.play(sX);
+        appIconAnimatorSet.play(sY);
+
+        // Fade out the app icon.
+        ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
+        alpha.setStartDelay(17);
+        alpha.setDuration(33);
+        appIconAnimatorSet.play(alpha);
+
+        for (Animator a : appIconAnimatorSet.getChildAnimations()) {
+            a.setInterpolator(Interpolators.AGGRESSIVE_EASE);
+        }
+        return appIconAnimatorSet;
+    }
+
+    private ValueAnimator getAppWindowAnimators(View v, RemoteAnimationTargetCompat[] targets) {
+        Rect iconBounds = new Rect();
+        ((BubbleTextView) v).getIconBounds(iconBounds);
+        int[] floatingViewBounds = new int[2];
+
+        Rect crop = new Rect();
+        Matrix matrix = new Matrix();
+
+        ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+        appAnimator.setDuration(500);
+        appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            boolean isFirstFrame = true;
+
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final float percent = animation.getAnimatedFraction();
+                final float easePercent = Interpolators.AGGRESSIVE_EASE.getInterpolation(percent);
+
+                // Calculate app icon size.
+                float iconWidth = iconBounds.width() * mFloatingView.getScaleX();
+                float iconHeight = iconBounds.height() * mFloatingView.getScaleY();
+
+                // Scale the app window to match the icon size.
+                float scaleX = iconWidth / mDeviceProfile.widthPx;
+                float scaleY = iconHeight / mDeviceProfile.heightPx;
+                float scale = Math.min(1f, Math.min(scaleX, scaleY));
+                matrix.setScale(scale, scale);
+
+                // Position the scaled window on top of the icon
+                int deviceWidth = mDeviceProfile.widthPx;
+                int deviceHeight = mDeviceProfile.heightPx;
+                float scaledWindowWidth = deviceWidth * scale;
+                float scaledWindowHeight = deviceHeight * scale;
+
+                float offsetX = (scaledWindowWidth - iconWidth) / 2;
+                float offsetY = (scaledWindowHeight - iconHeight) / 2;
+                mFloatingView.getLocationInWindow(floatingViewBounds);
+                float transX0 = floatingViewBounds[0] - offsetX;
+                float transY0 = floatingViewBounds[1] - offsetY;
+                matrix.postTranslate(transX0, transY0);
+
+                // Fade in the app window.
+                float alphaDelay = 0;
+                float alphaDuration = 50;
+                float alpha = getValue(1f, 0f, alphaDelay, alphaDuration,
+                        appAnimator.getDuration() * percent, Interpolators.AGGRESSIVE_EASE);
+
+                // Animate the window crop so that it starts off as a square, and then reveals
+                // horizontally.
+                float cropHeight = deviceHeight * easePercent + deviceWidth * (1 - easePercent);
+                float initialTop = (deviceHeight - deviceWidth) / 2f;
+                crop.left = 0;
+                crop.top = (int) (initialTop * (1 - easePercent));
+                crop.right = deviceWidth;
+                crop.bottom = (int) (crop.top + cropHeight);
+
+                TransactionCompat t = new TransactionCompat();
+                for (RemoteAnimationTargetCompat target : targets) {
+                    if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+                        t.setAlpha(target.leash, alpha);
+                        t.setMatrix(target.leash, matrix);
+                        t.setWindowCrop(target.leash, crop);
+                        Surface surface = getSurface(mFloatingView);
+                        t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
+                    }
+                    if (isFirstFrame) {
+                        t.show(target.leash);
+                    }
+                }
+                t.apply();
+
+                matrix.reset();
+                isFirstFrame = false;
+            }
+
+            /**
+             * Helper method that allows us to get interpolated values for embedded
+             * animations with a delay and/or different duration.
+             */
+            private float getValue(float start, float end, float delay, float duration,
+                                   float currentPlayTime, Interpolator i) {
+                float time = Math.max(0, currentPlayTime - delay);
+                float newPercent = Math.min(1f, time / duration);
+                newPercent = i.getInterpolation(newPercent);
+                return start * newPercent + end * (1 - newPercent);
+            }
+        });
+        return appAnimator;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
index 20ee547..6395473 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java
@@ -85,4 +85,9 @@
     public float getHoseatAlpha(Launcher launcher) {
         return launcher.getDeviceProfile().isVerticalBarLayout() ? 0 : 1;
     }
+
+    @Override
+    public LauncherState getHistoryForState(LauncherState previousState) {
+        return previousState == OVERVIEW ? OVERVIEW : NORMAL;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/EdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/EdgeSwipeController.java
new file mode 100644
index 0000000..356a144
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/EdgeSwipeController.java
@@ -0,0 +1,96 @@
+/*
+ * 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.launcher3.uioverrides;
+
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.touch.SwipeDetector.DIRECTION_NEGATIVE;
+import static com.android.launcher3.touch.SwipeDetector.DIRECTION_POSITIVE;
+import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;
+import static com.android.launcher3.touch.SwipeDetector.VERTICAL;
+import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.anim.SpringAnimationHandler;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.util.VerticalSwipeController;
+import com.android.quickstep.RecentsView;
+
+/**
+ * Extension of {@link VerticalSwipeController} to go from NORMAL to OVERVIEW.
+ */
+public class EdgeSwipeController extends VerticalSwipeController {
+
+    private static final Rect sTempRect = new Rect();
+
+    public EdgeSwipeController(Launcher l) {
+        super(l, NORMAL, OVERVIEW, l.getDeviceProfile().isVerticalBarLayout()
+                ? HORIZONTAL : VERTICAL);
+    }
+
+    @Override
+    protected boolean shouldInterceptTouch(MotionEvent ev) {
+        return mLauncher.isInState(NORMAL) && (ev.getEdgeFlags() & EDGE_NAV_BAR) != 0;
+    }
+
+    @Override
+    protected int getSwipeDirection(MotionEvent ev) {
+        return isTransitionFlipped() ? DIRECTION_NEGATIVE : DIRECTION_POSITIVE;
+    }
+
+    @Override
+    protected boolean isTransitionFlipped() {
+        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+            Rect insets = mLauncher.getDragLayer().getInsets();
+            return insets.left > insets.right;
+        }
+        return false;
+    }
+
+    @Override
+    protected void onTransitionComplete(boolean wasFling, boolean stateChanged) {
+        // TODO: Log something
+    }
+
+    @Override
+    protected void initSprings() {
+        mSpringHandlers = new SpringAnimationHandler[0];
+    }
+
+    @Override
+    protected float getShiftRange() {
+        return getShiftRange(mLauncher);
+    }
+
+    public static float getShiftRange(Launcher launcher) {
+        RecentsView.getPageRect(launcher.getDeviceProfile(), launcher, sTempRect);
+        DragLayer dl = launcher.getDragLayer();
+        Rect insets = dl.getInsets();
+
+        if (launcher.getDeviceProfile().isVerticalBarLayout()) {
+            if (insets.left > insets.right) {
+                return insets.left + sTempRect.left;
+            } else {
+                return dl.getWidth() - sTempRect.right + insets.right;
+            }
+        } else {
+            return dl.getHeight() - sTempRect.bottom + insets.bottom;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 989803a..68f6eed 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 
+import android.content.Context;
 import android.graphics.Rect;
 import android.view.View;
 
@@ -35,8 +36,6 @@
  */
 public class OverviewState extends LauncherState {
 
-    public static final float WORKSPACE_SCALE_ON_SCROLL = 0.9f;
-
     private static final int STATE_FLAGS = FLAG_SHOW_SCRIM | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED;
 
     public OverviewState(int id) {
@@ -46,15 +45,15 @@
     @Override
     public float[] getWorkspaceScaleAndTranslation(Launcher launcher) {
         Rect pageRect = new Rect();
-        RecentsView.getPageRect(launcher, pageRect);
+        RecentsView.getScaledDownPageRect(launcher.getDeviceProfile(), launcher, pageRect);
+        RecentsView rv = launcher.getOverviewPanel();
+
         if (launcher.getWorkspace().getNormalChildWidth() <= 0 || pageRect.isEmpty()) {
             return super.getWorkspaceScaleAndTranslation(launcher);
         }
 
-        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, overlap, pageRect);
@@ -74,15 +73,7 @@
 
     @Override
     public float getVerticalProgress(Launcher launcher) {
-        DeviceProfile grid = launcher.getDeviceProfile();
-        if (!grid.isVerticalBarLayout()) {
-            return 1f;
-        }
-
-        float total = grid.heightPx;
-        float searchHeight = total - grid.availableHeightPx +
-                launcher.getResources().getDimension(R.dimen.all_apps_search_box_full_height);
-        return 1 - (searchHeight / total);
+        return getVerticalProgress(launcher.getDeviceProfile(), launcher);
     }
 
     @Override
@@ -106,23 +97,36 @@
         float childWidth = ws.getNormalChildWidth();
         float childHeight = ws.getNormalChildHeight();
 
+        float scale = pageRect.height() / childHeight;
         Rect insets = launcher.getDragLayer().getInsets();
-        float scale = Math.min(pageRect.width() / childWidth, pageRect.height() / childHeight);
 
-        float halfHeight = ws.getHeight() / 2;
+        float halfHeight = ws.getExpectedHeight() / 2;
         float childTop = halfHeight - scale * (halfHeight - ws.getPaddingTop() - insets.top);
         float translationY = pageRect.top - childTop;
 
-        float halfWidth = ws.getWidth() / 2;
-        float translationX;
+        // Align the workspace horizontally centered with the task rect
+        float halfWidth = ws.getExpectedWidth() / 2;
+        float childCenter = halfWidth -
+                scale * (halfWidth - ws.getPaddingLeft() - insets.left - childWidth / 2);
+        float translationX = pageRect.exactCenterX() - childCenter;
+
         if (Utilities.isRtl(launcher.getResources())) {
-            float childRight = halfWidth + scale * (halfWidth - ws.getPaddingRight() - insets.right);
-            translationX = childRight - pageRect.right - offsetX / scale;
+            translationX -= offsetX / scale;
         } else {
-            float childLeft = halfWidth - scale * (halfWidth - ws.getPaddingLeft() - insets.left);
-            translationX = pageRect.left - childLeft + offsetX / scale;
+            translationX += offsetX / scale;
         }
 
         return new float[] {scale, translationX, translationY};
     }
+
+    public static float getVerticalProgress(DeviceProfile grid, Context context) {
+        if (!grid.isVerticalBarLayout()) {
+            return 1f;
+        }
+
+        float total = grid.heightPx;
+        float searchHeight = total - grid.availableHeightPx +
+                context.getResources().getDimension(R.dimen.all_apps_search_box_full_height);
+        return 1 - (searchHeight / total);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
new file mode 100644
index 0000000..1fd541a
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
@@ -0,0 +1,309 @@
+/*
+ * 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.launcher3.uioverrides;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.util.TouchController;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TaskView;
+
+import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
+
+/**
+ * Touch controller for swipe interaction in Overview state
+ */
+public class OverviewSwipeController extends AnimatorListenerAdapter
+        implements TouchController, SwipeDetector.Listener {
+
+    private static final String TAG = "OverviewSwipeController";
+
+    private static final float ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS = 0.1f;
+    private static final int SINGLE_FRAME_MS = 16;
+
+    // Progress after which the transition is assumed to be a success in case user does not fling
+    private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+
+    private final Launcher mLauncher;
+    private final SwipeDetector mDetector;
+    private final RecentsView mRecentsView;
+    private final int[] mTempCords = new int[2];
+
+    private AnimatorPlaybackController mCurrentAnimation;
+    private boolean mCurrentAnimationIsGoingUp;
+
+    private boolean mNoIntercept;
+    private boolean mSwipeDownEnabled;
+
+    private float mDisplacementShift;
+    private float mProgressMultiplier;
+    private float mEndDisplacement;
+
+    private TaskView mTaskBeingDragged;
+
+    public OverviewSwipeController(Launcher launcher) {
+        mLauncher = launcher;
+        mRecentsView = launcher.getOverviewPanel();
+        mDetector = new SwipeDetector(launcher, this, SwipeDetector.VERTICAL);
+    }
+
+    private boolean canInterceptTouch() {
+        if (mCurrentAnimation != null) {
+            // If we are already animating from a previous state, we can intercept.
+            return true;
+        }
+        if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
+            return false;
+        }
+        return mLauncher.isInState(OVERVIEW);
+    }
+
+    private boolean isEventOverHotseat(MotionEvent ev) {
+        if (mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+            return ev.getY() >
+                    mLauncher.getDragLayer().getHeight() * OVERVIEW.getVerticalProgress(mLauncher);
+        } else {
+            return mLauncher.getDragLayer().isEventOverHotseat(ev);
+        }
+    }
+
+    @Override
+    public void onAnimationCancel(Animator animation) {
+        if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
+            Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
+            mDetector.finishedScrolling();
+            mCurrentAnimation = null;
+        }
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mNoIntercept = !canInterceptTouch();
+            if (mNoIntercept) {
+                return false;
+            }
+
+            // Now figure out which direction scroll events the controller will start
+            // calling the callbacks.
+            final int directionsToDetectScroll;
+            boolean ignoreSlopWhenSettling = false;
+
+            if (mCurrentAnimation != null) {
+                directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                ignoreSlopWhenSettling = true;
+            } else {
+                mTaskBeingDragged = null;
+                mSwipeDownEnabled = true;
+
+                int currentPage = mRecentsView.getCurrentPage();
+                if (currentPage == 0) {
+                    // User is on home tile
+                    directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                } else {
+                    View view = mRecentsView.getChildAt(currentPage);
+                    if (mLauncher.getDragLayer().isEventOverView(view, ev) &&
+                            view instanceof TaskView) {
+                        // The tile can be dragged down to open the task.
+                        mTaskBeingDragged = (TaskView) view;
+                        directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH;
+                    } else if (isEventOverHotseat(ev)) {
+                        // The hotseat is being dragged
+                        directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE;
+                        mSwipeDownEnabled = false;
+                    } else {
+                        mNoIntercept = true;
+                        return false;
+                    }
+                }
+            }
+
+            mDetector.setDetectableScrollConditions(
+                    directionsToDetectScroll, ignoreSlopWhenSettling);
+        }
+
+        if (mNoIntercept) {
+            return false;
+        }
+
+        onControllerTouchEvent(ev);
+        return mDetector.isDraggingOrSettling();
+    }
+
+    @Override
+    public boolean onControllerTouchEvent(MotionEvent ev) {
+        return mDetector.onTouchEvent(ev);
+    }
+
+    private void reInitAnimationController(boolean goingUp) {
+        if (!goingUp && !mSwipeDownEnabled) {
+            goingUp = true;
+        }
+        if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
+            // No need to init
+            return;
+        }
+        if (mCurrentAnimation != null) {
+            mCurrentAnimation.setPlayFraction(0);
+        }
+        mCurrentAnimationIsGoingUp = goingUp;
+        float range = mLauncher.getAllAppsController().getShiftRange();
+        long maxDuration = (long) (2 * range);
+        DragLayer dl = mLauncher.getDragLayer();
+
+        if (mTaskBeingDragged == null) {
+            // User is either going to all apps or home
+            mCurrentAnimation = mLauncher.getStateManager()
+                    .createAnimationToNewWorkspace(goingUp ? ALL_APPS : NORMAL, maxDuration);
+            if (goingUp) {
+                mEndDisplacement = -range;
+            } else {
+                mEndDisplacement = EdgeSwipeController.getShiftRange(mLauncher);
+            }
+        } else {
+            if (goingUp) {
+                AnimatorSet anim = new AnimatorSet();
+                ObjectAnimator translate = ObjectAnimator.ofFloat(
+                        mTaskBeingDragged, View.TRANSLATION_Y, -mTaskBeingDragged.getBottom());
+                translate.setInterpolator(LINEAR);
+                translate.setDuration(maxDuration);
+                anim.play(translate);
+
+                ObjectAnimator alpha = ObjectAnimator.ofFloat(mTaskBeingDragged, View.ALPHA, 0);
+                alpha.setInterpolator(DEACCEL_1_5);
+                alpha.setDuration(maxDuration);
+                anim.play(alpha);
+                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
+                mEndDisplacement = -mTaskBeingDragged.getBottom();
+            } else {
+                AnimatorSet anim = new AnimatorSet();
+                // TODO: Setup a zoom animation
+                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
+
+                mTempCords[1] = mTaskBeingDragged.getHeight();
+                dl.getDescendantCoordRelativeToSelf(mTaskBeingDragged, mTempCords);
+                mEndDisplacement = dl.getHeight() - mTempCords[1];
+            }
+        }
+
+        mCurrentAnimation.getTarget().addListener(this);
+        mCurrentAnimation.dispatchOnStart();
+        mProgressMultiplier = 1 / mEndDisplacement;
+    }
+
+    @Override
+    public void onDragStart(boolean start) {
+        if (mCurrentAnimation == null) {
+            reInitAnimationController(mDetector.wasInitialTouchPositive());
+            mDisplacementShift = 0;
+        } else {
+            mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
+            mCurrentAnimation.pause();
+        }
+    }
+
+    @Override
+    public boolean onDrag(float displacement, float velocity) {
+        float totalDisplacement = displacement + mDisplacementShift;
+        boolean isGoingUp =
+                totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0;
+        if (isGoingUp != mCurrentAnimationIsGoingUp) {
+            reInitAnimationController(isGoingUp);
+        }
+        mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier);
+        return true;
+    }
+
+    @Override
+    public void onDragEnd(float velocity, boolean fling) {
+        final boolean goingToEnd;
+
+        if (fling) {
+            boolean goingUp = velocity < 0;
+            if (!goingUp && !mSwipeDownEnabled) {
+                goingToEnd = false;
+            } else if (goingUp != mCurrentAnimationIsGoingUp) {
+                // In case the fling is in opposite direction, make sure if is close enough
+                // from the start position
+                if (mCurrentAnimation.getProgressFraction()
+                        >= ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS) {
+                    // Not allowed
+                    goingToEnd = false;
+                } else {
+                    reInitAnimationController(goingUp);
+                    goingToEnd = true;
+                }
+            } else {
+                goingToEnd = true;
+            }
+        } else {
+            goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS;
+        }
+
+        float progress = mCurrentAnimation.getProgressFraction();
+        long animationDuration = SwipeDetector.calculateDuration(
+                velocity, goingToEnd ? (1 - progress) : progress);
+
+        float nextFrameProgress = Utilities.boundToRange(
+                progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f);
+
+
+        mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd));
+
+        ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+        anim.setFloatValues(nextFrameProgress, goingToEnd ? 1f : 0f);
+        anim.setDuration(animationDuration);
+        anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+        anim.start();
+    }
+
+    private void onCurrentAnimationEnd(boolean wasSuccess) {
+        // TODO: Might be a good time to log something.
+        if (mTaskBeingDragged == null) {
+            LauncherState state = wasSuccess ?
+                    (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW;
+            mLauncher.getStateManager().goToState(state, false);
+        } else if (wasSuccess) {
+            if (mCurrentAnimationIsGoingUp) {
+                mRecentsView.onTaskDismissed(mTaskBeingDragged);
+            } else {
+                mTaskBeingDragged.launchTask(false);
+            }
+        }
+        mDetector.finishedScrolling();
+        mTaskBeingDragged = null;
+        mCurrentAnimation = null;
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index d657e4e..1465822 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -15,6 +15,9 @@
  */
 package com.android.launcher3.uioverrides;
 
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
@@ -31,9 +34,6 @@
 import com.android.quickstep.AnimatedFloat;
 import com.android.quickstep.RecentsView;
 
-import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.LauncherState.OVERVIEW;
-
 public class RecentsViewStateController implements StateHandler {
 
     private final Launcher mLauncher;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
index f59f0de..fb59946 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
@@ -20,6 +20,7 @@
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 import static com.android.launcher3.anim.SpringAnimationHandler.Y_DIRECTION;
+import static com.android.quickstep.TouchInteractionService.EDGE_NAV_BAR;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -42,6 +43,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.AnimatorSetBuilder;
 import com.android.launcher3.anim.SpringAnimationHandler;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -88,8 +90,9 @@
     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 static final int FLAG_DISABLED_TWO_TARGETS = 1 << 5;
+    private static final int FLAG_DISABLED_BACK_TARGET = 1 << 6;
 
     private final Launcher mLauncher;
     private final SwipeDetector mDetector;
@@ -120,18 +123,25 @@
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
-        if (!mLauncher.isInState(NORMAL) && !mLauncher.isInState(ALL_APPS)) {
-            // Don't listen for the swipe gesture if we are already in some other state.
-            return false;
-        }
-        if (mAnimatingToOverview) {
-            return false;
-        }
         if (mCurrentAnimation != null) {
             // If we are already animating from a previous state, we can intercept.
             return true;
         }
-        if (mLauncher.isInState(ALL_APPS) && !mLauncher.getAppsView().shouldContainerScroll(ev)) {
+        if (mLauncher.isInState(NORMAL)) {
+            if ((ev.getEdgeFlags() & EDGE_NAV_BAR) != 0 &&
+                    !mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+                // On normal swipes ignore edge swipes
+                return false;
+            }
+        } else if (mLauncher.isInState(ALL_APPS)) {
+            if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
+                return false;
+            }
+        } else {
+            // Don't listen for the swipe gesture if we are already in some other state.
+            return false;
+        }
+        if (mAnimatingToOverview) {
             return false;
         }
         if (AbstractFloatingView.getTopOpenView(mLauncher) != null) {
@@ -237,6 +247,10 @@
 
             mDragPauseDetector = new DragPauseDetector(this::onDragPauseDetected);
             mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED_OUT_OF_RANGE);
+            if (FeatureFlags.ENABLE_TWO_SWIPE_TARGETS) {
+                mDragPauseDetector.addDisabledFlags(FLAG_DISABLED_TWO_TARGETS);
+            }
+
             mOverviewProgressRange = new FloatRange();
             mOverviewProgressRange.start = mLauncher.isInState(NORMAL)
                     ? MIN_PROGRESS_TO_OVERVIEW
@@ -247,22 +261,17 @@
             // Build current animation
             mFromState = mLauncher.getStateManager().getState();
             mToState = mLauncher.isInState(ALL_APPS) ? NORMAL : ALL_APPS;
+
+            if (mToState == NORMAL && mLauncher.getStateManager().getLastState() == OVERVIEW) {
+                mToState = OVERVIEW;
+                mDragPauseDetector.addDisabledFlags(FLAG_DISABLED_BACK_TARGET);
+            }
+
             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 {
+            if (!TouchInteractionService.isConnected()) {
                 mDragPauseDetector.addDisabledFlags(FLAG_OVERVIEW_DISABLED);
             }
 
@@ -283,14 +292,6 @@
         }
     }
 
-    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();
     }
@@ -324,7 +325,7 @@
 
         if (fling) {
             logAction = Touch.FLING;
-            targetState = velocity < 0 ? ALL_APPS : NORMAL;
+            targetState = velocity < 0 ? ALL_APPS : mLauncher.getStateManager().getLastState();
             // snap to top or bottom using the release velocity
         } else {
             logAction = Touch.SWIPE;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index 05bd171..67a7d6a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -19,17 +19,21 @@
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.os.Bundle;
+import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.widget.PopupMenu;
 import android.widget.Toast;
 
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppTransitionManager;
 import com.android.launcher3.LauncherStateManager.StateHandler;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.widget.WidgetsFullSheet;
+import com.android.quickstep.RecentsView;
 import com.android.systemui.shared.recents.view.RecentsTransition;
 
 public class UiFactory {
@@ -37,9 +41,16 @@
     public static final boolean USE_HARDWARE_BITMAP = false; // FeatureFlags.IS_DOGFOOD_BUILD;
 
     public static TouchController[] createTouchControllers(Launcher launcher) {
-        return new TouchController[] {
-                new TwoStepSwipeController(launcher),
-                new OverviewSwipeUpController(launcher)};
+        if (FeatureFlags.ENABLE_TWO_SWIPE_TARGETS) {
+            return new TouchController[]{
+                    new EdgeSwipeController(launcher),
+                    new TwoStepSwipeController(launcher),
+                    new OverviewSwipeController(launcher)};
+        } else {
+            return new TouchController[]{
+                    new TwoStepSwipeController(launcher),
+                    new OverviewSwipeController(launcher)};
+        }
     }
 
     public static AccessibilityDelegate newPageIndicatorAccessibilityDelegate() {
@@ -89,4 +100,13 @@
             return result;
         }
     }
+
+    public static void resetOverview(Launcher launcher) {
+        RecentsView recents = launcher.getOverviewPanel();
+        recents.reset();
+    }
+
+    public static Bundle getActivityLaunchOptions(Launcher launcher, View v) {
+        return new LauncherAppTransitionManager(launcher).getActivityLauncherOptions(v);
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java b/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java
index ed60b84..92a09dd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/WorkspaceCard.java
@@ -16,169 +16,80 @@
 package com.android.launcher3.uioverrides;
 
 import static com.android.launcher3.LauncherState.NORMAL;
-import static com.android.launcher3.uioverrides.OverviewState.WORKSPACE_SCALE_ON_SCROLL;
 import static com.android.quickstep.RecentsView.SCROLL_TYPE_WORKSPACE;
 
-import android.animation.FloatArrayEvaluator;
 import android.content.Context;
+import android.graphics.Canvas;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.widget.FrameLayout;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.widget.WidgetsFullSheet;
 import com.android.quickstep.RecentsView;
 import com.android.quickstep.RecentsView.PageCallbacks;
 import com.android.quickstep.RecentsView.ScrollState;
 
-public class WorkspaceCard extends FrameLayout implements PageCallbacks, OnClickListener {
+public class WorkspaceCard extends View implements PageCallbacks, OnClickListener {
 
     private final Rect mTempRect = new Rect();
-    private final float[] mEvaluatedFloats = new float[3];
-    private final FloatArrayEvaluator mEvaluator = new FloatArrayEvaluator(mEvaluatedFloats);
-
-    // UI related information
-    private float[] mScaleAndTranslatePage0, mScaleAndTranslatePage1;
-    private boolean mUIDataValid = false;
 
     private Launcher mLauncher;
     private Workspace mWorkspace;
 
+    private float mLinearInterpolationForPage2 = 1;
+    private float mTranslateXPage0, mTranslateXPage1;
+    private float mExtraScrollShift;
+
     private boolean mIsWorkspaceScrollingEnabled;
 
-    private View mWorkspaceClickTarget;
-    private View mWidgetsButton;
-
-    private boolean mLayoutHorizontal;
-
     public WorkspaceCard(Context context) {
-        super(context);
+        this(context, null);
     }
 
     public WorkspaceCard(Context context, AttributeSet attrs) {
-        super(context, attrs);
+        this(context, attrs, 0);
     }
 
     public WorkspaceCard(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        setOnClickListener(this);
     }
 
+    /**
+     * Draw nothing.
+     */
     @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
-        mWorkspaceClickTarget = findViewById(R.id.workspace_click_target);
-        mWidgetsButton = findViewById(R.id.widget_button);
-
-        mWorkspaceClickTarget.setOnClickListener(this);
-        mWidgetsButton.setOnClickListener(this);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        // We measure the dimensions of the PagedView to be larger than the pages so that when we
-        // zoom out (and scale down), the view is still contained in the parent
-        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
-        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
-        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
-        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-
-        if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
-            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-            return;
-        }
-
-        // Return early if we aren't given a proper dimension
-        if (widthSize <= 0 || heightSize <= 0) {
-            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-            return;
-        }
-
-        float workspaceWidth = mWorkspace.getNormalChildWidth();
-        float workspaceHeight = mWorkspace.getNormalChildHeight();
-
-        int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
-        float scaleX = availableWidth / workspaceWidth;
-
-        int availableHeight = heightSize - getPaddingTop() - getPaddingBottom();
-        float scaleY = availableHeight / workspaceHeight;
-
-        if (scaleX < scaleY) {
-            mLayoutHorizontal = false;
-            int childWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY);
-
-            int pageHeight = Math.round(workspaceHeight * scaleX);
-            mWorkspaceClickTarget.measure(childWidthSpec,
-                    MeasureSpec.makeMeasureSpec(pageHeight, MeasureSpec.EXACTLY));
-
-            int buttonHeight = availableHeight - pageHeight;
-            mWidgetsButton.measure(childWidthSpec,
-                    MeasureSpec.makeMeasureSpec(buttonHeight, MeasureSpec.EXACTLY));
-        } else {
-            mLayoutHorizontal = true;
-            int childHeightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY);
-
-            int pageWidth = Math.round(workspaceWidth * scaleY);
-            mWorkspaceClickTarget.measure(
-                    MeasureSpec.makeMeasureSpec(pageWidth, MeasureSpec.EXACTLY), childHeightSpec);
-
-            int buttonWidth = availableWidth - pageWidth;
-            mWidgetsButton.measure(
-                    MeasureSpec.makeMeasureSpec(buttonWidth, MeasureSpec.EXACTLY), childHeightSpec);
-        }
-        setMeasuredDimension(widthSize, heightSize);
-    }
+    public void draw(Canvas canvas) { }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
-        int x = getPaddingLeft();
-        int y = getPaddingTop();
+        super.onLayout(changed, left, top, right, bottom);
 
-        if (mLayoutHorizontal) {
-            final View first, second;
-            if (Utilities.isRtl(getResources())) {
-                first = mWidgetsButton;
-                second = mWorkspaceClickTarget;
-            } else {
-                first = mWorkspaceClickTarget;
-                second = mWidgetsButton;
-            }
-            int x2 = x + first.getMeasuredWidth();
-            first.layout(x, y,
-                    x2, y + first.getMeasuredHeight());
-            second.layout(x2, y,
-                    x2 + second.getMeasuredWidth(),
-                    y + second.getMeasuredHeight());
-        } else {
-            int y2 = y + mWorkspaceClickTarget.getMeasuredHeight();
-            mWorkspaceClickTarget.layout(x, y,
-                    x + mWorkspaceClickTarget.getMeasuredWidth(), y2);
-            mWidgetsButton.layout(x, y2,
-                    x + mWidgetsButton.getMeasuredWidth(),
-                    y2 + mWidgetsButton.getMeasuredHeight());
+        // Initiate data
+        mLinearInterpolationForPage2 = RecentsView.getScaledDownPageRect(
+                mLauncher.getDeviceProfile(), mLauncher, mTempRect);
+
+        float[] scale = OverviewState.getScaleAndTranslationForPageRect(mLauncher, 0, mTempRect);
+        mTranslateXPage0 = scale[1];
+        mTranslateXPage1 = OverviewState
+                .getScaleAndTranslationForPageRect(mLauncher,
+                        getResources().getDimension(R.dimen.workspace_overview_offset_x),
+                        mTempRect)[1];
+
+        mExtraScrollShift = 0;
+        if (mWorkspace != null && getWidth() > 0) {
+            float workspaceWidth = mWorkspace.getNormalChildWidth() * scale[0];
+            mExtraScrollShift = (workspaceWidth - getWidth()) / 2;
+            setScaleX(workspaceWidth / getWidth());
         }
-
-        mUIDataValid = false;
     }
 
     @Override
     public void onClick(View view) {
-        if (view == mWorkspaceClickTarget) {
-            mLauncher.getStateManager().goToState(NORMAL);
-        } else if (view == mWidgetsButton) {
-            WidgetsFullSheet.show(mLauncher, true);
-        }
-    }
-
-    @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        super.onSizeChanged(w, h, oldw, oldh);
-        mUIDataValid = false;
+        mLauncher.getStateManager().goToState(NORMAL);
     }
 
     public void setup(Launcher launcher) {
@@ -193,41 +104,24 @@
     @Override
     public int onPageScroll(ScrollState scrollState) {
         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.setTranslationX(mEvaluatedFloats[1]);
-            mWorkspace.setTranslationY(mEvaluatedFloats[2]);
-            translateX += mEvaluatedFloats[1] - mScaleAndTranslatePage0[1];
+            float shift = factor * (mTranslateXPage1 - mTranslateXPage0);
+            mWorkspace.setTranslationX(shift + mTranslateXPage0);
+            translateX += shift;
         }
 
         setTranslationX(translateX);
 
-        return SCROLL_TYPE_WORKSPACE;
-    }
-
-    private void initUiData() {
-        if (mUIDataValid && mScaleAndTranslatePage0 != null) {
-            return;
+        // If the workspace card is still the first page, shift all the other pages.
+        if (scrollState.linearInterpolation > mLinearInterpolationForPage2) {
+            scrollState.prevPageExtraWidth = 0;
+        } else if (mLinearInterpolationForPage2 > 0) {
+            scrollState.prevPageExtraWidth = mExtraScrollShift *
+                    (1 - scrollState.linearInterpolation / mLinearInterpolationForPage2);
+        } else {
+            scrollState.prevPageExtraWidth = mExtraScrollShift;
         }
-
-        float overlap = getResources().getDimension(R.dimen.workspace_overview_offset_x);
-
-        RecentsView.getPageRect(mLauncher, mTempRect);
-        mScaleAndTranslatePage0 = OverviewState
-                .getScaleAndTranslationForPageRect(mLauncher, 0, mTempRect);
-        Rect scaledDown = new Rect(mTempRect);
-        Utilities.scaleRectAboutCenter(scaledDown, WORKSPACE_SCALE_ON_SCROLL);
-        mScaleAndTranslatePage1 = OverviewState
-                .getScaleAndTranslationForPageRect(mLauncher, overlap, scaledDown);
-        mUIDataValid = true;
+        return SCROLL_TYPE_WORKSPACE;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index 3ca1865..168c1fe 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Handler;
@@ -33,14 +34,15 @@
 import android.view.ViewTreeObserver.OnPreDrawListener;
 
 import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.states.InternalStateHandler;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.launcher3.util.TraceHelper;
@@ -55,17 +57,14 @@
 public class NavBarSwipeInteractionHandler extends InternalStateHandler {
 
     private static final int STATE_LAUNCHER_READY = 1 << 0;
-    private static final int STATE_RECENTS_DELAY_COMPLETE = 1 << 1;
-    private static final int STATE_LOAD_PLAN_READY = 1 << 2;
-    private static final int STATE_RECENTS_FULLY_VISIBLE = 1 << 3;
     private static final int STATE_ACTIVITY_MULTIPLIER_COMPLETE = 1 << 4;
     private static final int STATE_SCALED_SNAPSHOT_RECENTS = 1 << 5;
     private static final int STATE_SCALED_SNAPSHOT_APP = 1 << 6;
 
-    private static final long RECENTS_VIEW_VISIBILITY_DELAY = 120;
     private static final long RECENTS_VIEW_VISIBILITY_DURATION = 150;
     private static final long MAX_SWIPE_DURATION = 200;
     private static final long MIN_SWIPE_DURATION = 80;
+    private static final int QUICK_SWITCH_SNAP_DURATION = 120;
 
     // Ideal velocity for a smooth transition
     private static final float PIXEL_PER_MS = 2f;
@@ -88,7 +87,7 @@
     // animated to 1, so allow for a smooth transition.
     private final AnimatedFloat mActivityMultiplier = new AnimatedFloat(this::updateFinalShift);
 
-    private final int mRunningTaskId;
+    private final Task mRunningTask;
     private final Context mContext;
 
     private final MultiStateCallback mStateCallback;
@@ -97,29 +96,44 @@
     private SnapshotDragView mDragView;
     private RecentsView mRecentsView;
     private RecentsViewStateController mStateController;
+    private QuickScrubController mQuickScrubController;
     private Hotseat mHotseat;
     private AllAppsScrim mAllAppsScrim;
-    private RecentsTaskLoadPlan mLoadPlan;
 
     private boolean mLauncherReady;
     private boolean mTouchEndHandled;
     private float mCurrentDisplacement;
+    private @TouchInteractionService.InteractionType int mInteractionType;
+    private boolean mStartedQuickScrubFromHome;
 
     private Bitmap mTaskSnapshot;
 
-    NavBarSwipeInteractionHandler(RunningTaskInfo runningTaskInfo, Context context) {
-        mRunningTaskId = runningTaskInfo.id;
+    NavBarSwipeInteractionHandler(RunningTaskInfo runningTaskInfo, Context context,
+            @TouchInteractionService.InteractionType int interactionType) {
+        // TODO: We need a better way for this
+        TaskKey taskKey = new TaskKey(runningTaskInfo.id, 0, null, UserHandle.myUserId(), 0);
+        mRunningTask = new Task(taskKey, null, null, "", "", Color.BLACK, Color.BLACK,
+                true, false, false, false, null, 0, null, false);
+
         mContext = context;
+        mInteractionType = interactionType;
         WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
 
+        DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext);
+        // TODO: If in multi window mode, dp = dp.getMultiWindowProfile()
+        dp = dp.copy(mContext);
+        // TODO: Use different insets for multi-window mode
+        dp.updateInsets(mStableInsets);
+        RecentsView.getPageRect(dp, mContext, mTargetRect);
+        mSourceRect.set(0, 0, dp.widthPx - mStableInsets.left - mStableInsets.right,
+                dp.heightPx - mStableInsets.top - mStableInsets.bottom);
+
         // Build the state callback
         mStateCallback = new MultiStateCallback();
         mStateCallback.addCallback(STATE_LAUNCHER_READY, this::onLauncherReady);
-        mStateCallback.addCallback(STATE_LOAD_PLAN_READY | STATE_RECENTS_DELAY_COMPLETE,
-                this::setTaskPlanToUi);
         mStateCallback.addCallback(STATE_SCALED_SNAPSHOT_APP, this::resumeLastTask);
-        mStateCallback.addCallback(STATE_RECENTS_FULLY_VISIBLE | STATE_SCALED_SNAPSHOT_RECENTS
-                | STATE_ACTIVITY_MULTIPLIER_COMPLETE,
+        mStateCallback.addCallback(
+                STATE_SCALED_SNAPSHOT_RECENTS | STATE_ACTIVITY_MULTIPLIER_COMPLETE,
                 this::onAnimationToLauncherComplete);
         mStateCallback.addCallback(STATE_LAUNCHER_READY | STATE_SCALED_SNAPSHOT_APP,
                 this::cleanupLauncher);
@@ -129,10 +143,6 @@
         mLauncherReady = true;
         executeFrameUpdate();
 
-        // Wait for some time before loading recents so that the first frame is fast
-        new Handler().postDelayed(() -> mStateCallback.setState(STATE_RECENTS_DELAY_COMPLETE),
-                RECENTS_VIEW_VISIBILITY_DELAY);
-
         long duration = Math.min(MAX_SWIPE_DURATION,
                 Math.max((long) (-mCurrentDisplacement / PIXEL_PER_MS), MIN_SWIPE_DURATION));
         if (mCurrentShift.getCurrentAnimation() != null) {
@@ -173,26 +183,39 @@
 
     @Override
     protected void init(Launcher launcher, boolean alreadyOnHome) {
-        AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
-        launcher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
-
         mLauncher = launcher;
+        mRecentsView = launcher.getOverviewPanel();
+        mRecentsView.showTask(mRunningTask);
+        mStateController = mRecentsView.getStateController();
+        mHotseat = mLauncher.getHotseat();
+        mAllAppsScrim = mLauncher.findViewById(R.id.all_apps_scrim);
+
+        AbstractFloatingView.closeAllOpenViews(mLauncher, alreadyOnHome);
+        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
+
         mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
         mLauncher.getDragLayer().addView(mDragView);
         mDragView.setPivotX(0);
         mDragView.setPivotY(0);
-        mRecentsView = mLauncher.getOverviewPanel();
-        mStateController = mRecentsView.getStateController();
-        mHotseat = mLauncher.getHotseat();
-        mAllAppsScrim = mLauncher.findViewById(R.id.all_apps_scrim);
+
+        boolean interactionIsQuick
+                = mInteractionType == TouchInteractionService.INTERACTION_QUICK_SCRUB
+                || mInteractionType == TouchInteractionService.INTERACTION_QUICK_SWITCH;
+        mStartedQuickScrubFromHome = alreadyOnHome && interactionIsQuick;
+        if (interactionIsQuick) {
+            mQuickScrubController = mRecentsView.getQuickScrubController();
+            mQuickScrubController.onQuickScrubStart(mStartedQuickScrubFromHome);
+            animateToProgress(1f, MAX_SWIPE_DURATION);
+            if (mStartedQuickScrubFromHome) {
+                mDragView.setVisibility(View.INVISIBLE);
+            }
+        }
 
         // Optimization
         if (!mLauncher.getDeviceProfile().isVerticalBarLayout()) {
             // All-apps search box is visible in vertical bar layout.
             mLauncher.getAppsView().setVisibility(View.GONE);
         }
-        mStateController.setTransitionProgress(1);
-        mStateController.setVisibility(false);
         TraceHelper.partitionSection("TouchInt", "Launcher on new intent");
     }
 
@@ -214,17 +237,10 @@
 
     @UiThread
     private void updateFinalShift() {
-        if (!mLauncherReady) {
+        if (!mLauncherReady || mStartedQuickScrubFromHome) {
             return;
         }
 
-        if (mTargetRect.isEmpty()) {
-            RecentsView.getPageRect(mLauncher, mTargetRect);
-            DragLayer dl = mLauncher.getDragLayer();
-            mSourceRect.set(0, 0, dl.getWidth() - mStableInsets.left - mStableInsets.right,
-                    dl.getHeight() - mStableInsets.top - mStableInsets.bottom);
-        }
-
         float shift = mCurrentShift.value * mActivityMultiplier.value;
         int hotseatSize = getHotseatSize();
 
@@ -251,26 +267,6 @@
     }
 
     @UiThread
-    public void setRecentsTaskLoadPlan(RecentsTaskLoadPlan loadPlan) {
-        mLoadPlan = loadPlan;
-        mStateCallback.setState(STATE_LOAD_PLAN_READY);
-    }
-
-    private void setTaskPlanToUi() {
-        mRecentsView.update(mLoadPlan);
-        mRecentsView.initToPage(mRecentsView.getFirstTaskIndex());
-        ObjectAnimator anim = mStateController.animateVisibility(true /* isVisible */)
-                .setDuration(RECENTS_VIEW_VISIBILITY_DURATION);
-        anim.addListener(new AnimationSuccessListener() {
-            @Override
-            public void onAnimationSuccess(Animator animator) {
-                mStateCallback.setState(STATE_RECENTS_FULLY_VISIBLE);
-            }
-        });
-        anim.start();
-    }
-
-    @UiThread
     public void endTouch(float endVelocity) {
         if (mTouchEndHandled) {
             return;
@@ -298,7 +294,12 @@
             }
         }
 
-        ObjectAnimator anim = mCurrentShift.animateToValue(endShift).setDuration(duration);
+        animateToProgress(endShift, duration);
+    }
+
+    /** Animates to the given progress, where 0 is the current app and 1 is overview. */
+    private void animateToProgress(float progress, long duration) {
+        ObjectAnimator anim = mCurrentShift.animateToValue(progress).setDuration(duration);
         anim.setInterpolator(Interpolators.SCROLL);
         anim.addListener(new AnimationSuccessListener() {
             @Override
@@ -312,19 +313,16 @@
 
     @UiThread
     private void resumeLastTask() {
-        TaskKey key = null;
-        if (mLoadPlan != null) {
-            Task task = mLoadPlan.getTaskStack().findTaskWithId(mRunningTaskId);
+        // TODO: We need a better way for this
+        TaskKey key = mRunningTask.key;
+        RecentsTaskLoadPlan loadPlan = RecentsModel.getInstance(mContext).getLastLoadPlan();
+        if (loadPlan != null) {
+            Task task = loadPlan.getTaskStack().findTaskWithId(key.id);
             if (task != null) {
                 key = task.key;
             }
         }
 
-        if (key == null) {
-            // TODO: We need a better way for this
-            key = new TaskKey(mRunningTaskId, 0, null, UserHandle.myUserId(), 0);
-        }
-
         ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
         ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(key, opts, null, null);
     }
@@ -342,5 +340,33 @@
         if (currentRecentsPage instanceof TaskView) {
             ((TaskView) currentRecentsPage).animateIconToScale(1f);
         }
+        if (mInteractionType == TouchInteractionService.INTERACTION_QUICK_SWITCH) {
+            for (int i = mRecentsView.getFirstTaskIndex(); i < mRecentsView.getPageCount(); i++) {
+                TaskView taskView = (TaskView) mRecentsView.getPageAt(i);
+                // TODO: Match the keys directly
+                if (taskView.getTask().key.id != mRunningTask.key.id) {
+                    mRecentsView.snapToPage(i, QUICK_SWITCH_SNAP_DURATION);
+                    taskView.postDelayed(() -> {taskView.launchTask(true);},
+                            QUICK_SWITCH_SNAP_DURATION);
+                    break;
+                }
+            }
+        } else if (mInteractionType == TouchInteractionService.INTERACTION_QUICK_SCRUB) {
+            if (mQuickScrubController != null) {
+                mQuickScrubController.snapToPageForCurrentQuickScrubSection();
+            }
+        }
+    }
+
+    public void onQuickScrubEnd() {
+        if (mQuickScrubController != null) {
+            mQuickScrubController.onQuickScrubEnd();
+        }
+    }
+
+    public void onQuickScrubProgress(float progress) {
+        if (mQuickScrubController != null) {
+            mQuickScrubController.onQuickScrubProgress(progress);
+        }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
new file mode 100644
index 0000000..f4c2055
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/QuickScrubController.java
@@ -0,0 +1,111 @@
+/*
+ * 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.view.HapticFeedbackConstants;
+
+import com.android.launcher3.Alarm;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.OnAlarmListener;
+
+/**
+ * Responds to quick scrub callbacks to page through and launch recent tasks.
+ *
+ * The behavior is to evenly divide the progress into sections, each of which scrolls one page.
+ * The first and last section set an alarm to auto-advance backwards or forwards, respectively.
+ */
+public class QuickScrubController implements OnAlarmListener {
+
+    private static final int NUM_QUICK_SCRUB_SECTIONS = 5;
+    private static final long AUTO_ADVANCE_DELAY = 500;
+    private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60;
+
+    private Launcher mLauncher;
+    private Alarm mAutoAdvanceAlarm;
+    private RecentsView mRecentsView;
+
+    private int mQuickScrubSection;
+    private int mStartPage;
+
+    public QuickScrubController(Launcher launcher) {
+        mLauncher = launcher;
+        mAutoAdvanceAlarm = new Alarm();
+        mAutoAdvanceAlarm.setOnAlarmListener(this);
+    }
+
+    public void onQuickScrubStart(boolean startingFromHome) {
+        mRecentsView = mLauncher.getOverviewPanel();
+        mStartPage = startingFromHome ? 0 : mRecentsView.getFirstTaskIndex();
+        mQuickScrubSection = 0;
+    }
+
+    public void onQuickScrubEnd() {
+        mAutoAdvanceAlarm.cancelAlarm();
+        if (mRecentsView != null) {
+            int page = mRecentsView.getNextPage();
+            // Settle on the page then launch it.
+            int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
+                    * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
+            mRecentsView.snapToPage(page, snapDuration);
+            mRecentsView.postDelayed(() -> {
+                if (page < mRecentsView.getFirstTaskIndex()) {
+                    mRecentsView.getPageAt(page).performClick();
+                } else {
+                    ((TaskView) mRecentsView.getPageAt(page)).launchTask(true);
+                }
+            }, snapDuration);
+        }
+    }
+
+    public void onQuickScrubProgress(float progress) {
+        int quickScrubSection = Math.round(progress * NUM_QUICK_SCRUB_SECTIONS);
+        if (quickScrubSection != mQuickScrubSection) {
+            int pageToGoTo = mRecentsView.getNextPage() + quickScrubSection - mQuickScrubSection;
+            goToPageWithHaptic(pageToGoTo);
+            if (quickScrubSection == NUM_QUICK_SCRUB_SECTIONS || quickScrubSection == 0) {
+                mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY);
+            } else {
+                mAutoAdvanceAlarm.cancelAlarm();
+            }
+            mQuickScrubSection = quickScrubSection;
+        }
+    }
+
+    public void snapToPageForCurrentQuickScrubSection() {
+        goToPageWithHaptic(mRecentsView.getCurrentPage() + mQuickScrubSection);
+    }
+
+    private void goToPageWithHaptic(int pageToGoTo) {
+        if (pageToGoTo != mRecentsView.getNextPage()) {
+            mRecentsView.snapToPage(pageToGoTo);
+            mRecentsView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP,
+                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+        }
+    }
+
+    @Override
+    public void onAlarm(Alarm alarm) {
+        int currPage = mRecentsView.getNextPage();
+        if (mQuickScrubSection == NUM_QUICK_SCRUB_SECTIONS
+                && currPage < mRecentsView.getPageCount() - 1) {
+            goToPageWithHaptic(currPage + 1);
+        } else if (mQuickScrubSection == 0 && currPage > mStartPage) {
+            goToPageWithHaptic(currPage - 1);
+        }
+        mAutoAdvanceAlarm.setAlarm(AUTO_ADVANCE_DELAY);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java
index 112f156..22658b2 100644
--- a/quickstep/src/com/android/quickstep/RecentsModel.java
+++ b/quickstep/src/com/android/quickstep/RecentsModel.java
@@ -15,8 +15,10 @@
  */
 package com.android.quickstep;
 
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
+import android.os.Build;
 import android.os.Looper;
 import android.os.UserHandle;
 
@@ -25,7 +27,9 @@
 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.TaskStackChangeListener;
 
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
@@ -33,7 +37,8 @@
 /**
  * Singleton class to load and manage recents model.
  */
-public class RecentsModel {
+@TargetApi(Build.VERSION_CODES.O)
+public class RecentsModel extends TaskStackChangeListener {
 
     // We do not need any synchronization for this variable as its only written on UI thread.
     private static RecentsModel INSTANCE;
@@ -59,6 +64,9 @@
     private final MainThreadExecutor mMainThreadExecutor;
 
     private RecentsTaskLoadPlan mLastLoadPlan;
+    private int mLastLoadPlanId;
+    private int mTaskChangeId;
+
     private RecentsModel(Context context) {
         mContext = context;
 
@@ -69,6 +77,10 @@
         mRecentsTaskLoader.startLoader(mContext);
 
         mMainThreadExecutor = new MainThreadExecutor();
+        ActivityManagerWrapper.getInstance().registerTaskStackListener(this);
+
+        mTaskChangeId = 1;
+        loadTasks(-1, null);
     }
 
     public RecentsTaskLoader getRecentsTaskLoader() {
@@ -80,8 +92,20 @@
      * @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.
+     * @return the request id associated with this call.
      */
-    public void loadTasks(int taskId, Consumer<RecentsTaskLoadPlan> callback) {
+    public int loadTasks(int taskId, Consumer<RecentsTaskLoadPlan> callback) {
+        final int requestId = mTaskChangeId;
+
+        // Fail fast if nothing has changed.
+        if (mLastLoadPlanId == mTaskChangeId) {
+            if (callback != null) {
+                final RecentsTaskLoadPlan plan = mLastLoadPlan;
+                mMainThreadExecutor.execute(() -> callback.accept(plan));
+            }
+            return requestId;
+        }
+
         BackgroundExecutor.get().submit(() -> {
             // Preload the plan
             RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(mContext);
@@ -91,11 +115,23 @@
             // Set the load plan on UI thread
             mMainThreadExecutor.execute(() -> {
                 mLastLoadPlan = loadPlan;
+                mLastLoadPlanId = requestId;
+
                 if (callback != null) {
                     callback.accept(loadPlan);
                 }
             });
         });
+        return requestId;
+    }
+
+    @Override
+    public void onTaskStackChanged() {
+        mTaskChangeId++;
+    }
+
+    public boolean isLoadPlanValid(int resultId) {
+        return mTaskChangeId == resultId;
     }
 
     public RecentsTaskLoadPlan getLastLoadPlan() {
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index e7e794f..605c83c 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -25,10 +25,10 @@
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherState;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.uioverrides.OverviewState;
 import com.android.launcher3.uioverrides.RecentsViewStateController;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
 import com.android.systemui.shared.recents.model.RecentsTaskLoader;
@@ -41,29 +41,38 @@
 
 import java.util.ArrayList;
 
-import static com.android.launcher3.LauncherState.OVERVIEW;
+import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.quickstep.TaskView.CURVE_FACTOR;
+import static com.android.quickstep.TaskView.CURVE_INTERPOLATOR;
 
 /**
  * A list of recent tasks.
  */
 public class RecentsView extends PagedView {
 
+    private static final Rect sTempStableInsets = new Rect();
+
     public static final int SCROLL_TYPE_NONE = 0;
     public static final int SCROLL_TYPE_TASK = 1;
     public static final int SCROLL_TYPE_WORKSPACE = 2;
 
+    private final Launcher mLauncher;
+    private QuickScrubController mQuickScrubController;
     private final ScrollState mScrollState = new ScrollState();
     private boolean mOverviewStateEnabled;
     private boolean mTaskStackListenerRegistered;
     private LayoutTransition mLayoutTransition;
 
+    /**
+     * TODO: Call reloadIdNeeded in onTaskStackChanged.
+     */
     private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
         @Override
         public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
             for (int i = mFirstTaskIndex; i < getChildCount(); i++) {
                 final TaskView taskView = (TaskView) getChildAt(i);
                 if (taskView.getTask().key.id == taskId) {
-                    taskView.getThumbnail().setThumbnail(snapshot);
+                    taskView.getThumbnail().setThumbnail(taskView.getTask(), snapshot);
                     return;
                 }
             }
@@ -73,6 +82,11 @@
     private RecentsViewStateController mStateController;
     private int mFirstTaskIndex;
 
+    private final RecentsModel mModel;
+    private int mLoadPlanId = -1;
+
+    private Task mFirstTask;
+
     public RecentsView(Context context) {
         this(context, null);
     }
@@ -88,6 +102,10 @@
         setClipChildren(true);
         setupLayoutTransition();
 
+        mLauncher = Launcher.getLauncher(context);
+        mQuickScrubController = new QuickScrubController(mLauncher);
+        mModel = RecentsModel.getInstance(context);
+
         mScrollState.isRtl = mIsRtl;
     }
 
@@ -106,9 +124,9 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        Rect padding = getPadding(Launcher.getLauncher(getContext()));
+        Rect padding =
+                getPadding(Launcher.getLauncher(getContext()).getDeviceProfile(), getContext());
         setPadding(padding.left, padding.top, padding.right, padding.bottom);
-
         mFirstTaskIndex = getPageCount();
     }
 
@@ -147,9 +165,8 @@
         updateTaskStackListenerState();
     }
 
-    public void update(RecentsTaskLoadPlan loadPlan) {
-        final RecentsTaskLoader loader = RecentsModel.getInstance(getContext())
-                .getRecentsTaskLoader();
+    private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
+        final RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
         if (stack == null) {
             removeAllViews();
@@ -159,10 +176,17 @@
         // 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();
+        final ArrayList<Task> tasks = new ArrayList<>(stack.getTasks());
         setLayoutTransition(null);
-        int requiredChildCount = tasks.size() + mFirstTaskIndex;
 
+        if (mFirstTask != null) {
+            // TODO: Handle this case here once we have a valid implementation for mFirstTask
+            if (tasks.isEmpty() || !keysEquals(tasks.get(tasks.size() - 1), mFirstTask)) {
+                // tasks.add(mFirstTask);
+            }
+        }
+
+        final int requiredChildCount = tasks.size() + mFirstTaskIndex;
         for (int i = getChildCount(); i < requiredChildCount; i++) {
             final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false);
             addView(taskView);
@@ -183,52 +207,36 @@
         }
     }
 
-    public void initToPage(int pageNo) {
-        setCurrentPage(pageNo);
-        if (getPageAt(mCurrentPage) instanceof TaskView) {
-            ((TaskView) getPageAt(mCurrentPage)).setIconScale(0);
-        }
-    }
-
-    public void launchTaskWithId(int taskId) {
-        for (int i = mFirstTaskIndex; 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);
+                ActivityManagerWrapper.getInstance()
+                        .registerTaskStackListener(mTaskStackListener);
+                reloadIfNeeded();
             } else {
-                ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+                ActivityManagerWrapper.getInstance()
+                        .unregisterTaskStackListener(mTaskStackListener);
             }
             mTaskStackListenerRegistered = registerStackListener;
         }
     }
 
-    private static Rect getPadding(Launcher launcher) {
-        DeviceProfile profile = launcher.getDeviceProfile();
-        Rect stableInsets = new Rect();
-        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
+    private static Rect getPadding(DeviceProfile profile, Context context) {
+        WindowManagerWrapper.getInstance().getStableInsets(sTempStableInsets);
         Rect padding = new Rect(profile.workspacePadding);
 
-        float taskWidth = profile.widthPx - stableInsets.left - stableInsets.right;
-        float taskHeight = profile.heightPx - stableInsets.top - stableInsets.bottom;
+        float taskWidth = profile.widthPx - sTempStableInsets.left - sTempStableInsets.right;
+        float taskHeight = profile.heightPx - sTempStableInsets.top - sTempStableInsets.bottom;
 
         float overviewHeight, overviewWidth;
         if (profile.isVerticalBarLayout()) {
             // Use the same padding on both sides for symmetry.
             float availableWidth = taskWidth - 2 * Math.max(padding.left, padding.right);
             float availableHeight = profile.availableHeightPx - padding.top - padding.bottom
-                    - stableInsets.top
-                    - profile.heightPx * (1 - OVERVIEW.getVerticalProgress(launcher));
+                    - sTempStableInsets.top
+                    - profile.heightPx * (1 - OverviewState.getVerticalProgress(profile, context));
 
             float scaledRatio = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
             overviewHeight = taskHeight * scaledRatio;
@@ -236,26 +244,52 @@
 
         } else {
             overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
-                    - stableInsets.top;
+                    - sTempStableInsets.top;
             overviewWidth = taskWidth * overviewHeight / taskHeight;
         }
 
-        padding.bottom = profile.availableHeightPx - padding.top - stableInsets.top
+        padding.bottom = profile.availableHeightPx - padding.top - sTempStableInsets.top
                 - Math.round(overviewHeight);
         padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
         return padding;
     }
 
-    public static void getPageRect(Launcher launcher, Rect outRect) {
-        DragLayer dl = launcher.getDragLayer();
-        Rect targetPadding = getPadding(launcher);
-        Rect insets = dl.getInsets();
+    /**
+     * Sets the {@param outRect} to match the position of the first tile such that it is scaled
+     * down to match the 2nd taskView.
+     * @return returns the factor which determines the scaling factor for the second task.
+     */
+    public static float getScaledDownPageRect(DeviceProfile dp, Context context, Rect outRect) {
+        getPageRect(dp, context, outRect);
+
+        int pageSpacing = context.getResources()
+                .getDimensionPixelSize(R.dimen.recents_page_spacing);
+        float halfScreenWidth = dp.widthPx * 0.5f;
+        float halfPageWidth = outRect.width() * 0.5f;
+        float pageCenter = outRect.right + pageSpacing + halfPageWidth;
+        float distanceFromCenter = Math.abs(halfScreenWidth - pageCenter);
+        float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
+        float linearInterpolation = Math.min(1, distanceFromCenter / distanceToReachEdge);
+
+        float scale = 1 - CURVE_INTERPOLATOR.getInterpolation(linearInterpolation) * CURVE_FACTOR;
+
+        int topMargin = context.getResources()
+                .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+        outRect.top -= topMargin;
+        Utilities.scaleRectAboutCenter(outRect, scale);
+        outRect.top += (int) (scale * topMargin);
+        return linearInterpolation;
+    }
+
+    public static void getPageRect(DeviceProfile grid, Context context, Rect outRect) {
+        Rect targetPadding = getPadding(grid, context);
+        Rect insets = grid.getInsets();
         outRect.set(
                 targetPadding.left + insets.left,
                 targetPadding.top + insets.top,
-                dl.getWidth() - targetPadding.right - insets.right,
-                dl.getHeight() - targetPadding.bottom - insets.bottom);
-        outRect.top += launcher.getResources()
+                grid.widthPx - targetPadding.right - insets.right,
+                grid.heightPx - targetPadding.bottom - insets.bottom);
+        outRect.top += context.getResources()
                 .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
     }
 
@@ -293,11 +327,78 @@
     public void onTaskDismissed(TaskView taskView) {
         ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
         removeView(taskView);
-        if (getChildCount() == mFirstTaskIndex) {
-            Launcher.getLauncher(getContext()).getStateManager().goToState(LauncherState.NORMAL);
+        if (getTaskCount() == 0) {
+            mLauncher.getStateManager().goToState(NORMAL);
         }
     }
 
+    public void reset() {
+        mFirstTask = null;
+        setCurrentPage(0);
+    }
+
+    public int getTaskCount() {
+        return getChildCount() - mFirstTaskIndex;
+    }
+
+    /**
+     * Reloads the view if anything in recents changed.
+     */
+    public void reloadIfNeeded() {
+        if (!mModel.isLoadPlanValid(mLoadPlanId)) {
+            int taskId = -1;
+            if (mFirstTask != null) {
+                taskId = mFirstTask.key.id;
+            }
+            mLoadPlanId = mModel.loadTasks(taskId, this::applyLoadPlan);
+        }
+    }
+
+    /**
+     * Ensures that the first task in the view represents {@param task} and reloads the view
+     * if needed. This allows the swipe-up gesture to assume that the first tile always
+     * corresponds to the correct task.
+     * All subsequent calls to reload will keep the task as the first item until {@link #reset()}
+     * is called.
+     * Also scrolls the view to this task
+     */
+    public void showTask(Task task) {
+        boolean needsReload = false;
+        boolean inflateFirstChild = true;
+        if (getTaskCount() > 0) {
+            TaskView tv = (TaskView) getChildAt(mFirstTaskIndex);
+            inflateFirstChild = !keysEquals(tv.getTask(), task);
+        }
+        if (inflateFirstChild) {
+            needsReload = true;
+            setLayoutTransition(null);
+            // Add an empty view for now
+            final TaskView taskView = (TaskView) LayoutInflater.from(getContext())
+                    .inflate(R.layout.task, this, false);
+            addView(taskView, mFirstTaskIndex);
+            taskView.bind(task);
+            setLayoutTransition(mLayoutTransition);
+        }
+        if (!needsReload) {
+            needsReload = !mModel.isLoadPlanValid(mLoadPlanId);
+        }
+        if (needsReload) {
+            mLoadPlanId = mModel.loadTasks(task.key.id, this::applyLoadPlan);
+        }
+        mFirstTask = task;
+        setCurrentPage(mFirstTaskIndex);
+        ((TaskView) getPageAt(mCurrentPage)).setIconScale(0);
+    }
+
+    private static boolean keysEquals(Task t1, Task t2) {
+        // TODO: Match the keys directly
+        return t1.key.id == t2.key.id;
+    }
+
+    public QuickScrubController getQuickScrubController() {
+        return mQuickScrubController;
+    }
+
     public interface PageCallbacks {
 
         /**
@@ -319,5 +420,7 @@
         public int halfPageWidth;
         public float distanceFromScreenCenter;
         public float linearInterpolation;
+
+        public float prevPageExtraWidth;
     }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 473681f..87dec67 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -22,35 +22,43 @@
 import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
+import android.graphics.ComposeShader;
 import android.graphics.LightingColorFilter;
+import android.graphics.LinearGradient;
 import android.graphics.Matrix;
 import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
 import android.graphics.Rect;
 import android.graphics.Shader;
 import android.util.AttributeSet;
-import android.widget.FrameLayout;
+import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.ThumbnailData;
 
 /**
  * A task in the Recents view.
  */
-public class TaskThumbnailView extends FrameLayout {
+public class TaskThumbnailView extends View {
+
+    private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256];
+
+    private final float mCornerRadius;
+    private final float mFadeLength;
+
+    private final Paint mPaint = new Paint();
+
+    private final Matrix mMatrix = new Matrix();
+    private final Rect mThumbnailRect = new Rect();
 
     private ThumbnailData mThumbnailData;
-
-    private Rect mThumbnailRect = new Rect();
-    private float mThumbnailScale;
-
-    private Matrix mMatrix = new Matrix();
-    private Paint mDrawPaint = new Paint();
-    protected Paint mBgFillPaint = new Paint();
     protected BitmapShader mBitmapShader;
 
+    private float mThumbnailScale;
     private float mDimAlpha = 1f;
-    private LightingColorFilter mLightingColorFilter = new LightingColorFilter(Color.WHITE, 0);
 
     public TaskThumbnailView(Context context) {
         this(context, null);
@@ -62,32 +70,34 @@
 
     public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setWillNotDraw(false);
-        setClipToOutline(true);
+        mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius);
+        mFadeLength = getResources().getDimension(R.dimen.task_fade_length);
     }
 
     /**
      * Updates this thumbnail.
      */
-    public void setThumbnail(ThumbnailData thumbnailData) {
+    public void setThumbnail(Task task, ThumbnailData thumbnailData) {
+        mPaint.setColor(task == null ? Color.BLACK : task.colorBackground | 0xFF000000);
+
         if (thumbnailData != null && thumbnailData.thumbnail != null) {
             Bitmap bm = thumbnailData.thumbnail;
             bm.prepareToDraw();
             mThumbnailScale = thumbnailData.scale;
             mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
-            mDrawPaint.setShader(mBitmapShader);
+            mPaint.setShader(mBitmapShader);
             mThumbnailRect.set(0, 0,
                     bm.getWidth() - thumbnailData.insets.left - thumbnailData.insets.right,
                     bm.getHeight() - thumbnailData.insets.top - thumbnailData.insets.bottom);
             mThumbnailData = thumbnailData;
             updateThumbnailMatrix();
-            updateThumbnailPaintFilter();
         } else {
             mBitmapShader = null;
-            mDrawPaint.setShader(null);
-            mThumbnailRect.setEmpty();
             mThumbnailData = null;
+            mPaint.setShader(null);
+            mThumbnailRect.setEmpty();
         }
+        updateThumbnailPaintFilter();
     }
 
     /**
@@ -100,43 +110,17 @@
 
     @Override
     protected void onDraw(Canvas canvas) {
-        int viewWidth = getMeasuredWidth();
-        int viewHeight = getMeasuredHeight();
-        int thumbnailWidth = Math.min(viewWidth,
-                (int) (mThumbnailRect.width() * mThumbnailScale));
-        int thumbnailHeight = Math.min(viewHeight,
-                (int) (mThumbnailRect.height() * mThumbnailScale));
-
-        if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 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), 0, viewWidth, viewHeight,
-                        mBgFillPaint);
-            }
-            if (thumbnailHeight < viewHeight) {
-                // Landscape thumbnail on a portrait task view
-                canvas.drawRect(0, Math.max(0, thumbnailHeight), viewWidth, viewHeight,
-                        mBgFillPaint);
-            }
-
-            // Draw the thumbnail
-            canvas.drawRect(0, 0, thumbnailWidth, thumbnailHeight, mDrawPaint);
-        } else {
-            canvas.drawRect(0, 0, viewWidth, viewHeight, mBgFillPaint);
-        }
+        canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
+                mCornerRadius, mCornerRadius, mPaint);
     }
 
     private void updateThumbnailPaintFilter() {
         int mul = (int) (mDimAlpha * 255);
         if (mBitmapShader != null) {
-            mLightingColorFilter = new LightingColorFilter(Color.argb(255, mul, mul, mul), 0);
-            mDrawPaint.setColorFilter(mLightingColorFilter);
-            mDrawPaint.setColor(0xFFffffff);
-            mBgFillPaint.setColorFilter(mLightingColorFilter);
+            mPaint.setColorFilter(getLightingColorFilter(mul));
         } else {
-            mDrawPaint.setColorFilter(null);
-            mDrawPaint.setColor(Color.argb(255, mul, mul, mul));
+            mPaint.setColorFilter(null);
+            mPaint.setColor(Color.argb(255, mul, mul, mul));
         }
         invalidate();
     }
@@ -170,6 +154,26 @@
             mMatrix.setTranslate(-mThumbnailData.insets.left, -mThumbnailData.insets.top);
             mMatrix.postScale(mThumbnailScale, mThumbnailScale);
             mBitmapShader.setLocalMatrix(mMatrix);
+
+            float bitmapHeight = Math.max(mThumbnailRect.height() * mThumbnailScale, 0);
+            Shader shader = mBitmapShader;
+            if (bitmapHeight < getMeasuredHeight()) {
+                int color = mPaint.getColor();
+                LinearGradient fade = new LinearGradient(
+                        0, bitmapHeight - mFadeLength, 0, bitmapHeight,
+                        color & 0x00FFFFFF, color, Shader.TileMode.CLAMP);
+                shader = new ComposeShader(fade, shader, Mode.DST_OVER);
+            }
+
+            float bitmapWidth = Math.max(mThumbnailRect.width() * mThumbnailScale, 0);
+            if (bitmapWidth < getMeasuredWidth()) {
+                int color = mPaint.getColor();
+                LinearGradient fade = new LinearGradient(
+                        bitmapWidth - mFadeLength, 0, bitmapWidth, 0,
+                        color & 0x00FFFFFF, color, Shader.TileMode.CLAMP);
+                shader = new ComposeShader(fade, shader, Mode.DST_OVER);
+            }
+            mPaint.setShader(shader);
         }
         invalidate();
     }
@@ -179,4 +183,17 @@
         super.onSizeChanged(w, h, oldw, oldh);
         updateThumbnailMatrix();
     }
+
+    private static LightingColorFilter getLightingColorFilter(int dimColor) {
+        if (dimColor < 0) {
+            dimColor = 0;
+        } else if (dimColor > 255) {
+            dimColor = 255;
+        }
+        if (sDimFilterCache[dimColor] == null) {
+            sDimFilterCache[dimColor] =
+                    new LightingColorFilter(Color.argb(255, dimColor, dimColor, dimColor), 0);
+        }
+        return sDimFilterCache[dimColor];
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index 94d85ee..0e999f8 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -19,26 +19,21 @@
 import static com.android.quickstep.RecentsView.SCROLL_TYPE_TASK;
 import static com.android.quickstep.RecentsView.SCROLL_TYPE_WORKSPACE;
 
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
 import android.app.ActivityOptions;
 import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Outline;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Property;
-import android.view.MotionEvent;
 import android.view.View;
-import android.view.animation.Interpolator;
+import android.view.ViewOutlineProvider;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.Interpolators;
-import com.android.launcher3.touch.SwipeDetector;
 import com.android.quickstep.RecentsView.PageCallbacks;
 import com.android.quickstep.RecentsView.ScrollState;
 import com.android.systemui.shared.recents.model.Task;
@@ -55,13 +50,12 @@
 /**
  * A task in the Recents view.
  */
-public class TaskView extends FrameLayout implements TaskCallbacks, SwipeDetector.Listener,
-        PageCallbacks {
+public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {
 
     /** Designates how "curvy" the carousel is from 0 to 1, where 0 is a straight line. */
-    private static final float CURVE_FACTOR = 0.25f;
+    public static final float CURVE_FACTOR = 0.25f;
     /** A circular curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
-    private static final TimeInterpolator CURVE_INTERPOLATOR
+    public static final TimeInterpolator CURVE_INTERPOLATOR
             = x -> (float) (1 - Math.sqrt(1 - Math.pow(x, 2)));
 
     /**
@@ -70,30 +64,8 @@
      */
     private static final float MAX_PAGE_SCRIM_ALPHA = 0.8f;
 
-    private static final int SWIPE_DIRECTIONS = SwipeDetector.DIRECTION_POSITIVE;
-
-    /**
-     * The task will appear fully dismissed when the distance swiped
-     * reaches this percentage of the card height.
-     */
-    private static final float SWIPE_DISTANCE_HEIGHT_PERCENTAGE = 0.38f;
-
     private static final long SCALE_ICON_DURATION = 120;
 
-    private static final Property<TaskView, Float> PROPERTY_SWIPE_PROGRESS =
-            new Property<TaskView, Float>(Float.class, "swipe_progress") {
-
-                @Override
-                public Float get(TaskView taskView) {
-                    return taskView.mSwipeProgress;
-                }
-
-                @Override
-                public void set(TaskView taskView, Float progress) {
-                    taskView.setSwipeProgress(progress);
-                }
-            };
-
     private static final Property<TaskView, Float> SCALE_ICON_PROPERTY =
             new Property<TaskView, Float>(Float.TYPE, "scale_icon") {
                 @Override
@@ -110,11 +82,6 @@
     private Task mTask;
     private TaskThumbnailView mSnapshotView;
     private ImageView mIconView;
-    private SwipeDetector mSwipeDetector;
-    private float mSwipeDistance;
-    private float mSwipeProgress;
-    private Interpolator mAlphaInterpolator;
-    private Interpolator mSwipeAnimInterpolator;
     private float mIconScale = 1f;
 
     public TaskView(Context context) {
@@ -127,14 +94,8 @@
 
     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setOnClickListener((view) -> {
-            launchTask(true /* animate */);
-        });
-
-        mSwipeDetector = new SwipeDetector(getContext(), this, SwipeDetector.VERTICAL);
-        mSwipeDetector.setDetectableScrollConditions(SWIPE_DIRECTIONS, false);
-        mAlphaInterpolator = Interpolators.ACCEL_1_5;
-        mSwipeAnimInterpolator = Interpolators.SCROLL_CUBIC;
+        setOnClickListener((view) -> launchTask(true /* animate */));
+        setOutlineProvider(new TaskOutlineProvider(getResources()));
     }
 
     @Override
@@ -144,15 +105,6 @@
         mIconView = findViewById(R.id.icon);
     }
 
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
-        View p = (View) getParent();
-        mSwipeDistance = (getMeasuredHeight() - p.getPaddingTop() - p.getPaddingBottom())
-                * SWIPE_DISTANCE_HEIGHT_PERCENTAGE;
-    }
-
     /**
      * Updates this task view to the given {@param task}.
      */
@@ -206,14 +158,14 @@
 
     @Override
     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
-        mSnapshotView.setThumbnail(thumbnailData);
+        mSnapshotView.setThumbnail(task, thumbnailData);
         mIconView.setImageDrawable(task.icon);
         mIconView.setOnLongClickListener(icon -> TaskMenuView.showForTask(this));
     }
 
     @Override
     public void onTaskDataUnloaded() {
-        mSnapshotView.setThumbnail(null);
+        mSnapshotView.setThumbnail(null, null);
         mIconView.setImageDrawable(null);
         mIconView.setOnLongClickListener(null);
     }
@@ -223,80 +175,6 @@
         // Do nothing
     }
 
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        mSwipeDetector.onTouchEvent(ev);
-        return super.onInterceptTouchEvent(ev);
-    }
-
-    @Override
-    public boolean onTouchEvent(MotionEvent event) {
-        mSwipeDetector.onTouchEvent(event);
-        return mSwipeDetector.isDraggingOrSettling() || super.onTouchEvent(event);
-    }
-
-    // Swipe detector methods
-
-    @Override
-    public void onDragStart(boolean start) {
-        getParent().requestDisallowInterceptTouchEvent(true);
-    }
-
-    @Override
-    public boolean onDrag(float displacement, float velocity) {
-        setSwipeProgress(Utilities.boundToRange(displacement / mSwipeDistance,
-                allowsSwipeUp() ? -1 : 0, allowsSwipeDown() ? 1 : 0));
-        return true;
-    }
-
-    /**
-     * Indicates the page is being removed.
-     * @param progress Ranges from -1 (fading upwards) to 1 (fading downwards).
-     */
-    private void setSwipeProgress(float progress) {
-        mSwipeProgress = progress;
-        float translationY = mSwipeProgress * mSwipeDistance;
-        float alpha = 1f - mAlphaInterpolator.getInterpolation(Math.abs(mSwipeProgress));
-        // Only change children to avoid changing our properties while dragging.
-        mIconView.setTranslationY(translationY);
-        mSnapshotView.setTranslationY(translationY);
-        mIconView.setAlpha(alpha);
-        mSnapshotView.setAlpha(alpha);
-    }
-
-    private boolean allowsSwipeUp() {
-        return (SWIPE_DIRECTIONS & SwipeDetector.DIRECTION_POSITIVE) != 0;
-    }
-
-    private boolean allowsSwipeDown() {
-        return (SWIPE_DIRECTIONS & SwipeDetector.DIRECTION_NEGATIVE) != 0;
-    }
-
-    @Override
-    public void onDragEnd(float velocity, boolean fling) {
-        boolean movingAwayFromCenter = velocity < 0 == mSwipeProgress < 0;
-        boolean flingAway = fling && movingAwayFromCenter
-                && (allowsSwipeUp() && velocity < 0 || allowsSwipeDown() && velocity > 0);
-        final boolean shouldRemove = flingAway || (!fling && Math.abs(mSwipeProgress) > 0.5f);
-        float fromProgress = mSwipeProgress;
-        float toProgress = !shouldRemove ? 0f : mSwipeProgress < 0 ? -1f : 1f;
-        ValueAnimator swipeAnimator = ObjectAnimator.ofFloat(this, PROPERTY_SWIPE_PROGRESS,
-                fromProgress, toProgress);
-        swipeAnimator.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (shouldRemove) {
-                    ((RecentsView) getParent()).onTaskDismissed(TaskView.this);
-                }
-                mSwipeDetector.finishedScrolling();
-            }
-        });
-        swipeAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
-                Math.abs(toProgress - fromProgress)));
-        swipeAnimator.setInterpolator(mSwipeAnimInterpolator);
-        swipeAnimator.start();
-    }
-
     public void animateIconToScale(float scale) {
         ObjectAnimator.ofFloat(this, SCALE_ICON_PROPERTY, scale)
                 .setDuration(SCALE_ICON_DURATION).start();
@@ -331,13 +209,32 @@
             // Make sure that the task cards do not overlap with the workspace card
             float min = scrollState.halfPageWidth * (1 - scale);
             if (scrollState.isRtl) {
-                setTranslationX(Math.min(translation, min));
+                setTranslationX(Math.min(translation, min) - scrollState.prevPageExtraWidth);
             } else {
-                setTranslationX(Math.max(translation, -min));
+                setTranslationX(Math.max(translation, -min) + scrollState.prevPageExtraWidth);
             }
         } else {
             setTranslationX(translation);
         }
+        scrollState.prevPageExtraWidth = 0;
         return SCROLL_TYPE_TASK;
     }
+
+
+    private static final class TaskOutlineProvider extends ViewOutlineProvider {
+
+        private final int mMarginTop;
+        private final float mRadius;
+
+        TaskOutlineProvider(Resources res) {
+            mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
+            mRadius = res.getDimension(R.dimen.task_corner_radius);
+        }
+
+        @Override
+        public void getOutline(View view, Outline outline) {
+            outline.setRoundRect(0, mMarginTop, view.getWidth(),
+                    view.getHeight(), mRadius);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 4321791..c35ffee 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -31,12 +31,15 @@
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.support.annotation.IntDef;
 import android.util.Log;
 import android.view.Choreographer;
 import android.view.Display;
@@ -56,6 +59,8 @@
 import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.function.Consumer;
 
 /**
@@ -64,8 +69,21 @@
 @TargetApi(Build.VERSION_CODES.O)
 public class TouchInteractionService extends Service {
 
+    public static final int EDGE_NAV_BAR = 1 << 8;
+
     private static final String TAG = "TouchInteractionService";
 
+    @IntDef(flag = true, value = {
+            INTERACTION_NORMAL,
+            INTERACTION_QUICK_SWITCH,
+            INTERACTION_QUICK_SCRUB
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface InteractionType {}
+    public static final int INTERACTION_NORMAL = 0;
+    public static final int INTERACTION_QUICK_SWITCH = 1;
+    public static final int INTERACTION_QUICK_SCRUB = 2;
+
     private final IBinder mMyBinder = new IOverviewProxy.Stub() {
 
         @Override
@@ -77,6 +95,30 @@
         public void onBind(ISystemUiProxy iSystemUiProxy) throws RemoteException {
             mISystemUiProxy = iSystemUiProxy;
         }
+
+        @Override
+        public void onQuickSwitch() {
+            startTouchTracking(INTERACTION_QUICK_SWITCH);
+        }
+
+        @Override
+        public void onQuickScrubStart() {
+            startTouchTracking(INTERACTION_QUICK_SCRUB);
+        }
+
+        @Override
+        public void onQuickScrubEnd() {
+            if (mInteractionHandler != null) {
+                mInteractionHandler.onQuickScrubEnd();
+            }
+        }
+
+        @Override
+        public void onQuickScrubProgress(float progress) {
+            if (mInteractionHandler != null) {
+                mInteractionHandler.onQuickScrubProgress(progress);
+            }
+        }
     };
 
     private final Consumer<MotionEvent> mOtherActivityTouchConsumer
@@ -121,10 +163,10 @@
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         ResolveInfo info = getPackageManager().resolveActivity(mHomeIntent, 0);
         mLauncher = new ComponentName(getPackageName(), info.activityInfo.name);
-        mHomeIntent.setComponent(mLauncher);
+        // Clear the packageName as system can fail to dedupe it b/64108432
+        mHomeIntent.setComponent(mLauncher).setPackage(null);
 
         mEventQueue = new MotionEventQueue(Choreographer.getInstance(), this::handleMotionEvent);
-        mRecentsModel.loadTasks(-1, null);
         sConnected = true;
     }
 
@@ -214,7 +256,7 @@
                 if (mInteractionHandler == null) {
                     if (Math.abs(displacement) >= mTouchSlop) {
                         mStartDisplacement = Math.signum(displacement) * mTouchSlop;
-                        startTouchTracking();
+                        startTouchTracking(INTERACTION_NORMAL);
                     }
                 } else {
                     // Move
@@ -242,10 +284,10 @@
         return mDisplayRotation == Surface.ROTATION_270 && mStableInsets.left > 0;
     }
 
-    private void startTouchTracking() {
+    private void startTouchTracking(@InteractionType int interactionType) {
         // Create the shared handler
         final NavBarSwipeInteractionHandler handler =
-                new NavBarSwipeInteractionHandler(mRunningTask, this);
+                new NavBarSwipeInteractionHandler(mRunningTask, this, interactionType);
 
         TraceHelper.partitionSection("TouchInt", "Thershold crossed ");
 
@@ -267,7 +309,7 @@
         });
 
         // Preload the plan
-        mRecentsModel.loadTasks(mRunningTask.id, handler::setRecentsTaskLoadPlan);
+        mRecentsModel.loadTasks(mRunningTask.id, null);
         mInteractionHandler = handler;
     }
 
@@ -287,11 +329,6 @@
     }
 
     private Bitmap getCurrentTaskSnapshot() {
-        if (mISystemUiProxy == null) {
-            Log.e(TAG, "Never received systemUIProxy");
-            return null;
-        }
-
         TraceHelper.beginSection("TaskSnapshot");
         // TODO: We are using some hardcoded layers for now, to best approximate the activity layers
         Point displaySize = new Point();
@@ -307,9 +344,13 @@
         try {
             return mISystemUiProxy.screenshot(new Rect(), displaySize.x, displaySize.y, 0, 100000,
                     false, rotation).toBitmap();
-        } catch (RemoteException e) {
+        } catch (Exception e) {
             Log.e(TAG, "Error capturing snapshot", e);
-            return null;
+
+            // Return a dummy bitmap
+            Bitmap bitmap = Bitmap.createBitmap(displaySize.x, displaySize.y, Config.RGB_565);
+            bitmap.eraseColor(Color.WHITE);
+            return bitmap;
         } finally {
             TraceHelper.endSection("TaskSnapshot");
         }
@@ -381,9 +422,12 @@
         }
 
         private void sendEvent(MotionEvent ev) {
+            int flags = ev.getEdgeFlags();
+            ev.setEdgeFlags(flags | EDGE_NAV_BAR);
             ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]);
             mTarget.dispatchTouchEvent(ev);
             ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]);
+            ev.setEdgeFlags(flags);
         }
     }
 }
diff --git a/res/layout-land/launcher.xml b/res/layout-land/launcher.xml
index 9bd3c67..f26bfbd 100644
--- a/res/layout-land/launcher.xml
+++ b/res/layout-land/launcher.xml
@@ -21,13 +21,13 @@
     android:id="@+id/launcher"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:background="?attr/workspaceStatusBarScrim"
     android:fitsSystemWindows="true">
 
     <com.android.launcher3.dragndrop.DragLayer
         android:id="@+id/drag_layer"
         android:clipChildren="false"
         android:clipToPadding="false"
-        android:background="?attr/workspaceStatusBarScrim"
         android:importantForAccessibility="no"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
diff --git a/res/layout-port/launcher.xml b/res/layout-port/launcher.xml
index b678398..cde3bd5 100644
--- a/res/layout-port/launcher.xml
+++ b/res/layout-port/launcher.xml
@@ -22,6 +22,7 @@
     android:id="@+id/launcher"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:background="?attr/workspaceStatusBarScrim"
     android:fitsSystemWindows="true">
 
     <com.android.launcher3.dragndrop.DragLayer
@@ -29,7 +30,6 @@
         android:clipChildren="false"
         android:importantForAccessibility="no"
         android:clipToPadding="false"
-        android:background="?attr/workspaceStatusBarScrim"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
diff --git a/res/layout-sw720dp/launcher.xml b/res/layout-sw720dp/launcher.xml
index 7e6c659..fe2f108 100644
--- a/res/layout-sw720dp/launcher.xml
+++ b/res/layout-sw720dp/launcher.xml
@@ -21,6 +21,7 @@
     android:id="@+id/launcher"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:background="?attr/workspaceStatusBarScrim"
     android:fitsSystemWindows="true">
 
     <com.android.launcher3.dragndrop.DragLayer
@@ -28,7 +29,6 @@
         android:clipChildren="false"
         android:clipToPadding="false"
         android:importantForAccessibility="no"
-        android:background="?attr/workspaceStatusBarScrim"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
diff --git a/res/layout/work_tab_footer.xml b/res/layout/work_tab_footer.xml
index e3416ac..dc0fdd4 100644
--- a/res/layout/work_tab_footer.xml
+++ b/res/layout/work_tab_footer.xml
@@ -73,7 +73,6 @@
         android:lines="1"
         android:minHeight="24dp"
         android:paddingStart="12dp"
-        android:text="@string/managed_by_your_organisation"
         android:textColor="?android:attr/textColorHint"
         android:textSize="13sp"/>
 
diff --git a/res/values/config.xml b/res/values/config.xml
index f48d474..2096200 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -101,6 +101,9 @@
     <!-- Package name of the default wallpaper picker. -->
     <string name="wallpaper_picker_package" translatable="false"></string>
 
+    <!-- Whitelisted package to retrieve packagename for badge. Can be empty. -->
+    <string name="shortcutinfocompat_badgepkg_whitelist" translatable="false"></string>
+
     <!-- View ID to use for QSB widget -->
     <item type="id" name="qsb_widget" />
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d77065c..ee09946 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -324,13 +324,16 @@
     <!-- Label of tab to indicate work apps -->
     <string name="all_apps_work_tab">Work</string>
 
-    <!-- Label of the work mode toggle -->
+    <!-- This string is in the work profile tab when a user has All Apps open on their phone. This is a label for a toggle to turn the work profile on and off. "Work profile" means a separate profile on a user's phone that's specifically for their work apps and managed by their company. "Work" is used as an adjective.-->
     <string name="work_profile_toggle_label">Work profile</string>
-    <!-- Title in bottom user education view in work tab -->
+    <!-- Title of an overlay in All Apps. This overlay is letting a user know about their work profile, which is managed by their employer. "Work apps" are apps in a user's work profile.-->
     <string name="bottom_work_tab_user_education_title">Find work apps here</string>
-    <!-- Body text in bottom user education view in work tab -->
-    <string name="bottom_work_tab_user_education_body">Each work app has an orange badge, which means it\'s kept secure by your organization. Work apps can be moved to your Home Screen for easier access.</string>
-    <!-- Label in work tab to tell users that work profile is managed by their organisation. -->
-    <string name="managed_by_your_organisation">Managed by your organisation</string>
+    <!-- Text in an overlay in All Apps. This overlay is letting a user know about their work profile, which is managed by their employer.-->
+    <string name="bottom_work_tab_user_education_body">Each work app has an orange badge and is kept secure by your organization. Move apps to your Home screen for easier access.</string>
+    <!-- This string is in the work profile tab when a user has All Apps open on their phone. It describes the label of a toggle, "Work profile," as being managed by the user's employer.
+    "Organization" is used to represent a variety of businesses, non-profits, and educational institutions).-->
+    <string name="work_mode_on_label">Managed by your organization</string>
+    <!-- This string appears under a the label of a toggle in the work profile tab on a user's phone. It describes the status of the toggle, "Work profile," when it's turned off. "Work profile" means a separate profile on a user's phone that's speficially for their work apps and is managed by their company.-->
+    <string name="work_mode_off_label">Notifications and apps are off</string>
 
 </resources>
diff --git a/src/com/android/launcher3/BaseActivity.java b/src/com/android/launcher3/BaseActivity.java
index e496495..a54c21c 100644
--- a/src/com/android/launcher3/BaseActivity.java
+++ b/src/com/android/launcher3/BaseActivity.java
@@ -31,6 +31,8 @@
     protected UserEventDispatcher mUserEventDispatcher;
     protected SystemUiController mSystemUiController;
 
+    private boolean mStarted;
+
     public DeviceProfile getDeviceProfile() {
         return mDeviceProfile;
     }
@@ -69,4 +71,20 @@
     public void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
     }
+
+    @Override
+    protected void onStart() {
+        mStarted = true;
+        super.onStart();
+    }
+
+    @Override
+    protected void onStop() {
+        mStarted = false;
+        super.onStop();
+    }
+
+    public boolean isStarted() {
+        return mStarted;
+    }
 }
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 76c7845..cc13263 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -91,7 +91,7 @@
         // Move to mScrollbar's coordinate system.
         // We need to take parent into account (view pager's location)
         ViewGroup parent = (ViewGroup) getParent();
-        int left = parent.getLeft() + getLeft() - mScrollbar.getLeft();
+        int left = parent.getLeft() - mScrollbar.getLeft();
         int top = parent.getTop() + getTop() - mScrollbar.getTop() - getScrollBarTop();
         ev.offsetLocation(left, top);
         try {
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 75e2f70..164efe5 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -102,8 +102,6 @@
 
     // All apps
     public int allAppsCellHeightPx;
-    public int allAppsNumCols;
-    public int allAppsNumPredictiveCols;
     public int allAppsIconSizePx;
     public int allAppsIconDrawablePaddingPx;
     public float allAppsIconTextSizePx;
@@ -214,6 +212,11 @@
         mBadgeRenderer = new BadgeRenderer(iconSizePx);
     }
 
+    public DeviceProfile copy(Context context) {
+        Point size = new Point(availableWidthPx, availableHeightPx);
+        return new DeviceProfile(context, inv, size, size, widthPx, heightPx, isLandscape);
+    }
+
     DeviceProfile getMultiWindowProfile(Context context, Point mwSize) {
         // We take the minimum sizes of this profile and it's multi-window variant to ensure that
         // the system decor is always excluded.
@@ -376,8 +379,8 @@
         updateWorkspacePadding();
     }
 
-    public void updateAppsViewNumCols() {
-        allAppsNumCols = allAppsNumPredictiveCols = inv.numColumns;
+    public Rect getInsets() {
+        return mInsets;
     }
 
     public Point getCellSize() {
@@ -453,9 +456,9 @@
                     mInsets.top + availableHeightPx);
         } else {
             // Folders should only appear below the drop target bar and above the hotseat
-            return new Rect(mInsets.left,
+            return new Rect(mInsets.left + edgeMarginPx,
                     mInsets.top + dropTargetBarSizePx + edgeMarginPx,
-                    mInsets.left + availableWidthPx,
+                    mInsets.left + availableWidthPx - edgeMarginPx,
                     mInsets.top + availableHeightPx - hotseatBarSizePx
                             - pageIndicatorSizePx - edgeMarginPx);
         }
@@ -509,6 +512,5 @@
         Configuration context = new Configuration(c.getResources().getConfiguration());
         context.orientation = orientation;
         return c.createConfigurationContext(context);
-
     }
 }
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 1f5aa13..25eacb5 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -46,7 +46,7 @@
     private CellLayout mContent;
 
     @ViewDebug.ExportedProperty(category = "launcher")
-    private final boolean mHasVerticalHotseat;
+    private boolean mHasVerticalHotseat;
 
     public Hotseat(Context context) {
         this(context, null);
@@ -59,7 +59,6 @@
     public Hotseat(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         mLauncher = Launcher.getLauncher(context);
-        mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout();
     }
 
     public CellLayout getLayout() {
@@ -91,13 +90,7 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        mContent = (CellLayout) findViewById(R.id.layout);
-        if (grid.isVerticalBarLayout()) {
-            mContent.setGridSize(1, grid.inv.numHotseatIcons);
-        } else {
-            mContent.setGridSize(grid.inv.numHotseatIcons, 1);
-        }
+        mContent = findViewById(R.id.layout);
 
         resetLayout();
     }
@@ -165,7 +158,11 @@
     public void setInsets(Rect insets) {
         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
         DeviceProfile grid = mLauncher.getDeviceProfile();
+        mHasVerticalHotseat = mLauncher.getDeviceProfile().isVerticalBarLayout();
+
         if (mHasVerticalHotseat) {
+            mContent.setGridSize(1, grid.inv.numHotseatIcons);
+
             lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
             if (insets.left > insets.right) {
                 lp.gravity = Gravity.LEFT;
@@ -180,6 +177,8 @@
                         grid.hotseatBarSidePaddingPx, insets.top, insets.right, insets.bottom);
             }
         } else {
+            mContent.setGridSize(grid.inv.numHotseatIcons, 1);
+
             lp.gravity = Gravity.BOTTOM;
             lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
             lp.height = grid.hotseatBarSizePx + insets.bottom;
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 4d58593..38a3044 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -769,12 +769,12 @@
         mAppWidgetHost.setListenIfResumed(true);
         NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
 
-        if (mShouldFadeInScrim && mDragLayer.getBackground() != null) {
+        if (mShouldFadeInScrim && mLauncherView.getBackground() != null) {
             if (mScrimAnimator != null) {
                 mScrimAnimator.cancel();
             }
-            mDragLayer.getBackground().setAlpha(0);
-            mScrimAnimator = ObjectAnimator.ofInt(mDragLayer.getBackground(),
+            mLauncherView.getBackground().setAlpha(0);
+            mScrimAnimator = ObjectAnimator.ofInt(mLauncherView.getBackground(),
                     LauncherAnimUtils.DRAWABLE_ALPHA, 0, 255);
             mScrimAnimator.addListener(new AnimatorListenerAdapter() {
                 @Override
@@ -915,7 +915,7 @@
      * Finds all the views we need and configure them properly.
      */
     private void setupViews() {
-        mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
+        mDragLayer = findViewById(R.id.drag_layer);
         mFocusHandler = mDragLayer.getFocusIndicatorHelper();
         mWorkspace = mDragLayer.findViewById(R.id.workspace);
         mWorkspace.initParentViews(mDragLayer);
@@ -1237,9 +1237,9 @@
                 }
 
                 // In all these cases, only animate if we're already on home
-                AbstractFloatingView.closeAllOpenViews(this, alreadyOnHome);
+                AbstractFloatingView.closeAllOpenViews(this, isStarted());
 
-                mStateManager.goToState(NORMAL, alreadyOnHome /* animated */);
+                mStateManager.goToState(NORMAL);
 
                 // Reset the apps view
                 if (!alreadyOnHome && mAppsView != null) {
@@ -1608,8 +1608,9 @@
         if (topView != null) {
             topView.onBackPressed();
         } else if (!isInState(NORMAL)) {
+            LauncherState lastState = mStateManager.getLastState();
             ued.logActionCommand(Action.Command.BACK, mStateManager.getState().containerType);
-            mStateManager.goToState(NORMAL);
+            mStateManager.goToState(lastState);
         } else {
             // Back button is a no-op here, but give at least some feedback for the button press
             mWorkspace.showOutlinesTemporarily();
@@ -1856,7 +1857,9 @@
         if (v != null) {
             intent.setSourceBounds(getViewBounds(v));
             // If there is no target package, use the default intent chooser animation
-            launchOptions = hasTargetPackage ? getActivityLaunchOptions(v) : null;
+            launchOptions = hasTargetPackage
+                    ? getActivityLaunchOptions(v, isInMultiWindowModeCompat())
+                    : null;
         } else {
             launchOptions = null;
         }
@@ -1910,8 +1913,7 @@
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.M)
-    public Bundle getActivityLaunchOptions(View v) {
+    public Bundle getDefaultActivityLaunchOptions(View v) {
         if (Utilities.ATLEAST_MARSHMALLOW) {
             int left = 0, top = 0;
             int width = v.getMeasuredWidth(), height = v.getMeasuredHeight();
@@ -1926,7 +1928,8 @@
                     height = bounds.height();
                 }
             }
-            return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height).toBundle();
+            return ActivityOptions.makeClipRevealAnimation(v, left, top, width, height)
+                    .toBundle();
         } else if (Utilities.ATLEAST_LOLLIPOP_MR1) {
             // On L devices, we use the device default slide-up transition.
             // On L MR1 devices, we use a custom version of the slide-up transition which
@@ -1937,6 +1940,13 @@
         return null;
     }
 
+    @TargetApi(Build.VERSION_CODES.M)
+    public Bundle getActivityLaunchOptions(View v, boolean useDefaultLaunchOptions) {
+        return useDefaultLaunchOptions
+                ? getDefaultActivityLaunchOptions(v)
+                : UiFactory.getActivityLaunchOptions(this, v);
+    }
+
     public Rect getViewBounds(View v) {
         int[] pos = new int[2];
         v.getLocationOnScreen(pos);
@@ -1949,11 +1959,20 @@
             Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
             return mAppLaunchSuccess;
         }
+
+        boolean isShortcut = Utilities.ATLEAST_MARSHMALLOW
+                && (item instanceof ShortcutInfo)
+                && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT
+                || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
+                && !((ShortcutInfo) item).isPromise();
+
         // Only launch using the new animation if the shortcut has not opted out (this is a
         // private contract between launcher and may be ignored in the future).
         boolean useLaunchAnimation = (v != null) &&
                 !intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION);
-        Bundle optsBundle = useLaunchAnimation ? getActivityLaunchOptions(v) : null;
+        Bundle optsBundle = useLaunchAnimation
+                ? getActivityLaunchOptions(v, isShortcut || isInMultiWindowModeCompat())
+                : null;
 
         UserHandle user = item == null ? null : item.user;
 
@@ -1963,11 +1982,7 @@
             intent.setSourceBounds(getViewBounds(v));
         }
         try {
-            if (Utilities.ATLEAST_MARSHMALLOW
-                    && (item instanceof ShortcutInfo)
-                    && (item.itemType == Favorites.ITEM_TYPE_SHORTCUT
-                     || item.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT)
-                    && !((ShortcutInfo) item).isPromise()) {
+            if (isShortcut) {
                 // Shortcuts need some special checks due to legacy reasons.
                 startShortcutIntentSafely(intent, optsBundle, item);
             } else if (user == null || user.equals(Process.myUserHandle())) {
@@ -2551,10 +2566,6 @@
         return bounceAnim;
     }
 
-    public boolean useVerticalBarLayout() {
-        return mDeviceProfile.isVerticalBarLayout();
-    }
-
     /**
      * Add the icons for all apps.
      *
diff --git a/src/com/android/launcher3/LauncherRootView.java b/src/com/android/launcher3/LauncherRootView.java
index 1a1bec6..18d5234 100644
--- a/src/com/android/launcher3/LauncherRootView.java
+++ b/src/com/android/launcher3/LauncherRootView.java
@@ -11,6 +11,8 @@
 import android.view.View;
 import android.view.ViewDebug;
 
+import com.android.launcher3.util.Themes;
+
 import static com.android.launcher3.util.SystemUiController.FLAG_DARK_NAV;
 import static com.android.launcher3.util.SystemUiController.UI_STATE_ROOT_VIEW;
 
@@ -66,6 +68,7 @@
 
         // Update device profile before notifying th children.
         mLauncher.getDeviceProfile().updateInsets(insets);
+        boolean resetState = !insets.equals(mInsets);
         setInsets(insets);
 
         if (mAlignedView != null) {
@@ -77,10 +80,20 @@
                 mAlignedView.setLayoutParams(lp);
             }
         }
+        if (resetState) {
+            mLauncher.getStateManager().reapplyState();
+        }
 
         return true; // I'll take it from here
     }
 
+    @Override
+    public void setInsets(Rect insets) {
+        super.setInsets(insets);
+        setBackground(insets.top == 0 ? null
+                : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
+    }
+
     public void dispatchInsets() {
         fitSystemWindows(mInsets);
     }
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 4f65d19..472a5a9 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -27,6 +27,7 @@
 import com.android.launcher3.uioverrides.AllAppsState;
 import com.android.launcher3.states.SpringLoadedState;
 import com.android.launcher3.uioverrides.OverviewState;
+import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 
 import java.util.Arrays;
@@ -54,6 +55,9 @@
 
     private static final LauncherState[] sAllStates = new LauncherState[4];
 
+    /**
+     * TODO: Create a separate class for NORMAL state.
+     */
     public static final LauncherState NORMAL = new LauncherState(0, ContainerType.WORKSPACE,
             0, FLAG_DISABLE_RESTORE | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED);
 
@@ -170,6 +174,20 @@
         };
     }
 
+    public LauncherState getHistoryForState(LauncherState previousState) {
+        // No history is supported
+        return NORMAL;
+    }
+
+    /**
+     * Called when the start transition ends and the user settles on this particular state.
+     */
+    public void onStateTransitionEnd(Launcher launcher) {
+        if (this == NORMAL) {
+            UiFactory.resetOverview(launcher);
+        }
+    }
+
     protected static void dispatchWindowStateChanged(Launcher launcher) {
         launcher.getWindow().getDecorView().sendAccessibilityEvent(TYPE_WINDOW_STATE_CHANGED);
     }
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index d25f958..8eeeec3 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -66,7 +66,7 @@
  *          - Go back with back key  TODO: make this not go to workspace
  *          - From all apps
  *          - From workspace
- *   - Enter and exit car mode (becuase it causes an extra configuration changed)
+ *   - Enter and exit car mode (becase it causes an extra configuration changed)
  *          - From all apps
  *          - From the center workspace
  *          - From another workspace
@@ -82,6 +82,9 @@
     private StateHandler[] mStateHandlers;
     private LauncherState mState = NORMAL;
 
+    private LauncherState mLastStableState = NORMAL;
+    private LauncherState mCurrentStableState = NORMAL;
+
     private StateListener mStateListener;
 
     public LauncherStateManager(Launcher l) {
@@ -108,7 +111,7 @@
      * @see #goToState(LauncherState, boolean, Runnable)
      */
     public void goToState(LauncherState state) {
-        goToState(state, true, 0, null);
+        goToState(state, mLauncher.isStarted() /* animated */, 0, null);
     }
 
     /**
@@ -143,10 +146,17 @@
         goToState(state, true, delay, null);
     }
 
+    public void reapplyState() {
+        if (mConfig.mCurrentAnimation == null) {
+            for (StateHandler handler : getStateHandlers()) {
+                handler.setState(mState);
+            }
+        }
+    }
+
     private void goToState(LauncherState state, boolean animated, long delay,
             Runnable onCompleteRunnable) {
         if (mLauncher.isInState(state) && mConfig.mCurrentAnimation == null) {
-
             // Run any queued runnable
             if (onCompleteRunnable != null) {
                 onCompleteRunnable.run();
@@ -262,11 +272,22 @@
     }
 
     private void onStateTransitionEnd(LauncherState state) {
+        // Only change the stable states after the transitions have finished
+        if (state != mCurrentStableState) {
+            mLastStableState = state.getHistoryForState(mCurrentStableState);
+            mCurrentStableState = state;
+        }
+
+        state.onStateTransitionEnd(mLauncher);
         mLauncher.getWorkspace().setClipChildren(!state.disablePageClipping);
         mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
         mLauncher.finishAutoCancelActionMode();
     }
 
+    public LauncherState getLastState() {
+        return mLastStableState;
+    }
+
     /**
      * Cancels the current animation.
      */
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 4c30853..ad94a6b 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -563,13 +563,21 @@
         computeScrollHelper();
     }
 
+    public int getExpectedHeight() {
+        return getMeasuredHeight();
+    }
+
     public int getNormalChildHeight() {
-        return  getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
+        return  getExpectedHeight() - getPaddingTop() - getPaddingBottom()
                 - mInsets.top - mInsets.bottom;
     }
 
+    public int getExpectedWidth() {
+        return getMeasuredWidth();
+    }
+
     public int getNormalChildWidth() {
-        return  getMeasuredWidth() - getPaddingLeft() - getPaddingRight()
+        return  getExpectedWidth() - getPaddingLeft() - getPaddingRight()
                 - mInsets.left - mInsets.right;
     }
 
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 893d820..c946a44 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -207,7 +207,7 @@
     private boolean mStripScreensOnPageStopMoving = false;
 
     private DragPreviewProvider mOutlineProvider = null;
-    private final boolean mWorkspaceFadeInAdjacentScreens;
+    private boolean mWorkspaceFadeInAdjacentScreens;
 
     final WallpaperOffsetInterpolator mWallpaperOffset;
     private boolean mUnlockWallpaperFromDefaultPageOnLayout;
@@ -292,8 +292,6 @@
 
         mLauncher = Launcher.getLauncher(context);
         mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
         mWallpaperManager = WallpaperManager.getInstance(context);
 
         mWallpaperOffset = new WallpaperOffsetInterpolator(this);
@@ -312,6 +310,9 @@
         mInsets.set(insets);
 
         DeviceProfile grid = mLauncher.getDeviceProfile();
+        mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx);
+        mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
+
         Rect padding = grid.workspacePadding;
         setPadding(padding.left, padding.top, padding.right, padding.bottom);
 
@@ -324,6 +325,13 @@
             // We assume symmetrical padding in portrait mode.
             setPageSpacing(Math.max(grid.defaultPageSpacingPx, padding.left + 1));
         }
+
+        int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
+        int paddingBottom = grid.cellLayoutBottomPaddingPx;
+        for (int i = mWorkspaceScreens.size() - 1; i >= 0; i--) {
+            mWorkspaceScreens.valueAt(i)
+                    .setPadding(paddingLeftRight, 0, paddingLeftRight, paddingBottom);
+        }
     }
 
     /**
@@ -445,12 +453,9 @@
      */
     protected void initWorkspace() {
         mCurrentPage = DEFAULT_PAGE;
-        DeviceProfile grid = mLauncher.getDeviceProfile();
-        setWillNotDraw(false);
         setClipToPadding(false);
 
         setupLayoutTransition();
-        mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx);
 
         // Set the wallpaper dimensions when Launcher starts up
         setWallpaperDimension();
@@ -3442,6 +3447,18 @@
     }
 
     @Override
+    public int getExpectedHeight() {
+        return getMeasuredHeight() <= 0
+                ? mLauncher.getDeviceProfile().heightPx : getMeasuredHeight();
+    }
+
+    @Override
+    public int getExpectedWidth() {
+        return getMeasuredWidth() <= 0
+                ? mLauncher.getDeviceProfile().widthPx : getMeasuredWidth();
+    }
+
+    @Override
     protected String getPageIndicatorDescription() {
         return getResources().getString(R.string.all_apps_button_label);
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index b88efdd..2e544ec 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -162,6 +162,7 @@
         }
         onAppsUpdated();
         mSearchUiManager.refreshSearchResult();
+        mHeader.onAppsUpdated();
     }
 
     /**
@@ -272,8 +273,6 @@
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         DeviceProfile grid = mLauncher.getDeviceProfile();
-        // Update the number of items in the grid before we measure the view
-        grid.updateAppsViewNumCols();
 
         if (mNumAppsPerRow != grid.inv.numColumns ||
                 mNumPredictedAppsPerRow != grid.inv.numColumns) {
@@ -361,18 +360,30 @@
     public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
         final PackageUserKey packageUserKey = new PackageUserKey(null, null);
         for (int j = 0; j < mAH.length; j++) {
-            if (mAH[j].recyclerView != null) {
-                final int n = mAH[j].recyclerView.getChildCount();
-                for (int i = 0; i < n; i++) {
-                    View child = mAH[j].recyclerView.getChildAt(i);
-                    if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) {
-                        continue;
-                    }
-                    ItemInfo info = (ItemInfo) child.getTag();
-                    if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) {
-                        ((BubbleTextView) child).applyBadgeState(info, true /* animate */);
-                    }
-                }
+            updateIconBadges(updatedBadges, packageUserKey, mAH[j].recyclerView);
+        }
+        if (mHeader != null) {
+            updateIconBadges(updatedBadges, packageUserKey, mHeader.getPredictionRow());
+        }
+    }
+
+    private void updateIconBadges(Set<PackageUserKey> updatedBadges, PackageUserKey packageUserKey,
+            ViewGroup parent) {
+        if (parent == null) {
+            return;
+        }
+        final int n = parent.getChildCount();
+        for (int i = 0; i < n; i++) {
+            View child = parent.getChildAt(i);
+            if (child instanceof PredictionRowView) {
+                updateIconBadges(updatedBadges, packageUserKey, (PredictionRowView) child);
+            }
+            if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) {
+                continue;
+            }
+            ItemInfo info = (ItemInfo) child.getTag();
+            if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) {
+                ((BubbleTextView) child).applyBadgeState(info, true /* animate */);
             }
         }
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 234eb81..769f9ba 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
 import com.android.launcher3.anim.SpringAnimationHandler;
+import com.android.launcher3.compat.UserManagerCompat;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
 import com.android.launcher3.discovery.AppDiscoveryItemView;
@@ -377,6 +378,11 @@
             case VIEW_TYPE_WORK_TAB_FOOTER:
                 WorkModeSwitch workModeToggle = holder.itemView.findViewById(R.id.work_mode_toggle);
                 workModeToggle.refresh();
+                TextView managedByLabel = holder.itemView.findViewById(R.id.managed_by_label);
+                boolean anyProfileQuietModeEnabled = UserManagerCompat.getInstance(
+                        managedByLabel.getContext()).isAnyProfileQuietModeEnabled();
+                managedByLabel.setText(anyProfileQuietModeEnabled
+                        ? R.string.work_mode_off_label : R.string.work_mode_on_label);
                 break;
         }
         if (mBindViewCallback != null) {
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderView.java b/src/com/android/launcher3/allapps/FloatingHeaderView.java
index 1419a2a..2391768 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderView.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderView.java
@@ -176,6 +176,7 @@
         mAnimator.start();
         mHeaderCollapsed = false;
         mSnappedScrolledY = -mMaxTranslation;
+        mCurrentRV.scrollToTop();
     }
 
     public boolean isExpanded() {
@@ -218,6 +219,9 @@
         p.y = getTop() - mCurrentRV.getTop() - mParent.getTop();
     }
 
+    public void onAppsUpdated() {
+        mPredictionRow.onAppsUpdated();
+    }
 }
 
 
diff --git a/src/com/android/launcher3/allapps/PredictionRowView.java b/src/com/android/launcher3/allapps/PredictionRowView.java
index 4aacc6d..267ef3c 100644
--- a/src/com/android/launcher3/allapps/PredictionRowView.java
+++ b/src/com/android/launcher3/allapps/PredictionRowView.java
@@ -28,9 +28,12 @@
 
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ComponentKeyMapper;
 import com.android.launcher3.util.Themes;
@@ -40,7 +43,8 @@
 import java.util.HashMap;
 import java.util.List;
 
-public class PredictionRowView extends LinearLayout {
+public class PredictionRowView extends LinearLayout implements
+        UserEventDispatcher.LogContainerProvider {
 
     private static final String TAG = "PredictionRowView";
 
@@ -49,7 +53,7 @@
     // The set of predicted app component names
     private final List<ComponentKeyMapper<AppInfo>> mPredictedAppComponents = new ArrayList<>();
     // The set of predicted apps resolved from the component names and the current set of apps
-    private final List<AppInfo> mPredictedApps = new ArrayList<>();
+    private final ArrayList<AppInfo> mPredictedApps = new ArrayList<>();
     private final Paint mPaint;
     // This adapter is only used to create an identical item w/ same behavior as in the all apps RV
     private AllAppsGridAdapter mAdapter;
@@ -105,7 +109,7 @@
     public void setNumAppsPerRow(int numPredictedAppsPerRow) {
         if (mNumPredictedAppsPerRow != numPredictedAppsPerRow) {
             mNumPredictedAppsPerRow = numPredictedAppsPerRow;
-            onAppsUpdated();
+            onPredictionsUpdated();
         }
     }
 
@@ -120,7 +124,7 @@
      * Sets the current set of predicted apps.
      *
      * This can be called before we get the full set of applications, we should merge the results
-     * only in onAppsUpdated() which is idempotent.
+     * only in onPredictionsUpdated() which is idempotent.
      *
      * If the number of predicted apps is the same as the previous list of predicted apps,
      * we can optimize by swapping them in place.
@@ -130,10 +134,11 @@
         mPredictedAppComponents.addAll(apps);
         mPredictedApps.clear();
         mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
-        onAppsUpdated();
+        onPredictionsUpdated();
     }
 
-    private void onAppsUpdated() {
+    private void onPredictionsUpdated() {
+        int childCountBefore = getChildCount();
         if (getChildCount() != mNumPredictedAppsPerRow) {
             while (getChildCount() > mNumPredictedAppsPerRow) {
                 removeViewAt(0);
@@ -160,6 +165,31 @@
                 icon.setVisibility(View.INVISIBLE);
             }
         }
+
+        if (getChildCount() > 0 && childCountBefore == 0
+                || getChildCount() == 0 && childCountBefore > 0) {
+            // setting up header to adjust the height
+            // only necessary if childcount switches from/to 0
+            Launcher.getLauncher(getContext()).getAppsView().setupHeader();
+        }
+    }
+
+    /**
+     * Refreshes the app icons in the row view, while preserving the same set of predictions.
+     */
+    public void onAppsUpdated() {
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (!(child instanceof BubbleTextView)) {
+                continue;
+            }
+            if (i >= mPredictedApps.size()) {
+                break;
+            }
+            BubbleTextView icon = (BubbleTextView) getChildAt(i);
+            icon.reset();
+            icon.applyFromApplicationInfo(mPredictedApps.get(i));
+        }
     }
 
     private List<AppInfo> processPredictedAppComponents(
@@ -199,4 +229,17 @@
             canvas.drawLine(x1, y, x2, y, mPaint);
         }
     }
+
+    @Override
+    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+            LauncherLogProto.Target targetParent) {
+        for (int i = 0; i < mPredictedApps.size(); i++) {
+            AppInfo appInfo = mPredictedApps.get(i);
+            if (appInfo == info) {
+                targetParent.containerType = LauncherLogProto.ContainerType.PREDICTION;
+                target.predictedRank = i;
+                break;
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index 32c9ce3..e7cf092 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -55,22 +55,9 @@
     }
 
     public void refresh() {
-        setCheckedInternal(!isAnyProfileQuietModeEnabled());
-        setEnabled(true);
-    }
-
-    private boolean isAnyProfileQuietModeEnabled() {
         UserManagerCompat userManager = UserManagerCompat.getInstance(getContext());
-        List<UserHandle> userProfiles = userManager.getUserProfiles();
-        for (UserHandle userProfile : userProfiles) {
-            if (Process.myUserHandle().equals(userProfile)) {
-                continue;
-            }
-            if (userManager.isQuietModeEnabled(userProfile)) {
-                return true;
-            }
-        }
-        return false;
+        setCheckedInternal(!userManager.isAnyProfileQuietModeEnabled());
+        setEnabled(true);
     }
 
     private void trySetQuietModeEnabledToAllProfilesAsync(boolean enabled) {
@@ -91,7 +78,7 @@
                     if (Process.myUserHandle().equals(userProfile)) {
                         continue;
                     }
-                    showConfirm |= !userManager.trySetQuietModeEnabled(enabled, userProfile);
+                    showConfirm |= !userManager.requestQuietModeEnabled(enabled, userProfile);
                 }
                 return showConfirm;
             }
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 6819386..68e9847 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -33,6 +33,12 @@
  */
 public abstract class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateListener {
 
+    /**
+     * Creates an animation controller for the provided animation.
+     * The actual duration does not matter as the animation is manually controlled. It just
+     * needs to be larger than the total number of pixels so that we don't have jittering due
+     * to float (animation-fraction * total duration) to int conversion.
+     */
     public static AnimatorPlaybackController wrap(AnimatorSet anim, long duration) {
 
         /**
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 8826e64..f3a3539 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -42,6 +42,8 @@
 
     public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
 
+    public static final Interpolator AGGRESSIVE_EASE = new PathInterpolator(0.2f, 0f, 0f, 1f);
+
     /**
      * Inversion of zInterpolate, compounded with an ease-out.
      */
diff --git a/src/com/android/launcher3/compat/UserManagerCompat.java b/src/com/android/launcher3/compat/UserManagerCompat.java
index 197f798..62055dc 100644
--- a/src/com/android/launcher3/compat/UserManagerCompat.java
+++ b/src/com/android/launcher3/compat/UserManagerCompat.java
@@ -63,5 +63,6 @@
     public abstract boolean isUserUnlocked(UserHandle user);
 
     public abstract boolean isDemoUser();
-    public abstract boolean trySetQuietModeEnabled(boolean enableQuietMode, UserHandle user);
+    public abstract boolean requestQuietModeEnabled(boolean enableQuietMode, UserHandle user);
+    public abstract boolean isAnyProfileQuietModeEnabled();
 }
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVL.java b/src/com/android/launcher3/compat/UserManagerCompatVL.java
index e6cc319..e57786d 100644
--- a/src/com/android/launcher3/compat/UserManagerCompatVL.java
+++ b/src/com/android/launcher3/compat/UserManagerCompatVL.java
@@ -83,7 +83,12 @@
     }
 
     @Override
-    public boolean trySetQuietModeEnabled(boolean enableQuietMode, UserHandle user) {
+    public boolean requestQuietModeEnabled(boolean enableQuietMode, UserHandle user) {
+        return false;
+    }
+
+    @Override
+    public boolean isAnyProfileQuietModeEnabled() {
         return false;
     }
 
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVN.java b/src/com/android/launcher3/compat/UserManagerCompatVN.java
index 50a0217..3733565 100644
--- a/src/com/android/launcher3/compat/UserManagerCompatVN.java
+++ b/src/com/android/launcher3/compat/UserManagerCompatVN.java
@@ -19,8 +19,11 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.os.Build;
+import android.os.Process;
 import android.os.UserHandle;
 
+import java.util.List;
+
 @TargetApi(Build.VERSION_CODES.N)
 public class UserManagerCompatVN extends UserManagerCompatVM {
 
@@ -37,5 +40,19 @@
     public boolean isUserUnlocked(UserHandle user) {
         return mUserManager.isUserUnlocked(user);
     }
+
+    @Override
+    public boolean isAnyProfileQuietModeEnabled() {
+        List<UserHandle> userProfiles = getUserProfiles();
+        for (UserHandle userProfile : userProfiles) {
+            if (Process.myUserHandle().equals(userProfile)) {
+                continue;
+            }
+            if (isQuietModeEnabled(userProfile)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
 
diff --git a/src/com/android/launcher3/compat/UserManagerCompatVP.java b/src/com/android/launcher3/compat/UserManagerCompatVP.java
index a0bf0ab..2e8a8eb 100644
--- a/src/com/android/launcher3/compat/UserManagerCompatVP.java
+++ b/src/com/android/launcher3/compat/UserManagerCompatVP.java
@@ -26,29 +26,29 @@
 public class UserManagerCompatVP extends UserManagerCompatVNMr1 {
     private static final String TAG = "UserManagerCompatVP";
 
-    private Method mTrySetQuietModeEnabledMethod;
+    private Method mRequestQuietModeEnabled;
 
     UserManagerCompatVP(Context context) {
         super(context);
         // TODO: Replace it with proper API call once SDK is ready.
         try {
-            mTrySetQuietModeEnabledMethod = UserManager.class.getDeclaredMethod(
-                    "trySetQuietModeEnabled", boolean.class, UserHandle.class);
+            mRequestQuietModeEnabled = UserManager.class.getDeclaredMethod(
+                    "requestQuietModeEnabled", boolean.class, UserHandle.class);
         } catch (NoSuchMethodException e) {
-            Log.e(TAG, "trySetQuietModeEnabled is not available", e);
+            Log.e(TAG, "requestQuietModeEnabled is not available", e);
         }
     }
 
     @Override
-    public boolean trySetQuietModeEnabled(boolean enableQuietMode, UserHandle user) {
-        if (mTrySetQuietModeEnabledMethod == null) {
+    public boolean requestQuietModeEnabled(boolean enableQuietMode, UserHandle user) {
+        if (mRequestQuietModeEnabled == null) {
             return false;
         }
         try {
             return (boolean)
-                    mTrySetQuietModeEnabledMethod.invoke(mUserManager, enableQuietMode, user);
+                    mRequestQuietModeEnabled.invoke(mUserManager, enableQuietMode, user);
         } catch (IllegalAccessException | InvocationTargetException e) {
-            Log.e(TAG, "Failed to invoke mTrySetQuietModeEnabledMethod", e);
+            Log.e(TAG, "Failed to invoke mRequestQuietModeEnabled", e);
         }
         return false;
     }
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 18797a4..2d0e630 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -57,4 +57,6 @@
     public static final boolean ALL_APPS_TABS_ENABLED = true;
     // When enabled prediction row is rendered as it's own custom view
     public static final boolean ALL_APPS_PREDICTION_ROW_VIEW = true;
+
+    public static final boolean ENABLE_TWO_SWIPE_TARGETS = true;
 }
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index c75e616..93a5679 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -366,13 +366,6 @@
     }
 
     @Override
-    public void setInsets(Rect insets) {
-        super.setInsets(insets);
-        setBackground(insets.top == 0 ? null
-                : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim));
-    }
-
-    @Override
     public LayoutParams generateLayoutParams(AttributeSet attrs) {
         return new LayoutParams(getContext(), attrs);
     }
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 9732261..a59b899 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -369,9 +369,9 @@
                 return new FixedSizeEmptyDrawable(iconSize);
             }
             ShortcutInfoCompat si = (ShortcutInfoCompat) obj;
-            Bitmap badge = LauncherIcons.getShortcutInfoBadge(si, appState.getIconCache())
-                    .iconBitmap;
-
+            LauncherIcons li = LauncherIcons.obtain(appState.getContext());
+            Bitmap badge = li.getShortcutInfoBadge(si, appState.getIconCache()).iconBitmap;
+            li.recycle();
             float badgeSize = mLauncher.getResources().getDimension(R.dimen.profile_badge_size);
             float insetFraction = (iconSize - badgeSize) / iconSize;
             return new InsetDrawable(new FastBitmapDrawable(badge),
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 64f96d5..8abafb0 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
 import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
 
 import android.animation.Animator;
@@ -929,7 +930,12 @@
         int centeredTop = centerY - height / 2;
 
         // We need to bound the folder to the currently visible workspace area
-        mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
+        if (mLauncher.isInState(OVERVIEW)) {
+            mLauncher.getDragLayer().getDescendantRectRelativeToSelf(mLauncher.getOverviewPanel(),
+                    sTempRect);
+        } else {
+            mLauncher.getWorkspace().getPageAreaRelativeToDragLayer(sTempRect);
+        }
         int left = Math.min(Math.max(sTempRect.left, centeredLeft),
                 sTempRect.right- width);
         int top = Math.min(Math.max(sTempRect.top, centeredTop),
diff --git a/src/com/android/launcher3/graphics/LauncherIcons.java b/src/com/android/launcher3/graphics/LauncherIcons.java
index 0c9f4d9..34fc921 100644
--- a/src/com/android/launcher3/graphics/LauncherIcons.java
+++ b/src/com/android/launcher3/graphics/LauncherIcons.java
@@ -356,10 +356,11 @@
         return result;
     }
 
-    public static ItemInfoWithIcon getShortcutInfoBadge(
-            ShortcutInfoCompat shortcutInfo, IconCache cache) {
+    public ItemInfoWithIcon getShortcutInfoBadge(ShortcutInfoCompat shortcutInfo, IconCache cache) {
         ComponentName cn = shortcutInfo.getActivity();
-        if (cn != null) {
+        String badgePkg = shortcutInfo.getBadgePackage(mContext);
+        boolean hasBadgePkgSet = !badgePkg.equals(shortcutInfo.getPackage());
+        if (cn != null && !hasBadgePkgSet) {
             // Get the app info for the source activity.
             AppInfo appInfo = new AppInfo();
             appInfo.user = shortcutInfo.getUserHandle();
@@ -370,7 +371,7 @@
             cache.getTitleAndIcon(appInfo, false);
             return appInfo;
         } else {
-            PackageItemInfo pkgInfo = new PackageItemInfo(shortcutInfo.getPackage());
+            PackageItemInfo pkgInfo = new PackageItemInfo(badgePkg);
             cache.getTitleAndIconForApp(pkgInfo, false);
             return pkgInfo;
         }
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index 7b70df7..114b2b8 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -62,6 +62,7 @@
 
     private static NotificationListener sNotificationListenerInstance = null;
     private static NotificationsChangedListener sNotificationsChangedListener;
+    private static StatusBarNotificationsChangedListener sStatusBarNotificationsChangedListener;
     private static boolean sIsConnected;
     private static boolean sIsCreated;
 
@@ -180,10 +181,19 @@
         }
     }
 
+    public static void setStatusBarNotificationsChangedListener
+            (StatusBarNotificationsChangedListener listener) {
+        sStatusBarNotificationsChangedListener = listener;
+    }
+
     public static void removeNotificationsChangedListener() {
         sNotificationsChangedListener = null;
     }
 
+    public static void removeStatusBarNotificationsChangedListener() {
+        sStatusBarNotificationsChangedListener = null;
+    }
+
     @Override
     public void onListenerConnected() {
         super.onListenerConnected();
@@ -205,7 +215,10 @@
     public void onNotificationPosted(final StatusBarNotification sbn) {
         super.onNotificationPosted(sbn);
         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, new NotificationPostedMsg(sbn))
-                .sendToTarget();
+            .sendToTarget();
+        if (sStatusBarNotificationsChangedListener != null) {
+            sStatusBarNotificationsChangedListener.onNotificationPosted(sbn);
+        }
     }
 
     /**
@@ -227,10 +240,13 @@
     public void onNotificationRemoved(final StatusBarNotification sbn) {
         super.onNotificationRemoved(sbn);
         Pair<PackageUserKey, NotificationKeyData> packageUserKeyAndNotificationKey
-                = new Pair<>(PackageUserKey.fromNotification(sbn),
-                        NotificationKeyData.fromNotification(sbn));
+            = new Pair<>(PackageUserKey.fromNotification(sbn),
+            NotificationKeyData.fromNotification(sbn));
         mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
-                .sendToTarget();
+            .sendToTarget();
+        if (sStatusBarNotificationsChangedListener != null) {
+            sStatusBarNotificationsChangedListener.onNotificationRemoved(sbn);
+        }
 
         NotificationGroup notificationGroup = mNotificationGroupMap.get(sbn.getGroupKey());
         if (notificationGroup != null) {
@@ -318,4 +334,9 @@
                 NotificationKeyData notificationKey);
         void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
     }
+
+    public interface StatusBarNotificationsChangedListener {
+        void onNotificationPosted(StatusBarNotification sbn);
+        void onNotificationRemoved(StatusBarNotification sbn);
+    }
 }
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index cedf291..f90abb4 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -68,7 +68,6 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.TriangleShape;
 import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.notification.NotificationInfo;
@@ -650,7 +649,7 @@
                 // reopen the container to ensure measurements etc. all work out. While this could
                 // be quite janky, in practice the user would typically see a small flicker as the
                 // animation restarts partway through, and this is a very rare edge case anyway.
-                ((PopupContainerWithArrow) getParent()).close(false);
+                close(false);
                 PopupContainerWithArrow.showForIcon(mOriginalIcon);
             }
         } else if (onClickListener == null && widgetsView != null) {
@@ -658,7 +657,7 @@
             if (mSystemShortcutContainer != this) {
                 mSystemShortcutContainer.removeView(widgetsView);
             } else {
-                ((PopupContainerWithArrow) getParent()).close(false);
+                close(false);
                 PopupContainerWithArrow.showForIcon(mOriginalIcon);
             }
         }
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index 42aa12b..83cbf59 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -82,7 +82,7 @@
                 @Override
                 public void onClick(View view) {
                     Rect sourceBounds = launcher.getViewBounds(view);
-                    Bundle opts = launcher.getActivityLaunchOptions(view);
+                    Bundle opts = launcher.getActivityLaunchOptions(view, true);
                     InfoDropTarget.startDetailsActivityForInfo(itemInfo, launcher, sourceBounds, opts);
                     launcher.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP,
                             ControlType.APPINFO_TARGET, view);
diff --git a/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java b/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java
index 9c91c87..325777d 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutInfoCompat.java
@@ -18,11 +18,14 @@
 
 import android.annotation.TargetApi;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ShortcutInfo;
 import android.os.Build;
 import android.os.UserHandle;
 
+import com.android.launcher3.R;
+
 /**
  * Wrapper class for {@link android.content.pm.ShortcutInfo}, representing deep shortcuts into apps.
  *
@@ -31,8 +34,8 @@
 @TargetApi(Build.VERSION_CODES.N)
 public class ShortcutInfoCompat {
     private static final String INTENT_CATEGORY = "com.android.launcher3.DEEP_SHORTCUT";
+    private static final String EXTRA_BADGEPKG = "badge_package";
     public static final String EXTRA_SHORTCUT_ID = "shortcut_id";
-
     private ShortcutInfo mShortcutInfo;
 
     public ShortcutInfoCompat(ShortcutInfo shortcutInfo) {
@@ -57,6 +60,15 @@
         return mShortcutInfo.getPackage();
     }
 
+    public String getBadgePackage(Context context) {
+        String whitelistedPkg = context.getString(R.string.shortcutinfocompat_badgepkg_whitelist);
+        if (whitelistedPkg.equals(getPackage())
+                && mShortcutInfo.getExtras().containsKey(EXTRA_BADGEPKG)) {
+            return mShortcutInfo.getExtras().getString(EXTRA_BADGEPKG);
+        }
+        return getPackage();
+    }
+
     public String getId() {
         return mShortcutInfo.getId();
     }
diff --git a/src/com/android/launcher3/touch/SwipeDetector.java b/src/com/android/launcher3/touch/SwipeDetector.java
index ff5f64c..df34885 100644
--- a/src/com/android/launcher3/touch/SwipeDetector.java
+++ b/src/com/android/launcher3/touch/SwipeDetector.java
@@ -286,6 +286,16 @@
         }
     }
 
+    /**
+     * Returns if the start drag was towards the positive direction or negative.
+     *
+     * @see #setDetectableScrollConditions(int, boolean)
+     * @see #DIRECTION_BOTH
+     */
+    public boolean wasInitialTouchPositive() {
+        return mSubtractDisplacement < 0;
+    }
+
     private boolean reportDragging() {
         if (mDisplacement != mLastDisplacement) {
             if (DBG) {
diff --git a/src/com/android/launcher3/util/VerticalSwipeController.java b/src/com/android/launcher3/util/VerticalSwipeController.java
index 7b1632c..5d47cd2 100644
--- a/src/com/android/launcher3/util/VerticalSwipeController.java
+++ b/src/com/android/launcher3/util/VerticalSwipeController.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.touch.SwipeDetector;
+import com.android.launcher3.touch.SwipeDetector.Direction;
 
 import java.util.ArrayList;
 
@@ -56,6 +57,7 @@
     protected final Launcher mLauncher;
     private final SwipeDetector mDetector;
     private final LauncherState mBaseState;
+    private final LauncherState mTargetState;
 
     private boolean mNoIntercept;
 
@@ -66,12 +68,18 @@
     // Ratio of transition process [0, 1] to drag displacement (px)
     private float mProgressMultiplier;
 
-    private SpringAnimationHandler[] mSpringHandlers;
+    protected SpringAnimationHandler[] mSpringHandlers;
 
     public VerticalSwipeController(Launcher l, LauncherState baseState) {
+        this(l, baseState, ALL_APPS, SwipeDetector.VERTICAL);
+    }
+
+    public VerticalSwipeController(
+            Launcher l, LauncherState baseState, LauncherState targetState, Direction dir) {
         mLauncher = l;
-        mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL);
+        mDetector = new SwipeDetector(l, this, dir);
         mBaseState = baseState;
+        mTargetState = targetState;
     }
 
     private boolean canInterceptTouch(MotionEvent ev) {
@@ -96,7 +104,7 @@
         }
     }
 
-    private void initSprings() {
+    protected void initSprings() {
         AllAppsContainerView appsView = mLauncher.getAppsView();
 
         SpringAnimationHandler handler = appsView.getSpringAnimationHandler();
@@ -178,12 +186,13 @@
             long maxAccuracy = (long) (2 * range);
 
             // Build current animation
-            mToState = mLauncher.isInState(ALL_APPS) ? mBaseState : ALL_APPS;
+            mToState = mLauncher.isInState(mTargetState) ? mBaseState : mTargetState;
             mCurrentAnimation = mLauncher.getStateManager()
                     .createAnimationToNewWorkspace(mToState, maxAccuracy);
             mCurrentAnimation.getTarget().addListener(this);
             mStartProgress = 0;
-            mProgressMultiplier = (mLauncher.isInState(ALL_APPS) ? 1 : -1) / range;
+            mProgressMultiplier =
+                    (mLauncher.isInState(mTargetState) ^ isTransitionFlipped() ? 1 : -1) / range;
             mCurrentAnimation.dispatchOnStart();
         } else {
             mCurrentAnimation.pause();
@@ -195,7 +204,11 @@
         }
     }
 
-    private float getShiftRange() {
+    protected boolean isTransitionFlipped() {
+        return false;
+    }
+
+    protected float getShiftRange() {
         return mLauncher.getAllAppsController().getShiftRange();
     }
 
@@ -213,27 +226,25 @@
         final float progress = mCurrentAnimation.getProgressFraction();
 
         if (fling) {
-            if (velocity < 0) {
-                targetState = ALL_APPS;
-                animationDuration = SwipeDetector.calculateDuration(velocity,
-                        mToState == ALL_APPS ? (1 - progress) : progress);
+            if (velocity < 0 ^ isTransitionFlipped()) {
+                targetState = mTargetState;
             } else {
                 targetState = mBaseState;
-                animationDuration = SwipeDetector.calculateDuration(velocity,
-                        mToState == ALL_APPS ? progress : (1 - progress));
             }
+            animationDuration = SwipeDetector.calculateDuration(velocity,
+                    mToState == targetState ? (1 - progress) : progress);
             // snap to top or bottom using the release velocity
         } else {
             if (progress > SUCCESS_TRANSITION_PROGRESS) {
                 targetState = mToState;
                 animationDuration = SwipeDetector.calculateDuration(velocity, 1 - progress);
             } else {
-                targetState = mToState == ALL_APPS ? mBaseState : ALL_APPS;
+                targetState = mToState == mTargetState ? mBaseState : mTargetState;
                 animationDuration = SwipeDetector.calculateDuration(velocity, progress);
             }
         }
 
-        if (fling && targetState == ALL_APPS) {
+        if (fling && targetState == mTargetState) {
             for (SpringAnimationHandler h : mSpringHandlers) {
                 // The icons are moving upwards, so we go to 0 from 1. (y-axis 1 is below 0.)
                 h.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewPanel.java b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewPanel.java
index 26dd68f..9a09c97 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewPanel.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewPanel.java
@@ -158,7 +158,7 @@
                 .setPackage(getContext().getPackageName());
         intent.setSourceBounds(mLauncher.getViewBounds(v));
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        getContext().startActivity(intent, mLauncher.getActivityLaunchOptions(v));
+        getContext().startActivity(intent, mLauncher.getActivityLaunchOptions(v, false));
     }
 
     @Override
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
index fc81e80..9819d71 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
@@ -18,12 +18,20 @@
 
 import static com.android.launcher3.LauncherState.OVERVIEW;
 
+import android.app.ActivityOptions;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
 import android.view.View.AccessibilityDelegate;
 
+import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.graphics.BitmapRenderer;
 import com.android.launcher3.util.TouchController;
 
@@ -56,4 +64,10 @@
         renderer.render(new Canvas(result));
         return result;
     }
+
+    public static void resetOverview(Launcher launcher) { }
+
+    public static Bundle getActivityLaunchOptions(Launcher launcher, View v) {
+        return launcher.getDefaultActivityLaunchOptions(v);
+    }
 }