Merge "Import translations. DO NOT MERGE" into ub-launcher3-master
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index ef50ac4..9069698 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index a8b91c5..22f014a 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -19,7 +19,6 @@
     android:theme="@style/HomeScreenElementTheme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_gravity="center"
     android:clipChildren="false"
     android:clipToPadding="false"
     android:alpha="0.0"
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
index fdf1adc..839d934 100644
--- a/quickstep/res/layout/task.xml
+++ b/quickstep/res/layout/task.xml
@@ -23,14 +23,14 @@
         android:id="@+id/snapshot"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_marginTop="24dp"
+        android:layout_marginTop="@dimen/task_thumbnail_top_margin"
         android:scaleType="matrix"
         android:background="@drawable/task_thumbnail_background"
         android:elevation="4dp" />
     <ImageView
         android:id="@+id/icon"
-        android:layout_width="48dp"
-        android:layout_height="48dp"
+        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"/>
 </com.android.quickstep.TaskView>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
new file mode 100644
index 0000000..4f85957
--- /dev/null
+++ b/quickstep/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<resources>
+
+    <dimen name="task_thumbnail_top_margin">24dp</dimen>
+    <dimen name="task_thumbnail_icon_size">48dp</dimen>
+
+    <dimen name="quickstep_fling_threshold_velocity">500dp</dimen>
+    <dimen name="quickstep_fling_min_velocity">250dp</dimen>
+
+</resources>
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 26f5d5b..1176034 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -29,9 +29,6 @@
  */
 public class OverviewState extends LauncherState {
 
-    // The percent to shrink the workspace during overview mode
-    public static final float SCALE_FACTOR = 0.7f;
-
     private static final int STATE_FLAGS = FLAG_SHOW_SCRIM | FLAG_MULTI_PAGE;
 
     public OverviewState(int id) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
index c490c3f..20abdc7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java
@@ -16,12 +16,17 @@
 
 package com.android.launcher3.uioverrides;
 
+import android.content.Intent;
 import android.view.View.AccessibilityDelegate;
+import android.widget.PopupMenu;
+import android.widget.Toast;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherStateManager.StateHandler;
+import com.android.launcher3.R;
 import com.android.launcher3.VerticalSwipeController;
 import com.android.launcher3.util.TouchController;
+import com.android.launcher3.widget.WidgetsFullSheet;
 
 public class UiFactory {
 
@@ -38,4 +43,29 @@
                 launcher.getAllAppsController(), launcher.getWorkspace(),
                 new RecentsViewStateController(launcher)};
     }
+
+    public static void onWorkspaceLongPress(Launcher launcher) {
+        PopupMenu menu = new PopupMenu(launcher, launcher.getWorkspace().getPageIndicator());
+        menu.getMenu().add(R.string.wallpaper_button_text).setOnMenuItemClickListener((i) -> {
+            launcher.onClickWallpaperPicker(null);
+            return true;
+        });
+        menu.getMenu().add(R.string.widget_button_text).setOnMenuItemClickListener((i) -> {
+            if (launcher.getPackageManager().isSafeMode()) {
+                Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
+            } else {
+                WidgetsFullSheet.show(launcher, true /* animated */);
+            }
+            return true;
+        });
+        if (launcher.hasSettings()) {
+            menu.getMenu().add(R.string.settings_button_text).setOnMenuItemClickListener((i) -> {
+                launcher.startActivity(new Intent(Intent.ACTION_APPLICATION_PREFERENCES)
+                        .setPackage(launcher.getPackageName())
+                        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+                return true;
+            });
+        }
+        menu.show();
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
new file mode 100644
index 0000000..1f6781e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.util.FloatProperty;
+
+/**
+ * A mutable float which allows animating the value
+ */
+public class AnimatedFloat {
+
+    public static FloatProperty<AnimatedFloat> VALUE = new FloatProperty<AnimatedFloat>("value") {
+        @Override
+        public void setValue(AnimatedFloat obj, float v) {
+            obj.updateValue(v);
+        }
+
+        @Override
+        public Float get(AnimatedFloat obj) {
+            return obj.value;
+        }
+    };
+
+    private final Runnable mUpdateCallback;
+    private ObjectAnimator mValueAnimator;
+
+    public float value;
+
+    public AnimatedFloat(Runnable updateCallback) {
+        mUpdateCallback = updateCallback;
+    }
+
+    public ObjectAnimator animateToValue(float v) {
+        if (mValueAnimator != null) {
+            mValueAnimator.cancel();
+        }
+        mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, v);
+        mValueAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (mValueAnimator == animator) {
+                    mValueAnimator = null;
+                }
+            }
+        });
+        return mValueAnimator;
+    }
+
+    /**
+     * Changes the value and calls the callback.
+     * Note that the value can be directly accessed as well to avoid notifying the callback.
+     */
+    public void updateValue(float v) {
+        if (Float.compare(v, value) != 0) {
+            value = v;
+            mUpdateCallback.run();
+        }
+    }
+
+    public ObjectAnimator getCurrentAnimation() {
+        return mValueAnimator;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/MotionEventQueue.java b/quickstep/src/com/android/quickstep/MotionEventQueue.java
new file mode 100644
index 0000000..e3c3a1b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/MotionEventQueue.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_MOVE;
+
+import android.view.Choreographer;
+import android.view.MotionEvent;
+
+import com.android.systemui.shared.system.ChoreographerCompat;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Helper class for batching input events
+ */
+public class MotionEventQueue implements Runnable {
+
+    // We use two arrays and swap the current index when one array is being consumed
+    private final EventArray[] mArrays = new EventArray[] {new EventArray(), new EventArray()};
+    private int mCurrentIndex = 0;
+
+    private final Choreographer mChoreographer;
+    private final Consumer<MotionEvent> mConsumer;
+
+    public MotionEventQueue(Choreographer choreographer, Consumer<MotionEvent> consumer) {
+        mChoreographer = choreographer;
+        mConsumer = consumer;
+    }
+
+    public void queue(MotionEvent event) {
+        synchronized (mArrays) {
+            EventArray array = mArrays[mCurrentIndex];
+            if (array.isEmpty()) {
+                ChoreographerCompat.postInputFrame(mChoreographer, this);
+            }
+
+            int eventAction = event.getAction();
+            if (eventAction == ACTION_MOVE && array.lastEventAction == ACTION_MOVE) {
+                // Replace and recycle the last event
+                array.set(array.size() - 1, event).recycle();
+            } else {
+                array.add(event);
+                array.lastEventAction = eventAction;
+            }
+        }
+    }
+
+    @Override
+    public void run() {
+        EventArray array = swapAndGetCurrentArray();
+        int size = array.size();
+        for (int i = 0; i < size; i++) {
+            MotionEvent event = array.get(i);
+            mConsumer.accept(event);
+            event.recycle();
+        }
+        array.clear();
+        array.lastEventAction = ACTION_CANCEL;
+    }
+
+    private EventArray swapAndGetCurrentArray() {
+        synchronized (mArrays) {
+            EventArray current = mArrays[mCurrentIndex];
+            mCurrentIndex = mCurrentIndex ^ 1;
+            return current;
+        }
+    }
+
+    private static class EventArray extends ArrayList<MotionEvent> {
+
+        public int lastEventAction = ACTION_CANCEL;
+
+        public EventArray() {
+            super(4);
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java
new file mode 100644
index 0000000..cca2729
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+package com.android.quickstep;
+
+import android.util.SparseArray;
+
+/**
+ * Utility class to help manage multiple callbacks based on different states.
+ */
+public class MultiStateCallback {
+
+    private final SparseArray<Runnable> mCallbacks = new SparseArray<>();
+
+    private int mState = 0;
+
+    /**
+     * Adds the provided state flags to the global state and executes any callbacks as a result.
+     * @param stateFlag
+     */
+    public void setState(int stateFlag) {
+        mState = mState | stateFlag;
+
+        int count = mCallbacks.size();
+        for (int i = 0; i < count; i++) {
+            int state = mCallbacks.keyAt(i);
+
+            if ((mState & state) == state) {
+                Runnable callback = mCallbacks.valueAt(i);
+                if (callback != null) {
+                    // Set the callback to null, so that it does not run again.
+                    mCallbacks.setValueAt(i, null);
+                    callback.run();
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the callbacks to be run when the provided states are enabled.
+     * The callback is only run once.
+     */
+    public void addCallback(int stateMask, Runnable callback) {
+        mCallbacks.put(stateMask, callback);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
index 1a8ae2a..af82fe9 100644
--- a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -20,56 +20,52 @@
 import android.animation.RectEvaluator;
 import android.annotation.TargetApi;
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityOptions;
+import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.os.Build;
+import android.os.Handler;
 import android.os.UserHandle;
-import android.support.annotation.BinderThread;
 import android.support.annotation.UiThread;
-import android.util.FloatProperty;
-import android.view.Choreographer;
-import android.view.Choreographer.FrameCallback;
 import android.view.View;
-import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnPreDrawListener;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.Hotseat;
 import com.android.launcher3.Launcher;
 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.OverviewState;
+import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.Task;
 import com.android.systemui.shared.recents.model.Task.TaskKey;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
-import com.android.systemui.shared.system.BackgroundExecutor;
 import com.android.systemui.shared.system.WindowManagerWrapper;
 
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-
 @TargetApi(Build.VERSION_CODES.O)
-public class NavBarSwipeInteractionHandler extends InternalStateHandler implements FrameCallback {
+public class NavBarSwipeInteractionHandler extends InternalStateHandler {
 
-    private static FloatProperty<NavBarSwipeInteractionHandler> SHIFT =
-            new FloatProperty<NavBarSwipeInteractionHandler>("currentShift") {
-        @Override
-        public void setValue(NavBarSwipeInteractionHandler handler, float v) {
-            handler.setShift(v);
-        }
+    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;
 
-        @Override
-        public Float get(NavBarSwipeInteractionHandler handler) {
-            return handler.mCurrentShift;
-        }
-    };
+    private static final long RECENTS_VIEW_VISIBILITY_DELAY = 120;
+    private static final long RECENTS_VIEW_VISIBILITY_DURATION = 150;
+    private static final long DEFAULT_SWIPE_DURATION = 200;
 
-    // The following constants need to be scaled based on density. The scaled versions will be
-    // assigned to the corresponding member variables below.
-    private static final int FLING_THRESHOLD_VELOCITY = 500;
-    private static final int MIN_FLING_VELOCITY = 250;
+    // Ideal velocity for a smooth transition
+    private static final float PIXEL_PER_MS = 2f;
 
     private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
 
@@ -79,47 +75,102 @@
     private final Rect mCurrentRect = new Rect();
     private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
 
-    private final Bitmap mTaskSnapshot;
-    private final int mRunningTaskId;
-    private Future<RecentsTaskLoadPlan> mFutureLoadPlan;
-
-    private Launcher mLauncher;
-    private Choreographer mChoreographer;
-    private SnapshotDragView mDragView;
-    private RecentsView mRecentsView;
-    private Hotseat mHotseat;
-
-    private float mStartDelta;
-    private float mLastDelta;
-
     // Shift in the range of [0, 1].
     // 0 => preview snapShot is completely visible, and hotseat is completely translated down
     // 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
     // visible.
-    private float mCurrentShift;
+    private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
 
-    // These are updated on the binder thread, and eventually picked up on doFrame
-    private volatile float mCurrentDisplacement;
-    private volatile float mEndVelocity;
-    private volatile boolean mTouchEnded = false;
+    // Activity multiplier in the range of [0, 1]. When the activity becomes visible, this is
+    // animated to 1, so allow for a smooth transition.
+    private final AnimatedFloat mActivityMultiplier = new AnimatedFloat(this::updateFinalShift);
 
-    NavBarSwipeInteractionHandler(Bitmap taskSnapShot, RunningTaskInfo runningTaskInfo) {
-        mTaskSnapshot = taskSnapShot;
+    private final int mRunningTaskId;
+    private final Context mContext;
+
+    private final MultiStateCallback mStateCallback;
+
+    private Launcher mLauncher;
+    private SnapshotDragView mDragView;
+    private RecentsView mRecentsView;
+    private Hotseat mHotseat;
+    private RecentsTaskLoadPlan mLoadPlan;
+
+    private boolean mLauncherReady;
+    private boolean mTouchEndHandled;
+    private float mCurrentDisplacement;
+
+    private Bitmap mTaskSnapshot;
+
+    NavBarSwipeInteractionHandler(RunningTaskInfo runningTaskInfo, Context context) {
         mRunningTaskId = runningTaskInfo.id;
+        mContext = context;
         WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
+
+        // 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,
+                this::onAnimationToLauncherComplete);
+        mStateCallback.addCallback(STATE_LAUNCHER_READY | STATE_SCALED_SNAPSHOT_APP,
+                this::cleanupLauncher);
+    }
+
+    private void onLauncherReady() {
+        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(DEFAULT_SWIPE_DURATION,
+                Math.max((long) (-mCurrentDisplacement / PIXEL_PER_MS), 0));
+        if (mCurrentShift.getCurrentAnimation() != null) {
+            ObjectAnimator anim = mCurrentShift.getCurrentAnimation();
+            long theirDuration = anim.getDuration() - anim.getCurrentPlayTime();
+
+            // TODO: Find a better heuristic
+            duration = (duration + theirDuration) / 2;
+        }
+        ObjectAnimator anim = mActivityMultiplier.animateToValue(1)
+                .setDuration(duration);
+        anim.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE);
+            }
+        });
+        anim.start();
+    }
+
+    public void setTaskSnapshot(Bitmap taskSnapshot) {
+        mTaskSnapshot = taskSnapshot;
     }
 
     @Override
     public void onLauncherResume() {
-        mStartDelta = mCurrentDisplacement;
-        mLastDelta = mStartDelta;
-        mChoreographer = Choreographer.getInstance();
-
-        scheduleNextFrame();
+        TraceHelper.partitionSection("TouchInt", "Launcher On resume");
+        mDragView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                mDragView.getViewTreeObserver().removeOnPreDrawListener(this);
+                mStateCallback.setState(STATE_LAUNCHER_READY);
+                TraceHelper.partitionSection("TouchInt", "Launcher drawn");
+                return true;
+            }
+        });
     }
 
     @Override
-    public void onCreate(Launcher launcher) {
+    protected void init(Launcher launcher, boolean alreadyOnHome) {
+        AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
+        launcher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
+
         mLauncher = launcher;
         mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
         mLauncher.getDragLayer().addView(mDragView);
@@ -130,154 +181,113 @@
 
         // Optimization
         mLauncher.getAppsView().setVisibility(View.GONE);
-
-        // Launch overview
-        mRecentsView.update(consumeLastLoadPlan());
-        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, false /* animate */);
+        mRecentsView.setVisibility(View.GONE);
+        TraceHelper.partitionSection("TouchInt", "Launcher on new intent");
     }
 
-    @Override
-    public void onNewIntent(Launcher launcher, boolean alreadyOnHome) {
-        mLauncher = launcher;
-        mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
-        mLauncher.getDragLayer().addView(mDragView);
-        mDragView.setPivotX(0);
-        mDragView.setPivotY(0);
-        mRecentsView = mLauncher.getOverviewPanel();
-        mHotseat = mLauncher.getHotseat();
-
-        // Optimization
-        mLauncher.getAppsView().setVisibility(View.GONE);
-
-        // Launch overview, animate if already on home
-        mRecentsView.update(consumeLastLoadPlan());
-        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
-    }
-
-    /**
-     * This is updated on the binder thread and is picked up on the UI thread during the next
-     * scheduled frame.
-     * TODO: Instead of continuously scheduling frames, post the motion events to UI thread
-     * (can ignore all continuous move events until the last move).
-     */
-    @BinderThread
+    @UiThread
     public void updateDisplacement(float displacement) {
         mCurrentDisplacement = displacement;
+        executeFrameUpdate();
     }
 
-    @BinderThread
-    public void endTouch(float endVelocity) {
-        mEndVelocity = endVelocity;
-        mTouchEnded = true;
-    }
-
-    @UiThread
-    private void scheduleNextFrame() {
-        if (!mTouchEnded) {
-            mChoreographer.postFrameCallback(this);
-        } else {
-            animateToFinalShift();
+    private void executeFrameUpdate() {
+        if (mLauncherReady) {
+            final float displacement = -mCurrentDisplacement;
+            int hotseatHeight = mHotseat.getHeight();
+            float translation = Utilities.boundToRange(displacement, 0, hotseatHeight);
+            float shift = hotseatHeight == 0 ? 0 : translation / hotseatHeight;
+            mCurrentShift.updateValue(shift);
         }
     }
 
-    @Override
-    public void doFrame(long l) {
-        mLastDelta = mCurrentDisplacement;
-
-        float translation = Utilities.boundToRange(mStartDelta - mLastDelta, 0,
-                mHotseat.getHeight());
-        int hotseatHeight = mHotseat.getHeight();
-        float shift = hotseatHeight == 0 ? 0 : translation / hotseatHeight;
-        setShift(shift);
-        scheduleNextFrame();
-    }
-
     @UiThread
-    private void setShift(float shift) {
+    private void updateFinalShift() {
+        if (!mLauncherReady) {
+            return;
+        }
+
         if (mTargetRect.isEmpty()) {
             DragLayer dl = mLauncher.getDragLayer();
-
-            // Init target rect.
-            View targetView = ((ViewGroup) mRecentsView.getChildAt(0)).getChildAt(0);
-            dl.getViewRectRelativeToSelf(targetView, mTargetRect);
-            mTargetRect.right = mTargetRect.left + mTargetRect.width();
-            mTargetRect.bottom = mTargetRect.top + mTargetRect.height();
             mSourceRect.set(0, 0, dl.getWidth(), dl.getHeight());
+            Rect targetPadding = RecentsView.getPadding(mLauncher);
+            Rect insets = dl.getInsets();
+            mTargetRect.set(
+                    targetPadding.left + insets.left,
+                    targetPadding.top + insets.top,
+                    mSourceRect.right - targetPadding.right - insets.right,
+                    mSourceRect.bottom - targetPadding.bottom - insets.bottom);
+            mTargetRect.top += mLauncher.getResources()
+                    .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
         }
 
-        if (!mSourceRect.isEmpty()) {
-            mCurrentShift = shift;
-            int hotseatHeight = mHotseat.getHeight();
-            mHotseat.setTranslationY((1 - shift) * hotseatHeight);
+        float shift = mCurrentShift.value * mActivityMultiplier.value;
+        int hotseatHeight = mHotseat.getHeight();
 
-            mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
+        mHotseat.setTranslationY((1 - shift) * hotseatHeight);
 
-            float scale = (float) mCurrentRect.width() / mSourceRect.width();
-            mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
-            mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
-            mDragView.setScaleX(scale);
-            mDragView.setScaleY(scale);
-            mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
-            mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
-        }
-    }
+        mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
 
-    void setLastLoadPlan(Future<RecentsTaskLoadPlan> futureLoadPlan) {
-        if (mFutureLoadPlan != null) {
-            mFutureLoadPlan.cancel(true);
-        }
-        mFutureLoadPlan = futureLoadPlan;
-    }
-
-    private RecentsTaskLoadPlan consumeLastLoadPlan() {
-        try {
-            if (mFutureLoadPlan != null) {
-                return mFutureLoadPlan.get();
-            }
-        } catch (InterruptedException | ExecutionException e) {
-            e.printStackTrace();
-        } finally {
-            mFutureLoadPlan = null;
-        }
-        return null;
+        float scale = (float) mCurrentRect.width() / mSourceRect.width();
+        mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
+        mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
+        mDragView.setScaleX(scale);
+        mDragView.setScaleY(scale);
+        mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
+        mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
     }
 
     @UiThread
-    private void animateToFinalShift() {
-        float flingThreshold = Utilities.pxFromDp(FLING_THRESHOLD_VELOCITY,
-                    mLauncher.getResources().getDisplayMetrics());
-        boolean isFling = Math.abs(mEndVelocity) > flingThreshold;
+    public void setRecentsTaskLoadPlan(RecentsTaskLoadPlan loadPlan) {
+        mLoadPlan = loadPlan;
+        mStateCallback.setState(STATE_LOAD_PLAN_READY);
+    }
 
-        long duration = 200;
+    private void setTaskPlanToUi() {
+        mRecentsView.update(mLoadPlan);
+        mRecentsView.setVisibility(View.VISIBLE);
+
+        // Animate alpha
+        mRecentsView.setAlpha(0);
+        mRecentsView.animate().alpha(1).setDuration(RECENTS_VIEW_VISIBILITY_DURATION)
+                .withEndAction(() -> mStateCallback.setState(STATE_RECENTS_FULLY_VISIBLE));
+    }
+
+    @UiThread
+    public void endTouch(float endVelocity) {
+        if (mTouchEndHandled) {
+            return;
+        }
+        mTouchEndHandled = true;
+
+        Resources res = mContext.getResources();
+        float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity);
+        boolean isFling = Math.abs(endVelocity) > flingThreshold;
+
+        long duration = DEFAULT_SWIPE_DURATION;
         final float endShift;
         if (!isFling) {
-            endShift = mCurrentShift >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
+            endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0;
         } else {
-            endShift = mEndVelocity < 0 ? 1 : 0;
-            float minFlingVelocity = Utilities.pxFromDp(MIN_FLING_VELOCITY,
-                    mLauncher.getResources().getDisplayMetrics());
-            if (Math.abs(mEndVelocity) > minFlingVelocity) {
-                float distanceToTravel = (endShift - mCurrentShift) * mHotseat.getHeight();
+            endShift = endVelocity < 0 ? 1 : 0;
+            float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity);
+            if (Math.abs(endVelocity) > minFlingVelocity && mLauncherReady) {
+                float distanceToTravel = (endShift - mCurrentShift.value) * mHotseat.getHeight();
 
                 // we want the page's snap velocity to approximately match the velocity at
                 // which the user flings, so we scale the duration by a value near to the
-                // derivative of the scroll interpolator at zero, ie. 5. We use 4 to make
-                // it a little slower.
-                duration = 4 * Math.round(1000 * Math.abs(distanceToTravel / mEndVelocity));
+                // derivative of the scroll interpolator at zero, ie. 5.
+                duration = 5 * Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
             }
         }
 
-        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHIFT, endShift)
-                .setDuration(duration);
+        ObjectAnimator anim = mCurrentShift.animateToValue(endShift).setDuration(duration);
         anim.setInterpolator(Interpolators.SCROLL);
         anim.addListener(new AnimationSuccessListener() {
             @Override
             public void onAnimationSuccess(Animator animator) {
-                if (Float.compare(mCurrentShift, 0) == 0) {
-                    resumeLastTask();
-                } else {
-                    mDragView.close(false);
-                }
+                mStateCallback.setState((Float.compare(mCurrentShift.value, 0) == 0)
+                        ? STATE_SCALED_SNAPSHOT_APP : STATE_SCALED_SNAPSHOT_RECENTS);
             }
         });
         anim.start();
@@ -285,12 +295,30 @@
 
     @UiThread
     private void resumeLastTask() {
+        TaskKey key = null;
+        if (mLoadPlan != null) {
+            Task task = mLoadPlan.getTaskStack().findTaskWithId(mRunningTaskId);
+            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);
+    }
+
+    private void cleanupLauncher() {
         // TODO: These should be done as part of ActivityOptions#OnAnimationStarted
         mHotseat.setTranslationY(0);
         mLauncher.setOnResumeCallback(() -> mDragView.close(false));
+    }
 
-        // TODO: For now, assume that the task stack will have loaded in the bg, will update
-        // the lib api later for direct call
-        mRecentsView.launchTaskWithId(mRunningTaskId);
+    private void onAnimationToLauncherComplete() {
+        mDragView.close(false);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index a0340b6..f92d773 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -22,6 +22,7 @@
 import android.widget.ArrayAdapter;
 
 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.recents.model.Task;
 
@@ -37,7 +38,8 @@
         super.onCreate(savedInstanceState);
 
         RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(this);
-        plan.preloadPlan(new RecentsTaskLoader(this, 1, 1, 0), -1, UserHandle.myUserId());
+        plan.preloadPlan(new PreloadOptions(), new RecentsTaskLoader(this, 1, 1, 0), -1,
+                UserHandle.myUserId());
 
         mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
         mAdapter.addAll(plan.getTaskStack().getTasks());
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 528b11d..ba88f99 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -16,10 +16,12 @@
 
 package com.android.quickstep;
 
+import android.animation.TimeInterpolator;
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
+import android.view.View;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
@@ -41,6 +43,12 @@
  */
 public class RecentsView extends PagedView {
 
+    /** Designates how "curvy" the carousel is from 0 to 1, where 0 is a straight line. */
+    private 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
+        = x -> (float) (1 - Math.sqrt(1 - Math.pow(x, 2)));
+
     private boolean mOverviewStateEnabled;
     private boolean mTaskStackListenerRegistered;
 
@@ -69,23 +77,14 @@
         super(context, attrs, defStyleAttr);
         setWillNotDraw(false);
         setPageSpacing((int) getResources().getDimension(R.dimen.recents_page_spacing));
+        enableFreeScroll(true);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        // TODO: These are rough calculations which currently use the stable insets
-        DeviceProfile profile = Launcher.getLauncher(getContext()).getDeviceProfile();
-        Rect stableInsets = new Rect();
-        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
-        Rect padding = profile.getWorkspacePadding(null);
-        float taskWidth = profile.getCurrentWidth() - stableInsets.left - stableInsets.right;
-        float taskHeight = profile.getCurrentHeight() - stableInsets.top - stableInsets.bottom;
-        float overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
-                - stableInsets.top;
-        float overviewWidth = taskWidth * overviewHeight / taskHeight;
-        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
+        Rect padding = getPadding(Launcher.getLauncher(getContext()));
         setPadding(padding.left, padding.top, padding.right, padding.bottom);
     }
 
@@ -166,4 +165,53 @@
             mTaskStackListenerRegistered = registerStackListener;
         }
     }
+
+    public static Rect getPadding(Launcher launcher) {
+        DeviceProfile profile = launcher.getDeviceProfile();
+        Rect stableInsets = new Rect();
+        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
+        Rect padding = profile.getWorkspacePadding(null);
+        float taskWidth = profile.getCurrentWidth() - stableInsets.left - stableInsets.right;
+        float taskHeight = profile.getCurrentHeight() - stableInsets.top - stableInsets.bottom;
+        float overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
+                - stableInsets.top;
+        float overviewWidth = taskWidth * overviewHeight / taskHeight;
+        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
+        return padding;
+    }
+
+    @Override
+    public void scrollTo(int x, int y) {
+        super.scrollTo(x, y);
+        updateCurveProperties();
+    }
+
+    /**
+     * Scales and adjusts translation of adjacent pages as if on a curved carousel.
+     */
+    private void updateCurveProperties() {
+        if (getPageCount() == 0 || getPageAt(0).getMeasuredWidth() == 0) {
+            return;
+        }
+        final int halfScreenWidth = getMeasuredWidth() / 2;
+        final int screenCenter = halfScreenWidth + getScrollX();
+        final int pageSpacing = getResources().getDimensionPixelSize(R.dimen.recents_page_spacing);
+        final int pageCount = getPageCount();
+        for (int i = 0; i < pageCount; i++) {
+            View page = getPageAt(i);
+            int pageWidth = page.getMeasuredWidth();
+            int halfPageWidth = pageWidth / 2;
+            int pageCenter = page.getLeft() + halfPageWidth;
+            float distanceFromScreenCenter = Math.abs(pageCenter - screenCenter);
+            float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
+            float linearInterpolation = Math.min(1, distanceFromScreenCenter / distanceToReachEdge);
+            float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
+            float scale = 1 - curveInterpolation * CURVE_FACTOR;
+            page.setScaleX(scale);
+            page.setScaleY(scale);
+            // Make sure the biggest card (i.e. the one in front) shows on top of the adjacent ones.
+            page.setTranslationZ(scale);
+            page.setTranslationX((screenCenter - pageCenter) * curveInterpolation * CURVE_FACTOR);
+        }
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 55d22e0..6e8bbeb 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -50,7 +50,7 @@
     protected Paint mBgFillPaint = new Paint();
     protected BitmapShader mBitmapShader;
 
-    private float mDimAlpha;
+    private float mDimAlpha = 1f;
     private LightingColorFilter mLightingColorFilter = new LightingColorFilter(Color.WHITE, 0);
 
     public TaskThumbnailView(Context context) {
@@ -64,7 +64,6 @@
     public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         setWillNotDraw(false);
-        setDimAlpha(1f);
         setClipToOutline(true);
     }
 
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
index f6408a8..029afd6 100644
--- a/quickstep/src/com/android/quickstep/TaskView.java
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -55,7 +55,6 @@
 
     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setWillNotDraw(false);
         setOnClickListener((view) -> {
             launchTask(true /* animate */);
         });
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 1218176..55fd448 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -17,8 +17,6 @@
 
 import static android.view.MotionEvent.INVALID_POINTER_ID;
 
-import static com.android.launcher3.states.InternalStateHandler.EXTRA_STATE_HANDLER;
-
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.ActivityOptions;
 import android.app.Service;
@@ -31,29 +29,28 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.Log;
+import android.view.Choreographer;
 import android.view.Display;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
 import android.view.ViewConfiguration;
 import android.view.WindowManager;
 
-import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.MainThreadExecutor;
 import com.android.launcher3.R;
+import com.android.launcher3.util.TraceHelper;
 import com.android.systemui.shared.recents.IOverviewProxy;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
-import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.Options;
+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 java.util.concurrent.Future;
-
 /**
  * Service connected by system-UI for handling touch interaction.
  */
@@ -67,7 +64,7 @@
 
         @Override
         public void onMotionEvent(MotionEvent ev) {
-            handleMotionEvent(ev);
+            mEventQueue.queue(ev);
         }
 
         @Override
@@ -80,8 +77,11 @@
     private RunningTaskInfo mRunningTask;
     private Intent mHomeIntent;
     private ComponentName mLauncher;
+    private MotionEventQueue mEventQueue;
+    private MainThreadExecutor mMainThreadExecutor;
 
-
+    private int mDisplayRotation;
+    private final Point mDisplaySize = new Point();
     private final PointF mDownPos = new PointF();
     private final PointF mLastPos = new PointF();
     private int mActivePointerId = INVALID_POINTER_ID;
@@ -111,6 +111,9 @@
                     res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
             sRecentsTaskLoader.startLoader(this);
         }
+
+        mMainThreadExecutor = new MainThreadExecutor();
+        mEventQueue = new MotionEventQueue(Choreographer.getInstance(), this::handleMotionEvent);
     }
 
     @Override
@@ -129,10 +132,14 @@
         }
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_DOWN: {
+                TraceHelper.beginSection("TouchInt");
                 mActivePointerId = ev.getPointerId(0);
                 mDownPos.set(ev.getX(), ev.getY());
                 mLastPos.set(mDownPos);
                 mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+                Display display = getSystemService(WindowManager.class).getDefaultDisplay();
+                display.getRealSize(mDisplaySize);
+                mDisplayRotation = display.getRotation();
 
                 mRunningTask = mAM.getRunningTask();
                 if (mRunningTask == null || mRunningTask.topActivity.equals(mLauncher)) {
@@ -188,6 +195,7 @@
             case MotionEvent.ACTION_CANCEL:
                 // TODO: Should be different than ACTION_UP
             case MotionEvent.ACTION_UP: {
+                TraceHelper.endSection("TouchInt");
 
                 endInteraction();
                 break;
@@ -197,45 +205,39 @@
 
     private void startTouchTracking() {
         // Create the shared handler
-        mInteractionHandler = new NavBarSwipeInteractionHandler(getCurrentTaskSnapshot(),
-                mRunningTask);
+        final NavBarSwipeInteractionHandler handler =
+                new NavBarSwipeInteractionHandler(mRunningTask, this);
 
         // Preload and start the recents activity on a background thread
         final Context context = this;
-        final int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().id;
         final RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(context);
-        Future<RecentsTaskLoadPlan> loadPlanFuture = BackgroundExecutor.get().submit(() -> {
-            // Preload the plan
-            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
-            loadPlan.preloadPlan(loader, runningTaskId, UserHandle.myUserId());
+        final int taskId = mRunningTask.id;
+        TraceHelper.partitionSection("TouchInt", "Thershold crossed ");
 
-            // Pass the
-            Bundle extras = new Bundle();
-            extras.putBinder(EXTRA_STATE_HANDLER, mInteractionHandler);
+        BackgroundExecutor.get().submit(() -> {
+            // Get the snap shot before
+            handler.setTaskSnapshot(getCurrentTaskSnapshot());
 
-            // Start the activity
-            Intent homeIntent = new Intent(mHomeIntent);
-            homeIntent.putExtras(extras);
+            // Start the launcher activity with our custom handler
+            Intent homeIntent = handler.addToIntent(new Intent(mHomeIntent));
             startActivity(homeIntent, ActivityOptions.makeCustomAnimation(this, 0, 0).toBundle());
+            TraceHelper.partitionSection("TouchInt", "Home started");
+
             /*
             ActivityManagerWrapper.getInstance().startRecentsActivity(null, options,
                     ActivityOptions.makeCustomAnimation(this, 0, 0), UserHandle.myUserId(),
                     null, null);
              */
 
-            // Kick off loading of the plan while the activity is starting
-            Options loadOpts = new Options();
-            loadOpts.runningTaskId = runningTaskId;
-            loadOpts.loadIcons = true;
-            loadOpts.loadThumbnails = true;
-            loadOpts.numVisibleTasks = 2;
-            loadOpts.numVisibleTaskThumbnails = 2;
-            loadOpts.onlyLoadForCache = false;
-            loadOpts.onlyLoadPausedActivities = false;
-            loader.loadTasks(loadPlan, loadOpts);
-        }, loadPlan);
-
-        mInteractionHandler.setLastLoadPlan(loadPlanFuture);
+            // Preload the plan
+            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+            PreloadOptions opts = new PreloadOptions();
+            opts.loadTitles = false;
+            loadPlan.preloadPlan(opts, loader, taskId, UserHandle.myUserId());
+            // Set the load plan on UI thread
+            mMainThreadExecutor.execute(() -> handler.setRecentsTaskLoadPlan(loadPlan));
+        });
+        mInteractionHandler = handler;
     }
 
     private void endInteraction() {
@@ -243,7 +245,7 @@
             mVelocityTracker.computeCurrentVelocity(1000,
                     ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
 
-            mInteractionHandler.endTouch(mVelocityTracker.getXVelocity(mActivePointerId));
+            mInteractionHandler.endTouch(mVelocityTracker.getYVelocity(mActivePointerId));
             mInteractionHandler = null;
         }
         mVelocityTracker.recycle();
@@ -255,17 +257,17 @@
             Log.e(TAG, "Never received systemUIProxy");
             return null;
         }
-        Display display = getSystemService(WindowManager.class).getDefaultDisplay();
-        Point size = new Point();
-        display.getRealSize(size);
 
+        TraceHelper.beginSection("TaskSnapshot");
         // TODO: We are using some hardcoded layers for now, to best approximate the activity layers
         try {
-            return mISystemUiProxy.screenshot(new Rect(), size.x, size.y, 0, 100000, false,
-                    display.getRotation());
+            return mISystemUiProxy.screenshot(new Rect(), mDisplaySize.x, mDisplaySize.y, 0, 100000,
+                    false, mDisplayRotation).toBitmap();
         } catch (RemoteException e) {
             Log.e(TAG, "Error capturing snapshot", e);
             return null;
+        } finally {
+            TraceHelper.endSection("TaskSnapshot");
         }
     }
 }
diff --git a/res/color/all_apps_tab_text.xml b/res/color/all_apps_tab_text.xml
new file mode 100644
index 0000000..f0c6310
--- /dev/null
+++ b/res/color/all_apps_tab_text.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?android:attr/colorAccent" android:state_selected="true"/>
+    <item android:color="?android:attr/textColorTertiary"/>
+</selector>
\ No newline at end of file
diff --git a/res/drawable/bg_notification_content.xml b/res/drawable/bg_notification_content.xml
new file mode 100644
index 0000000..cf129eb
--- /dev/null
+++ b/res/drawable/bg_notification_content.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="?attr/popupColorTertiary" />
+
+    <item android:height="3dp" android:top="0dp">
+        <shape>
+            <gradient
+                android:angle="270"
+                android:endColor="@android:color/transparent"
+                android:startColor="#33000000"
+                android:type="linear" />
+        </shape>
+    </item>
+</layer-list>
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 05f509f..c42c15c 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -35,27 +35,34 @@
         android:id="@+id/all_apps_header"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:clickable="true"
-        android:paddingTop="30dp"
+        android:paddingTop="@dimen/all_apps_header_top_padding"
+        android:clipToPadding="false"
         android:layout_below="@id/search_container_all_apps" >
 
         <com.android.launcher3.allapps.PredictionRowView
             android:id="@+id/header_content"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"/>
+            android:layout_height="wrap_content" />
 
-        <LinearLayout
-            android:id="@+id/tab_layout"
+        <include layout="@layout/all_apps_divider"
+            android:id="@+id/divider"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignBottom="@+id/tabs" />
+
+        <com.android.launcher3.views.SlidingTabStrip
+            android:id="@+id/tabs"
             android:layout_width="match_parent"
             android:layout_height="@dimen/all_apps_header_tab_height"
             android:layout_below="@id/header_content"
-            android:orientation="horizontal">
+            android:orientation="horizontal" >
             <Button
                 android:id="@+id/tab_personal"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:text="@string/all_apps_personal_tab"
+                android:textColor="@color/all_apps_tab_text"
                 android:background="?android:attr/selectableItemBackground"/>
             <Button
                 android:id="@+id/tab_work"
@@ -63,9 +70,9 @@
                 android:layout_height="match_parent"
                 android:layout_weight="1"
                 android:text="@string/all_apps_work_tab"
+                android:textColor="@color/all_apps_tab_text"
                 android:background="?android:attr/selectableItemBackground"/>
-        </LinearLayout>
-
+        </com.android.launcher3.views.SlidingTabStrip>
     </RelativeLayout>
 
     <!-- Note: we are reusing/repurposing a system attribute for search layout, because of a
diff --git a/res/layout/all_apps_tabs.xml b/res/layout/all_apps_tabs.xml
index fa1d591..54a9b88 100644
--- a/res/layout/all_apps_tabs.xml
+++ b/res/layout/all_apps_tabs.xml
@@ -25,7 +25,7 @@
     android:clipChildren="false"
     android:clipToPadding="false"
     android:descendantFocusability="afterDescendants"
-    android:paddingTop="30dp">
+    android:paddingTop="@dimen/all_apps_header_top_padding">
 
     <include layout="@layout/all_apps_rv_layout" />
 
diff --git a/res/layout/notification.xml b/res/layout/notification.xml
deleted file mode 100644
index 1eebb43..0000000
--- a/res/layout/notification.xml
+++ /dev/null
@@ -1,97 +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.
--->
-
-<com.android.launcher3.notification.NotificationItemView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/notification_view"
-    android:layout_width="@dimen/bg_popup_item_width"
-    android:layout_height="wrap_content"
-    android:theme="@style/PopupItem"
-    android:elevation="@dimen/deep_shortcuts_elevation">
-
-    <RelativeLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:clipChildren="false">
-
-        <View
-            android:id="@+id/gutter_top"
-            android:layout_width="match_parent"
-            android:layout_height="4dp"
-            android:theme="@style/PopupGutter"
-            android:visibility="gone" />
-
-        <FrameLayout
-            android:id="@+id/header"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/notification_header_height"
-            android:paddingStart="@dimen/notification_padding_start"
-            android:paddingEnd="@dimen/notification_padding_end"
-            android:background="?attr/popupColorPrimary"
-            android:elevation="@dimen/notification_elevation"
-            android:layout_below="@id/gutter_top" >
-            <TextView
-                android:id="@+id/notification_text"
-                android:layout_width="wrap_content"
-                android:layout_height="match_parent"
-                android:layout_gravity="start"
-                android:gravity="center_vertical"
-                android:text="@string/notifications_header"
-                android:textSize="@dimen/notification_header_text_size"
-                android:textColor="?android:attr/textColorPrimary" />
-            <TextView
-                android:id="@+id/notification_count"
-                android:layout_width="@dimen/notification_icon_size"
-                android:layout_height="match_parent"
-                android:layout_gravity="end"
-                android:gravity="center"
-                android:textSize="@dimen/notification_header_count_text_size"
-                android:fontFamily="sans-serif-medium"
-                android:textColor="?android:attr/textColorPrimary" />
-        </FrameLayout>
-
-        <include layout="@layout/notification_main"
-            android:id="@+id/main_view"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/notification_main_height"
-            android:layout_below="@id/header" />
-
-        <View
-            android:id="@+id/divider"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/popup_item_divider_height"
-            android:background="?attr/popupColorTertiary"
-            android:layout_below="@id/main_view"
-            android:visibility="gone" />
-
-        <include layout="@layout/notification_footer"
-            android:id="@+id/footer"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/notification_footer_height"
-            android:layout_below="@id/divider" />
-
-        <View
-            android:id="@+id/gutter_bottom"
-            android:layout_width="match_parent"
-            android:layout_height="4dp"
-            android:theme="@style/PopupGutter"
-            android:visibility="gone"
-            android:layout_below="@id/footer" />
-
-    </RelativeLayout>
-
-</com.android.launcher3.notification.NotificationItemView>
diff --git a/res/layout/notification_content.xml b/res/layout/notification_content.xml
new file mode 100644
index 0000000..d01be01
--- /dev/null
+++ b/res/layout/notification_content.xml
@@ -0,0 +1,134 @@
+<?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.
+-->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <!-- header -->
+    <FrameLayout
+        android:id="@+id/header"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/notification_header_height"
+        android:paddingEnd="@dimen/notification_padding_end"
+        android:paddingStart="@dimen/notification_padding_start">
+        <TextView
+            android:id="@+id/notification_text"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_gravity="start"
+            android:gravity="center_vertical"
+            android:text="@string/notifications_header"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="@dimen/notification_header_text_size" />
+        <TextView
+            android:id="@+id/notification_count"
+            android:layout_width="@dimen/notification_icon_size"
+            android:layout_height="match_parent"
+            android:layout_gravity="end"
+            android:fontFamily="sans-serif-medium"
+            android:gravity="center"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="@dimen/notification_header_count_text_size" />
+    </FrameLayout>
+
+    <!-- Main view -->
+    <com.android.launcher3.notification.NotificationMainView
+        android:id="@+id/main_view"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/notification_main_height"
+        android:background="@drawable/bg_notification_content"
+        android:focusable="true" >
+
+        <LinearLayout
+            android:id="@+id/text_and_background"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="?attr/popupColorPrimary"
+            android:gravity="center_vertical"
+            android:orientation="vertical"
+            android:paddingBottom="14dp"
+            android:paddingEnd="@dimen/notification_main_text_padding_end"
+            android:paddingStart="@dimen/notification_padding_start">
+            <TextView
+                android:id="@+id/title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:fontFamily="sans-serif"
+                android:lines="1"
+                android:textAlignment="viewStart"
+                android:textColor="?android:attr/textColorPrimary"
+                android:textSize="@dimen/notification_main_title_size" />
+
+            <TextView
+                android:id="@+id/text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:fontFamily="sans-serif"
+                android:lines="1"
+                android:textColor="?android:attr/textColorSecondary"
+                android:textSize="@dimen/notification_main_text_size" />
+        </LinearLayout>
+
+        <View
+            android:id="@+id/popup_item_icon"
+            android:layout_width="@dimen/notification_icon_size"
+            android:layout_height="@dimen/notification_icon_size"
+            android:layout_gravity="center_vertical|end"
+            android:layout_marginBottom="7dp"
+            android:layout_marginEnd="@dimen/notification_padding_end" />
+
+    </com.android.launcher3.notification.NotificationMainView>
+
+    <!-- Divider -->
+    <View
+        android:id="@+id/divider"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/popup_item_divider_height"
+        android:layout_below="@id/main_view"
+        android:background="?attr/popupColorTertiary" />
+
+    <!-- Footer -->
+    <com.android.launcher3.notification.NotificationFooterLayout
+        android:id="@+id/footer"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/notification_footer_height"
+        android:layout_gravity="center_vertical"
+        android:clipChildren="false">
+
+        <LinearLayout
+            android:id="@+id/icon_row"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipChildren="false"
+            android:clipToPadding="false"
+            android:gravity="end|center_vertical"
+            android:orientation="horizontal"
+            android:padding="@dimen/notification_footer_icon_row_padding"/>
+
+        <View
+            android:id="@+id/overflow"
+            android:layout_width="@dimen/horizontal_ellipsis_size"
+            android:layout_height="@dimen/horizontal_ellipsis_size"
+            android:layout_gravity="start|center_vertical"
+            android:layout_marginStart="@dimen/horizontal_ellipsis_offset"
+            android:background="@drawable/horizontal_ellipsis" />
+
+    </com.android.launcher3.notification.NotificationFooterLayout>
+</merge>
\ No newline at end of file
diff --git a/res/layout/notification_footer.xml b/res/layout/notification_footer.xml
deleted file mode 100644
index 86280e0..0000000
--- a/res/layout/notification_footer.xml
+++ /dev/null
@@ -1,46 +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.
--->
-
-
-<com.android.launcher3.notification.NotificationFooterLayout
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:elevation="@dimen/notification_elevation"
-    android:clipChildren="false"
-    android:layout_gravity="center_vertical"
-    android:background="?attr/popupColorPrimary">
-
-    <LinearLayout
-        android:id="@+id/icon_row"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:gravity="end|center_vertical"
-        android:padding="@dimen/notification_footer_icon_row_padding"
-        android:clipToPadding="false"
-        android:clipChildren="false"/>
-
-    <View
-        android:id="@+id/overflow"
-        android:layout_width="@dimen/horizontal_ellipsis_size"
-        android:layout_height="@dimen/horizontal_ellipsis_size"
-        android:background="@drawable/horizontal_ellipsis"
-        android:layout_marginStart="@dimen/horizontal_ellipsis_offset"
-        android:layout_gravity="start|center_vertical" />
-
-</com.android.launcher3.notification.NotificationFooterLayout>
-
diff --git a/res/layout/notification_gutter.xml b/res/layout/notification_gutter.xml
new file mode 100644
index 0000000..10e7f7d
--- /dev/null
+++ b/res/layout/notification_gutter.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+<View
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="4dp"
+    android:layout_marginTop="4dp"
+    android:background="@drawable/bg_notification_content" />
\ No newline at end of file
diff --git a/res/layout/notification_main.xml b/res/layout/notification_main.xml
deleted file mode 100644
index f94face..0000000
--- a/res/layout/notification_main.xml
+++ /dev/null
@@ -1,66 +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.
--->
-
-
-<com.android.launcher3.notification.NotificationMainView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:focusable="true"
-    android:elevation="@dimen/notification_elevation" >
-
-    <LinearLayout
-        android:id="@+id/text_and_background"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:gravity="center_vertical"
-        android:background="?attr/popupColorPrimary"
-        android:paddingStart="@dimen/notification_padding_start"
-        android:paddingEnd="@dimen/notification_main_text_padding_end"
-        android:paddingBottom="14dp">
-        <TextView
-            android:id="@+id/title"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:textAlignment="viewStart"
-            android:fontFamily="sans-serif"
-            android:textSize="@dimen/notification_main_title_size"
-            android:textColor="?android:attr/textColorPrimary"
-            android:lines="1"
-            android:ellipsize="end" />
-
-        <TextView
-            android:id="@+id/text"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:fontFamily="sans-serif"
-            android:textSize="@dimen/notification_main_text_size"
-            android:textColor="?android:attr/textColorSecondary"
-            android:lines="1"
-            android:ellipsize="end" />
-    </LinearLayout>
-
-    <View
-        android:id="@+id/popup_item_icon"
-        android:layout_width="@dimen/notification_icon_size"
-        android:layout_height="@dimen/notification_icon_size"
-        android:layout_marginEnd="@dimen/notification_padding_end"
-        android:layout_marginBottom="7dp"
-        android:layout_gravity="center_vertical|end" />
-
-</com.android.launcher3.notification.NotificationMainView>
-
diff --git a/res/layout/popup_container.xml b/res/layout/popup_container.xml
index 67db4a5..c737407 100644
--- a/res/layout/popup_container.xml
+++ b/res/layout/popup_container.xml
@@ -19,11 +19,8 @@
     android:id="@+id/deep_shortcuts_container"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:paddingTop="4dp"
-    android:paddingBottom="4dp"
+    android:background="?attr/popupColorPrimary"
     android:clipToPadding="false"
     android:clipChildren="false"
     android:elevation="@dimen/deep_shortcuts_elevation"
-    android:orientation="vertical">
-
-</com.android.launcher3.popup.PopupContainerWithArrow>
\ No newline at end of file
+    android:orientation="vertical" />
\ No newline at end of file
diff --git a/res/layout/shortcuts_item.xml b/res/layout/shortcuts_item.xml
deleted file mode 100644
index 7cd996d..0000000
--- a/res/layout/shortcuts_item.xml
+++ /dev/null
@@ -1,39 +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.
--->
-
-<com.android.launcher3.shortcuts.ShortcutsItemView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/shortcuts_view"
-    android:layout_width="@dimen/bg_popup_item_width"
-    android:layout_height="wrap_content"
-    android:elevation="@dimen/deep_shortcuts_elevation">
-
-    <LinearLayout
-        android:id="@+id/content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-
-        <!-- The shortcuts header is added at runtime when necessary. -->
-
-        <LinearLayout
-            android:id="@+id/shortcuts"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:orientation="vertical" />
-    </LinearLayout>
-
-</com.android.launcher3.shortcuts.ShortcutsItemView>
diff --git a/res/layout/system_shortcut_icons.xml b/res/layout/system_shortcut_icons.xml
index 34d63e7..4daf469 100644
--- a/res/layout/system_shortcut_icons.xml
+++ b/res/layout/system_shortcut_icons.xml
@@ -22,6 +22,4 @@
     android:orientation="horizontal"
     android:gravity="end|center_vertical"
     android:background="?attr/popupColorSecondary"
-    android:elevation="1dp"
-    android:outlineProvider="none" />
-    <!-- We have elevation so this is drawn on top, but no outline provider to remove shadow -->
+    android:clipToPadding="true" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 94db0cc..266e0b0 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -91,6 +91,9 @@
     <dimen name="all_apps_background_canvas_height">475dp</dimen>
     <dimen name="all_apps_caret_workspace_offset">18dp</dimen>
     <dimen name="all_apps_header_tab_height">50dp</dimen>
+    <dimen name="all_apps_tabs_indicator_height">2dp</dimen>
+    <dimen name="all_apps_header_top_padding">36dp</dimen>
+    <dimen name="all_apps_prediction_row_divider_height">17dp</dimen>
 
 <!-- Search bar in All Apps -->
     <dimen name="all_apps_header_max_elevation">3dp</dimen>
@@ -180,6 +183,7 @@
     <dimen name="deep_shortcut_drag_handle_size">16dp</dimen>
     <dimen name="popup_padding_start">10dp</dimen>
     <dimen name="popup_padding_end">16dp</dimen>
+    <dimen name="popup_vertical_padding">4dp</dimen>
     <dimen name="popup_arrow_width">10dp</dimen>
     <dimen name="popup_arrow_height">8dp</dimen>
     <dimen name="popup_arrow_vertical_offset">-2dp</dimen>
@@ -227,7 +231,6 @@
     <dimen name="notification_footer_icon_size">18dp</dimen>
     <!-- notification_icon_size + notification_padding_end + 16dp padding between icon and text -->
     <dimen name="notification_main_text_padding_end">52dp</dimen>
-    <dimen name="notification_elevation">2dp</dimen>
     <dimen name="horizontal_ellipsis_size">18dp</dimen>
     <!-- arrow_horizontal_offset_start - (ellipsis_size - arrow_width) / 2 -->
     <dimen name="horizontal_ellipsis_offset">19dp</dimen>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 8129e81..8cc4743 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -136,13 +136,6 @@
     <style name="PopupItem">
         <item name="android:colorControlHighlight">?attr/popupColorTertiary</item>
     </style>
-    <style name="PopupGutter">
-        <item name="android:backgroundTintMode">multiply</item>
-        <item name="android:backgroundTint">?attr/popupColorSecondary</item>
-        <item name="android:background">@drawable/gutter_horizontal</item>
-        <item name="android:elevation">@dimen/notification_elevation</item>
-        <item name="android:outlineProvider">none</item>
-    </style>
 
     <!-- Drop targets -->
     <style name="DropTargetButtonBase">
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index d8c4efa..cfb55cc 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -53,6 +53,7 @@
 public abstract class ButtonDropTarget extends TextView
         implements DropTarget, DragController.DragListener, OnClickListener {
 
+    private static final int[] sTempCords = new int[2];
     private static final int DRAG_VIEW_DROP_DURATION = 285;
 
     private final boolean mHideParentOnDisable;
@@ -257,9 +258,9 @@
         super.getHitRect(outRect);
         outRect.bottom += mBottomDragPadding;
 
-        int[] coords = new int[2];
-        mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, coords);
-        outRect.offsetTo(coords[0], coords[1]);
+        sTempCords[0] = sTempCords[1] = 0;
+        mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords);
+        outRect.offsetTo(sTempCords[0], sTempCords[1]);
     }
 
     public Rect getIconRect(DragObject dragObject) {
diff --git a/src/com/android/launcher3/InsettableFrameLayout.java b/src/com/android/launcher3/InsettableFrameLayout.java
index be76490..60f5ca2 100644
--- a/src/com/android/launcher3/InsettableFrameLayout.java
+++ b/src/com/android/launcher3/InsettableFrameLayout.java
@@ -9,8 +9,7 @@
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
-public class InsettableFrameLayout extends FrameLayout implements
-    ViewGroup.OnHierarchyChangeListener, Insettable {
+public class InsettableFrameLayout extends FrameLayout implements Insettable {
 
     @ViewDebug.ExportedProperty(category = "launcher")
     protected Rect mInsets = new Rect();
@@ -21,7 +20,6 @@
 
     public InsettableFrameLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
-        setOnHierarchyChangeListener(this);
     }
 
     public void setFrameLayoutChildInsets(View child, Rect newInsets, Rect oldInsets) {
@@ -95,12 +93,8 @@
     }
 
     @Override
-    public void onChildViewAdded(View parent, View child) {
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
         setFrameLayoutChildInsets(child, mInsets, new Rect());
     }
-
-    @Override
-    public void onChildViewRemoved(View parent, View child) {
-    }
-
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 60c00fb..b7986da 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -60,6 +60,7 @@
 import android.content.pm.PackageManager;
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
@@ -107,7 +108,6 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.dragndrop.PinItemDragListener;
 import com.android.launcher3.dynamicui.WallpaperColorInfo;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
@@ -118,12 +118,12 @@
 import com.android.launcher3.model.ModelWriter;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pageindicators.PageIndicator;
-import com.android.launcher3.popup.BaseActionPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.states.AllAppsState;
 import com.android.launcher3.states.InternalStateHandler;
+import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -359,9 +359,27 @@
 
         mPopupDataProvider = new PopupDataProvider(this);
 
-        restoreState(savedInstanceState);
+        mRotationEnabled = getResources().getBoolean(R.bool.allow_rotation);
+        // In case we are on a device with locked rotation, we should look at preferences to check
+        // if the user has specifically allowed rotation.
+        if (!mRotationEnabled) {
+            mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext());
+            mRotationPrefChangeHandler = new RotationPrefChangeHandler();
+            mSharedPrefs.registerOnSharedPreferenceChangeListener(mRotationPrefChangeHandler);
+        }
 
-        InternalStateHandler.handleCreate(this, getIntent());
+        boolean internalStateHandled = InternalStateHandler.handleCreate(this, getIntent());
+        if (internalStateHandled) {
+            // Temporarily enable the rotation
+            mRotationEnabled = true;
+
+            if (savedInstanceState != null) {
+                // InternalStateHandler has already set the appropriate state.
+                // We dont need to do anything.
+                savedInstanceState.remove(RUNTIME_STATE);
+            }
+        }
+        restoreState(savedInstanceState);
 
         // We only load the page synchronously if the user rotates (or triggers a
         // configuration change) while launcher is in the foreground
@@ -369,10 +387,13 @@
         if (savedInstanceState != null) {
             currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
         }
+
         if (!mModel.startLoader(currentScreen)) {
-            // If we are not binding synchronously, show a fade in animation when
-            // the first page bind completes.
-            mDragLayer.setAlpha(0);
+            if (!internalStateHandled) {
+                // If we are not binding synchronously, show a fade in animation when
+                // the first page bind completes.
+                mDragLayer.setAlpha(0);
+            }
         } else {
             // Pages bound synchronously.
             mWorkspace.setCurrentPage(currentScreen);
@@ -384,20 +405,6 @@
         mDefaultKeySsb = new SpannableStringBuilder();
         Selection.setSelection(mDefaultKeySsb, 0);
 
-        mRotationEnabled = getResources().getBoolean(R.bool.allow_rotation);
-        // In case we are on a device with locked rotation, we should look at preferences to check
-        // if the user has specifically allowed rotation.
-        if (!mRotationEnabled) {
-            mRotationEnabled = Utilities.isAllowRotationPrefEnabled(getApplicationContext());
-            mRotationPrefChangeHandler = new RotationPrefChangeHandler();
-            mSharedPrefs.registerOnSharedPreferenceChangeListener(mRotationPrefChangeHandler);
-        }
-
-        if (PinItemDragListener.handleDragRequest(this, getIntent())) {
-            // Temporarily enable the rotation
-            mRotationEnabled = true;
-        }
-
         // On large interfaces, or on devices that a user has specifically enabled screen rotation,
         // we want the screen to auto-rotate based on the current orientation
         setRequestedOrientation(mRotationEnabled
@@ -767,10 +774,7 @@
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onStop();
         }
-
-        if (Utilities.ATLEAST_NOUGAT_MR1) {
-            mAppWidgetHost.stopListening();
-        }
+        mAppWidgetHost.setListenIfResumed(false);
 
         if (!mAppLaunchSuccess) {
             getUserEventDispatcher().logActionCommand(Action.Command.STOP,
@@ -787,10 +791,7 @@
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.onStart();
         }
-
-        if (Utilities.ATLEAST_NOUGAT_MR1) {
-            mAppWidgetHost.startListening();
-        }
+        mAppWidgetHost.setListenIfResumed(true);
 
         if (!isWorkspaceLoading()) {
             NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
@@ -1065,7 +1066,6 @@
 
         // Setup the drag controller (drop targets have to be added in reverse order in priority)
         mDragController.setMoveTarget(mWorkspace);
-        mDragController.addDropTarget(mWorkspace);
         mDropTargetBar.setup(mDragController);
 
         mAllAppsController.setupViews(mAppsView, mHotseat, mWorkspace);
@@ -1260,9 +1260,9 @@
                 mWorkspace.updateIconBadges(updatedBadges);
                 mAppsView.updateIconBadges(updatedBadges);
 
-                BaseActionPopup popup = BaseActionPopup.getOpen(Launcher.this);
-                if (popup instanceof PopupContainerWithArrow) {
-                    ((PopupContainerWithArrow) popup).updateNotificationHeader(updatedBadges);
+                PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(Launcher.this);
+                if (popup != null) {
+                    popup.updateNotificationHeader(updatedBadges);
                 }
             }
         };
@@ -1349,65 +1349,47 @@
         boolean shouldMoveToDefaultScreen = alreadyOnHome && isInState(NORMAL)
                 && AbstractFloatingView.getTopOpenView(this) == null;
         boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
+        boolean internalStateHandled = InternalStateHandler
+                .handleNewIntent(this, intent, alreadyOnHome);
+
         if (isActionMain) {
-            if (mWorkspace == null) {
-                // Can be cases where mWorkspace is null, this prevents a NPE
-                return;
-            }
+            if (!internalStateHandled) {
+                // Note: There should be at most one log per method call. This is enforced
+                // implicitly by using if-else statements.
+                UserEventDispatcher ued = getUserEventDispatcher();
+                AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(this);
+                if (topOpenView != null) {
+                    topOpenView.logActionCommand(Action.Command.HOME_INTENT);
+                } else if (alreadyOnHome) {
+                    Target target = newContainerTarget(mStateManager.getState().containerType);
+                    target.pageIndex = mWorkspace.getCurrentPage();
+                    ued.logActionCommand(Action.Command.HOME_INTENT, target);
+                }
 
-            // Note: There should be at most one log per method call. This is enforced implicitly
-            // by using if-else statements.
-            UserEventDispatcher ued = getUserEventDispatcher();
-            AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(this);
-            if (topOpenView != null) {
-                topOpenView.logActionCommand(Action.Command.HOME_INTENT);
-            } else if (alreadyOnHome) {
-                Target target = newContainerTarget(mStateManager.getState().containerType);
-                target.pageIndex = mWorkspace.getCurrentPage();
-                ued.logActionCommand(Action.Command.HOME_INTENT, target);
-            }
+                // In all these cases, only animate if we're already on home
+                AbstractFloatingView.closeAllOpenViews(this, alreadyOnHome);
 
-            // In all these cases, only animate if we're already on home
-            AbstractFloatingView.closeAllOpenViews(this, alreadyOnHome);
-            mStateManager.goToState(NORMAL, alreadyOnHome /* animated */);
+                mStateManager.goToState(NORMAL, alreadyOnHome /* animated */);
+
+                // Reset the apps view
+                if (!alreadyOnHome && mAppsView != null) {
+                    mAppsView.reset();
+                }
+
+                if (shouldMoveToDefaultScreen && !mWorkspace.isTouchActive()) {
+                    mWorkspace.post(mWorkspace::moveToDefaultScreen);
+                }
+            }
 
             final View v = getWindow().peekDecorView();
             if (v != null && v.getWindowToken() != null) {
                 UiThreadHelper.hideKeyboardAsync(this, v.getWindowToken());
             }
 
-            // Reset the apps view
-            if (!alreadyOnHome && mAppsView != null) {
-                mAppsView.reset();
-            }
-
             if (mLauncherCallbacks != null) {
                 mLauncherCallbacks.onHomeIntent();
             }
         }
-        PinItemDragListener.handleDragRequest(this, intent);
-
-        if (mLauncherCallbacks != null) {
-            mLauncherCallbacks.onNewIntent(intent);
-        }
-
-        // Defer moving to the default screen until after we callback to the LauncherCallbacks
-        // as slow logic in the callbacks eat into the time the scroller expects for the snapToPage
-        // animation.
-        if (isActionMain) {
-            if (shouldMoveToDefaultScreen && !mWorkspace.isTouchActive()) {
-
-                mWorkspace.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (mWorkspace != null) {
-                            mWorkspace.moveToDefaultScreen();
-                        }
-                    }
-                });
-            }
-        }
-        InternalStateHandler.handleNewIntent(this, intent, alreadyOnHome);
 
         TraceHelper.endSection("NEW_INTENT");
     }
@@ -2081,11 +2063,16 @@
             intent.setPackage(pickerPackage);
         }
 
-        intent.setSourceBounds(getViewBounds(v));
+        final Bundle launchOptions;
+        if (v != null) {
+            intent.setSourceBounds(getViewBounds(v));
+            // If there is no target package, use the default intent chooser animation
+            launchOptions = hasTargetPackage ? getActivityLaunchOptions(v) : null;
+        } else {
+            launchOptions = null;
+        }
         try {
-            startActivityForResult(intent, REQUEST_PICK_WALLPAPER,
-                    // If there is no target package, use the default intent chooser animation
-                    hasTargetPackage ? getActivityLaunchOptions(v) : null);
+            startActivityForResult(intent, REQUEST_PICK_WALLPAPER, launchOptions);
         } catch (ActivityNotFoundException e) {
             setWaitingForResult(null);
             Toast.makeText(this, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
@@ -2241,7 +2228,7 @@
                     getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
                             Action.Direction.NONE, ContainerType.WORKSPACE,
                             mWorkspace.getCurrentPage());
-                    getStateManager().goToState(OVERVIEW);
+                    UiFactory.onWorkspaceLongPress(this);
                     mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                             HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
                     return true;
@@ -2278,7 +2265,7 @@
                     getUserEventDispatcher().logActionOnContainer(Action.Touch.LONGPRESS,
                             Action.Direction.NONE, ContainerType.WORKSPACE,
                             mWorkspace.getCurrentPage());
-                    getStateManager().goToState(OVERVIEW);
+                    UiFactory.onWorkspaceLongPress(this);
                 }
                 mWorkspace.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
@@ -3181,7 +3168,7 @@
                             && mAccessibilityDelegate.performAction(focusedView,
                                     (ItemInfo) focusedView.getTag(),
                                     LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
-                        BaseActionPopup.getOpen(this).requestFocus();
+                        PopupContainerWithArrow.getOpen(this).requestFocus();
                         return true;
                     }
                     break;
diff --git a/src/com/android/launcher3/LauncherAppWidgetHost.java b/src/com/android/launcher3/LauncherAppWidgetHost.java
index 70440fa..9aa74b3 100644
--- a/src/com/android/launcher3/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/LauncherAppWidgetHost.java
@@ -16,7 +16,8 @@
 
 package com.android.launcher3;
 
-import android.app.Activity;
+import static android.app.Activity.RESULT_CANCELED;
+
 import android.appwidget.AppWidgetHost;
 import android.appwidget.AppWidgetHostView;
 import android.appwidget.AppWidgetManager;
@@ -41,12 +42,17 @@
  */
 public class LauncherAppWidgetHost extends AppWidgetHost {
 
+    private static final int FLAG_LISTENING = 1;
+    private static final int FLAG_RESUMED = 1 << 1;
+    private static final int FLAG_LISTEN_IF_RESUMED = 1 << 2;
+
     public static final int APPWIDGET_HOST_ID = 1024;
 
     private final ArrayList<ProviderChangedListener> mProviderChangeListeners = new ArrayList<>();
     private final SparseArray<LauncherAppWidgetHostView> mViews = new SparseArray<>();
 
     private final Context mContext;
+    private int mFlags = FLAG_RESUMED;
 
     public LauncherAppWidgetHost(Context context) {
         super(context, APPWIDGET_HOST_ID);
@@ -66,7 +72,7 @@
         if (FeatureFlags.GO_DISABLE_WIDGETS) {
             return;
         }
-
+        mFlags |= FLAG_LISTENING;
         try {
             super.startListening();
         } catch (Exception e) {
@@ -85,10 +91,59 @@
         if (FeatureFlags.GO_DISABLE_WIDGETS) {
             return;
         }
-
+        mFlags &= ~FLAG_LISTENING;
         super.stopListening();
     }
 
+    /**
+     * Updates the resumed state of the host.
+     * When a host is not resumed, it defers calls to startListening until host is resumed again.
+     * But if the host was already listening, it will not call stopListening.
+     *
+     * @see #setListenIfResumed(boolean)
+     */
+    public void setResumed(boolean isResumed) {
+        if (isResumed == ((mFlags & FLAG_RESUMED) != 0)) {
+            return;
+        }
+        if (isResumed) {
+            mFlags |= FLAG_RESUMED;
+            // Start listening if we were supposed to start listening on resume
+            if ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0 && (mFlags & FLAG_LISTENING) == 0) {
+                startListening();
+            }
+        } else {
+            mFlags &= ~FLAG_RESUMED;
+        }
+    }
+
+    /**
+     * Updates the listening state of the host. If the host is not resumed, startListening is
+     * deferred until next resume.
+     *
+     * @see #setResumed(boolean)
+     */
+    public void setListenIfResumed(boolean listenIfResumed) {
+        if (!Utilities.ATLEAST_NOUGAT_MR1) {
+            return;
+        }
+        if (listenIfResumed == ((mFlags & FLAG_LISTEN_IF_RESUMED) != 0)) {
+            return;
+        }
+        if (listenIfResumed) {
+            mFlags |= FLAG_LISTEN_IF_RESUMED;
+            if ((mFlags & FLAG_RESUMED) != 0) {
+                // If we are resumed, start listening immediately. Note we do not check for
+                // duplicate calls before calling startListening as startListening is safe to call
+                // multiple times.
+                startListening();
+            }
+        } else {
+            mFlags &= ~FLAG_LISTEN_IF_RESUMED;
+            stopListening();
+        }
+    }
+
     @Override
     public int allocateAppWidgetId() {
         if (FeatureFlags.GO_DISABLE_WIDGETS) {
@@ -203,12 +258,7 @@
     }
 
     private void sendActionCancelled(final BaseActivity activity, final int requestCode) {
-        new Handler().post(new Runnable() {
-            @Override
-            public void run() {
-                activity.onActivityResult(requestCode, Activity.RESULT_CANCELED, null);
-            }
-        });
+        new Handler().post(() -> activity.onActivityResult(requestCode, RESULT_CANCELED, null));
     }
 
     /**
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index 78d753a..928258f 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -45,7 +45,6 @@
     void onPause();
     void onDestroy();
     void onSaveInstanceState(Bundle outState);
-    void onNewIntent(Intent intent);
     void onActivityResult(int requestCode, int resultCode, Intent data);
     void onRequestPermissionsResult(int requestCode, String[] permissions,
             int[] grantResults);
diff --git a/src/com/android/launcher3/LauncherStateManager.java b/src/com/android/launcher3/LauncherStateManager.java
index f016e8d..1e6016b 100644
--- a/src/com/android/launcher3/LauncherStateManager.java
+++ b/src/com/android/launcher3/LauncherStateManager.java
@@ -25,7 +25,6 @@
 import android.os.Looper;
 import android.view.View;
 
-import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.anim.AnimationLayerSet;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
@@ -83,6 +82,8 @@
     private StateHandler[] mStateHandlers;
     private LauncherState mState = NORMAL;
 
+    private StateListener mStateListener;
+
     public LauncherStateManager(Launcher l) {
         mUiHandler = new Handler(Looper.getMainLooper());
         mLauncher = l;
@@ -99,6 +100,10 @@
         return mStateHandlers;
     }
 
+    public void setStateListener(StateListener stateListener) {
+        mStateListener = stateListener;
+    }
+
     /**
      * @see #goToState(LauncherState, boolean, Runnable)
      */
@@ -157,6 +162,9 @@
             for (StateHandler handler : getStateHandlers()) {
                 handler.setState(state);
             }
+            if (mStateListener != null) {
+                mStateListener.onStateSetImmediately(state);
+            }
             mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
 
             // Run any queued runnable
@@ -210,6 +218,17 @@
             public void onAnimationStart(Animator animation) {
                 // Change the internal state only when the transition actually starts
                 setState(state);
+                if (mStateListener != null) {
+                    mStateListener.onStateTransitionStart(state);
+                }
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                super.onAnimationEnd(animation);
+                if (mStateListener != null) {
+                    mStateListener.onStateTransitionComplete(mState);
+                }
             }
 
             @Override
@@ -230,6 +249,7 @@
         mState.onStateDisabled(mLauncher);
         mState = state;
         mState.onStateEnabled(mLauncher);
+        mLauncher.getAppWidgetHost().setResumed(state == LauncherState.NORMAL);
     }
 
     /**
@@ -304,4 +324,15 @@
         void setStateWithAnimation(LauncherState toState, AnimationLayerSet layerViews,
                 AnimatorSet anim, AnimationConfig config);
     }
+
+    public interface StateListener {
+
+        /**
+         * Called when the state is set without an animation.
+         */
+        void onStateSetImmediately(LauncherState state);
+
+        void onStateTransitionStart(LauncherState toState);
+        void onStateTransitionComplete(LauncherState finalState);
+    }
 }
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 4240a30..9f6efb3 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -30,7 +30,6 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
@@ -87,13 +86,13 @@
     public static final int INVALID_RESTORE_PAGE = -1001;
 
     private boolean mFreeScroll = false;
+    private boolean mSettleOnPageInFreeScroll = false;
 
     protected int mFlingThresholdVelocity;
     protected int mMinFlingVelocity;
     protected int mMinSnapVelocity;
 
     protected boolean mFirstLayout = true;
-    private int mNormalChildHeight;
 
     @ViewDebug.ExportedProperty(category = "launcher")
     protected int mCurrentPage;
@@ -166,8 +165,6 @@
     @Thunk static int REORDERING_REORDER_REPOSITION_DURATION = 300;
     private static int REORDERING_SIDE_PAGE_HOVER_TIMEOUT = 80;
 
-    private float mMinScale = 1f;
-    private boolean mUseMinScale = false;
     @Thunk View mDragView;
     private Runnable mSidePageHoverRunnable;
     @Thunk int mSidePageHoverIndex = -1;
@@ -273,12 +270,6 @@
         }
     }
 
-    public void setMinScale(float f) {
-        mMinScale = f;
-        mUseMinScale = true;
-        requestLayout();
-    }
-
     @Override
     public void setScaleX(float scaleX) {
         super.setScaleX(scaleX);
@@ -597,56 +588,9 @@
         computeScrollHelper();
     }
 
-    public static class LayoutParams extends ViewGroup.LayoutParams {
-        public boolean isFullScreenPage = false;
-
-        // If true, the start edge of the page snaps to the start edge of the viewport.
-        public boolean matchStartEdge = false;
-
-        /**
-         * {@inheritDoc}
-         */
-        public LayoutParams(int width, int height) {
-            super(width, height);
-        }
-
-        public LayoutParams(Context context, AttributeSet attrs) {
-            super(context, attrs);
-        }
-
-        public LayoutParams(ViewGroup.LayoutParams source) {
-            super(source);
-        }
-    }
-
-    @Override
-    public LayoutParams generateLayoutParams(AttributeSet attrs) {
-        return new LayoutParams(getContext(), attrs);
-    }
-
-    @Override
-    protected LayoutParams generateDefaultLayoutParams() {
-        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
-    }
-
-    @Override
-    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
-        return new LayoutParams(p);
-    }
-
-    @Override
-    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
-        return p instanceof LayoutParams;
-    }
-
-    public void addFullScreenPage(View page) {
-        LayoutParams lp = generateDefaultLayoutParams();
-        lp.isFullScreenPage = true;
-        super.addView(page, 0, lp);
-    }
-
     public int getNormalChildHeight() {
-        return mNormalChildHeight;
+        return  getViewportHeight() - getPaddingTop() - getPaddingBottom()
+                - mInsets.top - mInsets.bottom;
     }
 
     @Override
@@ -662,22 +606,7 @@
         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
-        // NOTE: We multiply by 2f to account for the fact that depending on the offset of the
-        // viewport, we can be at most one and a half screens offset once we scale down
-        DisplayMetrics dm = getResources().getDisplayMetrics();
-        int maxSize = Math.max(dm.widthPixels + mInsets.left + mInsets.right,
-                dm.heightPixels + mInsets.top + mInsets.bottom);
 
-        int parentWidthSize = (int) (2f * maxSize);
-        int parentHeightSize = (int) (2f * maxSize);
-        int scaledWidthSize, scaledHeightSize;
-        if (mUseMinScale) {
-            scaledWidthSize = (int) (parentWidthSize / mMinScale);
-            scaledHeightSize = (int) (parentHeightSize / mMinScale);
-        } else {
-            scaledWidthSize = widthSize;
-            scaledHeightSize = heightSize;
-        }
         mViewport.set(0, 0, widthSize, heightSize);
 
         if (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) {
@@ -691,71 +620,19 @@
             return;
         }
 
-        /* Allow the height to be set as WRAP_CONTENT. This allows the particular case
-         * of the All apps view on XLarge displays to not take up more space then it needs. Width
-         * is still not allowed to be set as WRAP_CONTENT since many parts of the code expect
-         * each page to have the same width.
-         */
-        final int verticalPadding = getPaddingTop() + getPaddingBottom();
-        final int horizontalPadding = getPaddingLeft() + getPaddingRight();
-
-        int referenceChildWidth = 0;
         // The children are given the same width and height as the workspace
         // unless they were set to WRAP_CONTENT
         if (DEBUG) Log.d(TAG, "PagedView.onMeasure(): " + widthSize + ", " + heightSize);
-        if (DEBUG) Log.d(TAG, "PagedView.scaledSize: " + scaledWidthSize + ", " + scaledHeightSize);
-        if (DEBUG) Log.d(TAG, "PagedView.parentSize: " + parentWidthSize + ", " + parentHeightSize);
-        if (DEBUG) Log.d(TAG, "PagedView.horizontalPadding: " + horizontalPadding);
-        if (DEBUG) Log.d(TAG, "PagedView.verticalPadding: " + verticalPadding);
-        final int childCount = getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            // disallowing padding in paged view (just pass 0)
-            final View child = getPageAt(i);
-            if (child.getVisibility() != GONE) {
-                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
 
-                int childWidthMode;
-                int childHeightMode;
-                int childWidth;
-                int childHeight;
+        int myWidthSpec = MeasureSpec.makeMeasureSpec(
+                getViewportWidth() - mInsets.left - mInsets.right, MeasureSpec.EXACTLY);
+        int myHeightSpec = MeasureSpec.makeMeasureSpec(
+                getViewportHeight() - mInsets.top - mInsets.bottom, MeasureSpec.EXACTLY);
 
-                if (!lp.isFullScreenPage) {
-                    if (lp.width == LayoutParams.WRAP_CONTENT) {
-                        childWidthMode = MeasureSpec.AT_MOST;
-                    } else {
-                        childWidthMode = MeasureSpec.EXACTLY;
-                    }
-
-                    if (lp.height == LayoutParams.WRAP_CONTENT) {
-                        childHeightMode = MeasureSpec.AT_MOST;
-                    } else {
-                        childHeightMode = MeasureSpec.EXACTLY;
-                    }
-
-                    childWidth = getViewportWidth() - horizontalPadding
-                            - mInsets.left - mInsets.right;
-                    childHeight = getViewportHeight() - verticalPadding
-                            - mInsets.top - mInsets.bottom;
-                    mNormalChildHeight = childHeight;
-                } else {
-                    childWidthMode = MeasureSpec.EXACTLY;
-                    childHeightMode = MeasureSpec.EXACTLY;
-
-                    childWidth = getViewportWidth();
-                    childHeight = getViewportHeight();
-                }
-                if (referenceChildWidth == 0) {
-                    referenceChildWidth = childWidth;
-                }
-
-                final int childWidthMeasureSpec =
-                        MeasureSpec.makeMeasureSpec(childWidth, childWidthMode);
-                    final int childHeightMeasureSpec =
-                        MeasureSpec.makeMeasureSpec(childHeight, childHeightMode);
-                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
-            }
-        }
-        setMeasuredDimension(scaledWidthSize, scaledHeightSize);
+        // measureChildren takes accounts for content padding, we only need to care about extra
+        // space due to insets.
+        measureChildren(myWidthSpec, myHeightSpec);
+        setMeasuredDimension(widthSize, heightSize);
     }
 
     @SuppressLint("DrawAllocation")
@@ -780,10 +657,7 @@
 
         int verticalPadding = getPaddingTop() + getPaddingBottom();
 
-        LayoutParams lp = (LayoutParams) getChildAt(startIndex).getLayoutParams();
-        LayoutParams nextLp;
-
-        int childLeft = offsetX + (lp.isFullScreenPage ? 0 : getPaddingLeft());
+        int childLeft = offsetX + getPaddingLeft();
         if (mPageScrolls == null || childCount != mChildCountOnLastLayout) {
             mPageScrolls = new int[childCount];
         }
@@ -791,14 +665,9 @@
         for (int i = startIndex; i != endIndex; i += delta) {
             final View child = getPageAt(i);
             if (child.getVisibility() != View.GONE) {
-                lp = (LayoutParams) child.getLayoutParams();
-                int childTop;
-                if (lp.isFullScreenPage) {
-                    childTop = offsetY;
-                } else {
-                    childTop = offsetY + getPaddingTop() + mInsets.top;
-                    childTop += (getViewportHeight() - mInsets.top - mInsets.bottom - verticalPadding - child.getMeasuredHeight()) / 2;
-                }
+                int childTop = offsetY + getPaddingTop() + mInsets.top;
+                childTop += (getViewportHeight() - mInsets.top - mInsets.bottom - verticalPadding
+                        - child.getMeasuredHeight()) / 2;
 
                 final int childWidth = child.getMeasuredWidth();
                 final int childHeight = child.getMeasuredHeight();
@@ -807,26 +676,10 @@
                 child.layout(childLeft, childTop,
                         childLeft + child.getMeasuredWidth(), childTop + childHeight);
 
-                int scrollOffsetLeft = lp.isFullScreenPage ? 0 : getPaddingLeft();
+                int scrollOffsetLeft = getPaddingLeft();
                 mPageScrolls[i] = childLeft - scrollOffsetLeft - offsetX;
 
-                int pageGap = mPageSpacing;
-                int next = i + delta;
-                if (next != endIndex) {
-                    nextLp = (LayoutParams) getPageAt(next).getLayoutParams();
-                } else {
-                    nextLp = null;
-                }
-
-                // Prevent full screen pages from showing in the viewport
-                // when they are not the current page.
-                if (lp.isFullScreenPage) {
-                    pageGap = getPaddingLeft();
-                } else if (nextLp != null && nextLp.isFullScreenPage) {
-                    pageGap = getPaddingRight();
-                }
-
-                childLeft += childWidth + pageGap + getChildGap();
+                childLeft += childWidth + mPageSpacing + getChildGap();
             }
         }
 
@@ -1289,12 +1142,7 @@
         } else {
             View child = getChildAt(index);
 
-            int scrollOffset = 0;
-            LayoutParams lp = (LayoutParams) child.getLayoutParams();
-            if (!lp.isFullScreenPage) {
-                scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft();
-            }
-
+            int scrollOffset = scrollOffset = mIsRtl ? getPaddingRight() : getPaddingLeft();
             int baselineX = mPageScrolls[index] + scrollOffset + getViewportOffsetX();
             return (int) (child.getX() - baselineX);
         }
@@ -1322,7 +1170,12 @@
      * return true if freescroll has been enabled, false otherwise
      */
     protected void enableFreeScroll() {
+        enableFreeScroll(false);
+    }
+
+    protected void enableFreeScroll(boolean settleOnPageInFreeScroll) {
         setEnableFreeScroll(true);
+        mSettleOnPageInFreeScroll = settleOnPageInFreeScroll;
     }
 
     protected void disableFreeScroll() {
@@ -1566,7 +1419,22 @@
                     mScroller.setInterpolator(mDefaultInterpolator);
                     mScroller.fling(initialScrollX,
                             getScrollY(), vX, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
-                    mNextPage = getPageNearestToCenterOfScreen((int) (mScroller.getFinalX() / scaleX));
+                    int unscaledScrollX = (int) (mScroller.getFinalX() / scaleX);
+                    mNextPage = getPageNearestToCenterOfScreen(unscaledScrollX);
+                    int firstPageScroll = getScrollForPage(!mIsRtl ? 0 : getPageCount() - 1);
+                    int lastPageScroll = getScrollForPage(!mIsRtl ? getPageCount() - 1 : 0);
+                    if (mSettleOnPageInFreeScroll && unscaledScrollX > firstPageScroll
+                            && unscaledScrollX < lastPageScroll) {
+                        // Make sure we land directly on a page. If flinging past one of the ends,
+                        // don't change the velocity as it will get stopped at the end anyway.
+                        mScroller.setFinalX((int) (getScrollForPage(mNextPage) * getScaleX()));
+                        // Ensure the scroll/snap doesn't happen too fast;
+                        int extraScrollDuration = OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION
+                                - mScroller.getDuration();
+                        if (extraScrollDuration > 0) {
+                            mScroller.extendDuration(extraScrollDuration);
+                        }
+                    }
                     invalidate();
                 }
                 onScrollInteractionEnd();
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 93fe17c..0db5a16 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -80,7 +80,6 @@
 import com.android.launcher3.graphics.PreloadIconDrawable;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
-import com.android.launcher3.uioverrides.OverviewState;
 import com.android.launcher3.uioverrides.UiFactory;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -440,10 +439,7 @@
         setClipChildren(false);
         setClipToPadding(false);
 
-        // TODO: Remove this
-        setMinScale(OverviewState.SCALE_FACTOR);
         setupLayoutTransition();
-
         mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx);
 
         // Set the wallpaper dimensions when Launcher starts up
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 271a133..2bb95cb 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -58,8 +58,8 @@
 import com.android.launcher3.util.ComponentKeyMapper;
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Themes;
 import com.android.launcher3.util.TransformingTouchDelegate;
+import com.android.launcher3.views.SlidingTabStrip;
 
 import java.util.HashMap;
 import java.util.List;
@@ -281,6 +281,9 @@
                 mAH[i].recyclerView.scrollToTop();
             }
         }
+        if (mFloatingHeaderHandler != null) {
+            mFloatingHeaderHandler.reset();
+        }
         // Reset the search bar and base recycler view after transitioning home
         mSearchUiManager.reset();
     }
@@ -301,6 +304,7 @@
         });
 
         mHeader = findViewById(R.id.all_apps_header);
+        mFloatingHeaderHandler = new FloatingHeaderHandler(mHeader);
         rebindAdapters(mUsingTabs);
 
         mSearchContainer = findViewById(R.id.search_container_all_apps);
@@ -431,10 +435,14 @@
             mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher);
             setupWorkProfileTabs();
             setupHeader();
-            mHeader.setVisibility(View.VISIBLE);
         } else {
-            mHeader.setVisibility(View.GONE);
             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
+            if (FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW) {
+                setupHeader();
+            } else {
+                mFloatingHeaderHandler = null;
+                mHeader.setVisibility(View.GONE);
+            }
         }
 
         applyTouchDelegate();
@@ -471,6 +479,7 @@
     }
 
     private void setupWorkProfileTabs() {
+        final SlidingTabStrip tabs = findViewById(R.id.tabs);
         mViewPager.setAdapter(new TabsPagerAdapter());
         mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
 
@@ -478,6 +487,7 @@
 
             @Override
             public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+                tabs.updateIndicatorPosition(position, positionOffset);
                 if (positionOffset == 0 && !mVisible || positionOffset > 0 && mVisible) {
                     mVisible = positionOffset == 0;
                     for (int i = 0; i < mAH.length; i++) {
@@ -490,6 +500,7 @@
 
             @Override
             public void onPageSelected(int pos) {
+                tabs.updateTabTextColor(pos);
                 mFloatingHeaderHandler.setMainActive(pos == 0);
                 applyTouchDelegate();
                 if (mAH[pos].recyclerView != null) {
@@ -509,7 +520,7 @@
     }
 
     public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
-        if (mUsingTabs) {
+        if (mFloatingHeaderHandler != null) {
             mFloatingHeaderHandler.getContentView().setPredictedApps(apps);
         }
         mAH[AdapterHolder.MAIN].appsList.setPredictedApps(apps);
@@ -532,15 +543,24 @@
     }
 
     private void setupHeader() {
+        mHeader.setVisibility(View.VISIBLE);
         int contentHeight = mLauncher.getDeviceProfile().allAppsCellHeightPx;
+        if (!mUsingTabs) {
+            contentHeight += getResources()
+                    .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
+        }
         RecyclerView mainRV = mAH[AdapterHolder.MAIN].recyclerView;
-        RecyclerView workRV = mAH[AdapterHolder.WORK] != null
-                ? mAH[AdapterHolder.WORK].recyclerView : null;
-        mFloatingHeaderHandler = new FloatingHeaderHandler(mHeader, mainRV, workRV, contentHeight);
-        mFloatingHeaderHandler.getContentView().setNumAppsPerRow(mNumPredictedAppsPerRow);
-        mFloatingHeaderHandler.getContentView().setComponentToAppMap(mComponentToAppMap);
+        RecyclerView workRV = mAH[AdapterHolder.WORK].recyclerView;
+        mFloatingHeaderHandler.setup(mainRV, workRV, contentHeight);
+        mFloatingHeaderHandler.getContentView().setup(mAH[AdapterHolder.MAIN].adapter,
+                mComponentToAppMap, mNumPredictedAppsPerRow);
+
+        int padding = contentHeight;
+        if (!mUsingTabs) {
+            padding += mHeader.getPaddingTop() + mHeader.getPaddingBottom();
+        }
         for (int i = 0; i < mAH.length; i++) {
-            mAH[i].paddingTopForTabs = contentHeight;
+            mAH[i].paddingTopForTabs = padding;
             mAH[i].applyPadding();
         }
     }
@@ -553,7 +573,9 @@
 
     public void onSearchResultsChanged() {
         for (int i = 0; i < mAH.length; i++) {
-            mAH[i].recyclerView.onSearchResultsChanged();
+            if (mAH[i].recyclerView != null) {
+                mAH[i].recyclerView.onSearchResultsChanged();
+            }
         }
     }
 
@@ -637,9 +659,14 @@
 
         void applyPadding() {
             if (recyclerView != null) {
-                int paddingTop = mUsingTabs ? paddingTopForTabs : padding.top;
+                int paddingTop = mUsingTabs || FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW
+                        ? paddingTopForTabs : padding.top;
                 recyclerView.setPadding(padding.left, paddingTop, padding.right, padding.bottom);
             }
+            if (mFloatingHeaderHandler != null) {
+                mFloatingHeaderHandler.getContentView()
+                        .setPadding(padding.left, 0 , padding.right, 0);
+            }
         }
 
         void applyNumsPerRow() {
@@ -649,7 +676,7 @@
                 }
                 adapter.setNumAppsPerRow(mNumAppsPerRow);
                 appsList.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
-                if (mUsingTabs && mFloatingHeaderHandler != null) {
+                if (mFloatingHeaderHandler != null) {
                     mFloatingHeaderHandler.getContentView()
                             .setNumAppsPerRow(mNumPredictedAppsPerRow);
                 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index f9dde2f..7cf2d95 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -460,7 +460,7 @@
         mFastScrollerSections.clear();
         mAdapterItems.clear();
 
-        if (!FeatureFlags.ALL_APPS_TABS_ENABLED) {
+        if (!FeatureFlags.ALL_APPS_PREDICTION_ROW_VIEW) {
             if (DEBUG_PREDICTIONS) {
                 if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
                     mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderHandler.java b/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
index 984966b..0b39b7d 100644
--- a/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
+++ b/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
@@ -15,36 +15,52 @@
  */
 package com.android.launcher3.allapps;
 
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
 import android.graphics.Rect;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
 
 import com.android.launcher3.R;
 
-public class FloatingHeaderHandler extends RecyclerView.OnScrollListener {
+public class FloatingHeaderHandler extends RecyclerView.OnScrollListener
+        implements ValueAnimator.AnimatorUpdateListener {
 
-    private final int mMaxTranslation;
     private final View mHeaderView;
-    private final PredictionRowView mContentView;
-    private final RecyclerView mMainRV;
-    private final RecyclerView mWorkRV;
+    private final PredictionRowView mPredictionRow;
+    private final ViewGroup mTabLayout;
+    private final View mDivider;
     private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
+    private final ValueAnimator mAnimator = ValueAnimator.ofInt(0, 0);
 
+    private RecyclerView mMainRV;
+    private RecyclerView mWorkRV;
+    private boolean mTopOnlyMode;
     private boolean mHeaderHidden;
+    private int mMaxTranslation;
     private int mSnappedScrolledY;
     private int mTranslationY;
     private int mMainScrolledY;
     private int mWorkScrolledY;
     private boolean mMainRVActive;
 
-    public FloatingHeaderHandler(@NonNull View header, @NonNull RecyclerView personalRV,
-            @Nullable RecyclerView workRV, int contentHeight) {
+    public FloatingHeaderHandler(@NonNull ViewGroup header) {
         mHeaderView = header;
-        mContentView = mHeaderView.findViewById(R.id.header_content);
-        mContentView.getLayoutParams().height = contentHeight;
-        mMaxTranslation = contentHeight;
+        mTabLayout = header.findViewById(R.id.tabs);
+        mDivider = header.findViewById(R.id.divider);
+        mPredictionRow = header.findViewById(R.id.header_content);
+    }
+
+    public void setup(@NonNull RecyclerView personalRV, @Nullable RecyclerView workRV,
+        int predictionRowHeight) {
+        mTopOnlyMode = workRV == null;
+        mTabLayout.setVisibility(mTopOnlyMode ? View.GONE : View.VISIBLE);
+        mPredictionRow.getLayoutParams().height = predictionRowHeight;
+        mMaxTranslation = predictionRowHeight;
         mMainRV = personalRV;
         mMainRV.addOnScrollListener(this);
         mWorkRV = workRV;
@@ -52,6 +68,18 @@
             workRV.addOnScrollListener(this);
         }
         setMainActive(true);
+        setupDivider();
+    }
+
+    private void setupDivider() {
+        Resources res = mHeaderView.getResources();
+        int verticalGap = res.getDimensionPixelSize(R.dimen.all_apps_divider_margin_vertical);
+        int sideGap = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
+        mDivider.setPadding(sideGap, verticalGap,sideGap, mTopOnlyMode ? verticalGap : 0);
+        RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mDivider.getLayoutParams();
+        lp.removeRule(RelativeLayout.ALIGN_BOTTOM);
+        lp.addRule(RelativeLayout.ALIGN_BOTTOM, mTopOnlyMode ? R.id.header_content : R.id.tabs);
+        mDivider.setLayoutParams(lp);
     }
 
     public void setMainActive(boolean active) {
@@ -65,7 +93,15 @@
     }
 
     public PredictionRowView getContentView() {
-        return mContentView;
+        return mPredictionRow;
+    }
+
+    public ViewGroup getTabLayout() {
+        return mTabLayout;
+    }
+
+    public View getDivider() {
+        return mDivider;
     }
 
     @Override
@@ -75,27 +111,39 @@
             return;
         }
 
+        if (mAnimator.isStarted()) {
+            mAnimator.cancel();
+        }
+
         int current = isMainRV
                 ? (mMainScrolledY -= dy)
                 : (mWorkScrolledY -= dy);
 
-        if (dy == 0) {
-            setExpanded(true);
-        } else {
-            moved(current);
-            apply();
-        }
+        moved(current);
+        apply();
+    }
+
+    public void reset() {
+        mMainScrolledY = 0;
+        mWorkScrolledY = 0;
+        setExpanded(true);
+    }
+
+    private boolean canSnapAt(int currentScrollY) {
+        return !mTopOnlyMode || Math.abs(currentScrollY) <= mPredictionRow.getHeight();
     }
 
     private void moved(final int currentScrollY) {
         if (mHeaderHidden) {
             if (currentScrollY <= mSnappedScrolledY) {
-                mSnappedScrolledY = currentScrollY;
+                if (canSnapAt(currentScrollY)) {
+                    mSnappedScrolledY = currentScrollY;
+                }
             } else {
                 mHeaderHidden = false;
             }
             mTranslationY = currentScrollY;
-        } else {
+        } else if (!mHeaderHidden) {
             mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
 
             // update state vars
@@ -110,20 +158,36 @@
     }
 
     private void apply() {
+        int uncappedTranslationY = mTranslationY;
         mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
-        mHeaderView.setTranslationY(mTranslationY);
+        mPredictionRow.setTranslationY(uncappedTranslationY);
+        mTabLayout.setTranslationY(mTranslationY);
+        mDivider.setTranslationY(mTopOnlyMode ? uncappedTranslationY : mTranslationY);
         mClip.top = mMaxTranslation + mTranslationY;
+        // clipping on a draw might cause additional redraw
         mMainRV.setClipBounds(mClip);
         if (mWorkRV != null) {
             mWorkRV.setClipBounds(mClip);
         }
     }
 
+    @Override
+    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+        if (!mTopOnlyMode && newState == RecyclerView.SCROLL_STATE_IDLE
+                && mTranslationY != -mMaxTranslation && mTranslationY != 0) {
+            float scroll = Math.abs(getCurrentScroll());
+            boolean expand =  scroll > mMaxTranslation
+                    ? Math.abs(mTranslationY) < mMaxTranslation / 2 : true;
+            setExpanded(expand);
+        }
+    }
+
     private void setExpanded(boolean expand) {
         int translateTo = expand ? 0 : -mMaxTranslation;
-        mTranslationY = translateTo;
-        apply();
-
+        mAnimator.setIntValues(mTranslationY, translateTo);
+        mAnimator.addUpdateListener(this);
+        mAnimator.setDuration(150);
+        mAnimator.start();
         mHeaderHidden = !expand;
         mSnappedScrolledY = expand ? getCurrentScroll() - mMaxTranslation : getCurrentScroll();
     }
@@ -136,4 +200,10 @@
         return mMainRVActive ? mMainScrolledY : mWorkScrolledY;
     }
 
+    @Override
+    public void onAnimationUpdate(ValueAnimator animation) {
+        mTranslationY = (Integer) animation.getAnimatedValue();
+        apply();
+    }
+
 }
diff --git a/src/com/android/launcher3/allapps/PredictionRowView.java b/src/com/android/launcher3/allapps/PredictionRowView.java
index 5551f07..45ef6c1 100644
--- a/src/com/android/launcher3/allapps/PredictionRowView.java
+++ b/src/com/android/launcher3/allapps/PredictionRowView.java
@@ -21,9 +21,11 @@
 import android.support.annotation.Nullable;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.View;
 import android.widget.LinearLayout;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ComponentKeyMapper;
@@ -43,6 +45,8 @@
     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<>();
+    // This adapter is only used to create an identical item w/ same behavior as in the all apps RV
+    private AllAppsGridAdapter mAdapter;
 
     public PredictionRowView(@NonNull Context context) {
         this(context, null);
@@ -53,8 +57,11 @@
         setOrientation(LinearLayout.HORIZONTAL);
     }
 
-    public void setComponentToAppMap(HashMap<ComponentKey, AppInfo> componentToAppMap) {
-        this.mComponentToAppMap = componentToAppMap;
+    public void setup(AllAppsGridAdapter adapter,
+            HashMap<ComponentKey, AppInfo> componentToAppMap, int numPredictedAppsPerRow) {
+        mAdapter = adapter;
+        mComponentToAppMap = componentToAppMap;
+        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
     }
 
     /**
@@ -64,10 +71,6 @@
         mNumPredictedAppsPerRow = numPredictedAppsPerRow;
     }
 
-    public void onAppsUpdated() {
-        // TODO
-    }
-
     /**
      * Returns the predicted apps.
      */
@@ -87,15 +90,35 @@
     public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
         mPredictedAppComponents.clear();
         mPredictedAppComponents.addAll(apps);
+        mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+        onAppsUpdated();
+    }
 
-        List<AppInfo> newPredictedApps = processPredictedAppComponents(apps);
-        // We only need to do work if any of the visible predicted apps have changed.
-        if (!newPredictedApps.equals(mPredictedApps)) {
-            if (newPredictedApps.size() == mPredictedApps.size()) {
-                swapInNewPredictedApps(newPredictedApps);
+    private void onAppsUpdated() {
+        if (getChildCount() != mNumPredictedAppsPerRow) {
+            while (getChildCount() > mNumPredictedAppsPerRow) {
+                removeViewAt(0);
+            }
+            while (getChildCount() < mNumPredictedAppsPerRow) {
+                AllAppsGridAdapter.ViewHolder holder = mAdapter
+                        .onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON);
+                BubbleTextView icon = (BubbleTextView) holder.itemView;
+                LinearLayout.LayoutParams params =
+                        new LayoutParams(0, icon.getLayoutParams().height);
+                params.weight = 1;
+                icon.setLayoutParams(params);
+                addView(icon);
+            }
+        }
+
+        for (int i = 0; i < getChildCount(); i++) {
+            BubbleTextView icon = (BubbleTextView) getChildAt(i);
+            icon.reset();
+            if (mPredictedApps.size() > i) {
+                icon.setVisibility(View.VISIBLE);
+                icon.applyFromApplicationInfo(mPredictedApps.get(i));
             } else {
-                // We need to update the appIndex of all the items.
-                onAppsUpdated();
+                icon.setVisibility(View.INVISIBLE);
             }
         }
     }
@@ -124,16 +147,4 @@
         }
         return predictedApps;
     }
-
-    /**
-     * Swaps out the old predicted apps with the new predicted apps, in place. This optimization
-     * allows us to skip an entire relayout that would otherwise be called by notifyDataSetChanged.
-     *
-     * Note: This should only be called if the # of predicted apps is the same.
-     *       This method assumes that predicted apps are the first items in the adapter.
-     */
-    private void swapInNewPredictedApps(List<AppInfo> apps) {
-        // TODO
-    }
-
 }
diff --git a/src/com/android/launcher3/anim/RevealOutlineAnimation.java b/src/com/android/launcher3/anim/RevealOutlineAnimation.java
index 51d00d9..1312da9 100644
--- a/src/com/android/launcher3/anim/RevealOutlineAnimation.java
+++ b/src/com/android/launcher3/anim/RevealOutlineAnimation.java
@@ -28,10 +28,6 @@
     /** Sets the progress, from 0 to 1, of the reveal animation. */
     abstract void setProgress(float progress);
 
-    public ValueAnimator createRevealAnimator(final View revealView) {
-        return createRevealAnimator(revealView, false);
-    }
-
     public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) {
         ValueAnimator va =
                 isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
@@ -39,8 +35,13 @@
 
         va.addListener(new AnimatorListenerAdapter() {
             private boolean mWasCanceled = false;
+            private boolean mIsClippedToOutline;
+            private ViewOutlineProvider mOldOutlineProvider;
 
             public void onAnimationStart(Animator animation) {
+                mIsClippedToOutline = revealView.getClipToOutline();
+                mOldOutlineProvider = revealView.getOutlineProvider();
+
                 revealView.setOutlineProvider(RevealOutlineAnimation.this);
                 revealView.setClipToOutline(true);
                 if (shouldRemoveElevationDuringAnimation()) {
@@ -55,8 +56,8 @@
 
             public void onAnimationEnd(Animator animation) {
                 if (!mWasCanceled) {
-                    revealView.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
-                    revealView.setClipToOutline(false);
+                    revealView.setOutlineProvider(mOldOutlineProvider);
+                    revealView.setClipToOutline(mIsClippedToOutline);
                     if (shouldRemoveElevationDuringAnimation()) {
                         revealView.setTranslationZ(0);
                     }
diff --git a/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java b/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java
index d01b26c..9c09477 100644
--- a/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java
+++ b/src/com/android/launcher3/anim/RoundedRectRevealOutlineProvider.java
@@ -18,11 +18,6 @@
 
 import android.graphics.Rect;
 
-import com.android.launcher3.popup.PopupContainerWithArrow;
-
-import static com.android.launcher3.popup.PopupContainerWithArrow.ROUNDED_BOTTOM_CORNERS;
-import static com.android.launcher3.popup.PopupContainerWithArrow.ROUNDED_TOP_CORNERS;
-
 /**
  * A {@link RevealOutlineAnimation} that provides an outline that interpolates between two radii
  * and two {@link Rect}s.
@@ -37,21 +32,12 @@
     private final Rect mStartRect;
     private final Rect mEndRect;
 
-    private final @PopupContainerWithArrow.RoundedCornerFlags int mRoundedCorners;
-
     public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect,
             Rect endRect) {
-        this(startRadius, endRadius, startRect, endRect,
-                ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS);
-    }
-
-    public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect,
-            Rect endRect, int roundedCorners) {
         mStartRadius = startRadius;
         mEndRadius = endRadius;
         mStartRect = startRect;
         mEndRect = endRect;
-        mRoundedCorners = roundedCorners;
     }
 
     @Override
@@ -65,13 +51,7 @@
 
         mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left);
         mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top);
-        if ((mRoundedCorners & ROUNDED_TOP_CORNERS) == 0) {
-            mOutline.top -= mOutlineRadius;
-        }
         mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right);
         mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom);
-        if ((mRoundedCorners & ROUNDED_BOTTOM_CORNERS) == 0) {
-            mOutline.bottom += mOutlineRadius;
-        }
     }
 }
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index 1924710..7cf3da0 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -61,4 +61,6 @@
 
     // When enabled shows a work profile tab in all apps
     public static final boolean ALL_APPS_TABS_ENABLED = false;
+    // When enabled prediction row is rendered as it's own custom view
+    public static final boolean ALL_APPS_PREDICTION_ROW_VIEW = false;
 }
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index c843e72..db199c1 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -147,11 +147,12 @@
         // Start home and pass the draw request params
         PinItemDragListener listener = new PinItemDragListener(mRequest, bounds,
                 img.getBitmap().getWidth(), img.getWidth());
-        Intent homeIntent = new Intent(Intent.ACTION_MAIN)
-                .addCategory(Intent.CATEGORY_HOME)
-                .setPackage(getPackageName())
-                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                .putExtra(PinItemDragListener.EXTRA_PIN_ITEM_DRAG_LISTENER, listener);
+
+        Intent homeIntent = listener.addToIntent(
+                new Intent(Intent.ACTION_MAIN)
+                        .addCategory(Intent.CATEGORY_HOME)
+                        .setPackage(getPackageName())
+                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
 
         if (!getResources().getBoolean(R.bool.allow_rotation) &&
                 !Utilities.isAllowRotationPrefEnabled(this) &&
diff --git a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
index 96aaee0..4629dad 100644
--- a/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/BaseItemDragListener.java
@@ -16,22 +16,25 @@
 
 package com.android.launcher3.dragndrop;
 
+import static com.android.launcher3.LauncherState.NORMAL;
+
 import android.content.ClipDescription;
 import android.content.Intent;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.Parcel;
 import android.os.SystemClock;
 import android.util.Log;
 import android.view.DragEvent;
 import android.view.View;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.states.InternalStateHandler;
 import com.android.launcher3.widget.PendingItemDragHelper;
 
 import java.util.UUID;
@@ -39,7 +42,7 @@
 /**
  * {@link DragSource} for handling drop from a different window.
  */
-public abstract class BaseItemDragListener implements
+public abstract class BaseItemDragListener extends InternalStateHandler implements
         View.OnDragListener, DragSource, DragOptions.PreDragCondition {
 
     private static final String TAG = "BaseItemDragListener";
@@ -67,25 +70,16 @@
         mId = UUID.randomUUID().toString();
     }
 
-    protected BaseItemDragListener(Parcel parcel) {
-        mPreviewRect = Rect.CREATOR.createFromParcel(parcel);
-        mPreviewBitmapWidth = parcel.readInt();
-        mPreviewViewWidth = parcel.readInt();
-        mId = parcel.readString();
-    }
-
-    protected void writeToParcel(Parcel parcel, int i) {
-        mPreviewRect.writeToParcel(parcel, i);
-        parcel.writeInt(mPreviewBitmapWidth);
-        parcel.writeInt(mPreviewViewWidth);
-        parcel.writeString(mId);
-    }
-
     public String getMimeType() {
         return MIME_TYPE_PREFIX + mId;
     }
 
-    public void setLauncher(Launcher launcher) {
+    @Override
+    public void init(Launcher launcher, boolean alreadyOnHome) {
+        AbstractFloatingView.closeAllOpenViews(launcher, alreadyOnHome);
+        launcher.getStateManager().goToState(NORMAL, alreadyOnHome /* animated */);
+        launcher.getDragLayer().setOnDragListener(this);
+
         mLauncher = launcher;
         mDragController = launcher.getDragController();
     }
@@ -182,4 +176,7 @@
             mLauncher.getDragLayer().setOnDragListener(null);
         }
     }
+
+    @Override
+    public void onLauncherResume() { }
 }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 9402383..818cea7 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -605,29 +605,32 @@
     }
 
     private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
-        final Rect r = mRectTemp;
+        mDragObject.x = x;
+        mDragObject.y = y;
 
+        final Rect r = mRectTemp;
         final ArrayList<DropTarget> dropTargets = mDropTargets;
         final int count = dropTargets.size();
-        for (int i=count-1; i>=0; i--) {
+        for (int i = count - 1; i >= 0; i--) {
             DropTarget target = dropTargets.get(i);
             if (!target.isDropEnabled())
                 continue;
 
             target.getHitRectRelativeToDragLayer(r);
-
-            mDragObject.x = x;
-            mDragObject.y = y;
             if (r.contains(x, y)) {
-
                 dropCoordinates[0] = x;
                 dropCoordinates[1] = y;
                 mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates);
-
                 return target;
             }
         }
-        return null;
+        // Pass all unhandled drag to workspace. Workspace finds the correct
+        // cell layout to drop to in the existing drag/drop logic.
+        dropCoordinates[0] = x;
+        dropCoordinates[1] = y;
+        mLauncher.getDragLayer().mapCoordInSelfToDescendant(mLauncher.getWorkspace(),
+                dropCoordinates);
+        return mLauncher.getWorkspace();
     }
 
     public void setWindowToken(IBinder token) {
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8189b23..9f9822c 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -703,13 +703,14 @@
     }
 
     @Override
-    public void onChildViewAdded(View parent, View child) {
-        super.onChildViewAdded(parent, child);
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
         updateChildIndices();
     }
 
     @Override
-    public void onChildViewRemoved(View parent, View child) {
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
         updateChildIndices();
     }
 
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index b9d97ac..924bb4c 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -18,23 +18,18 @@
 
 import android.annotation.TargetApi;
 import android.appwidget.AppWidgetManager;
-import android.content.Intent;
 import android.content.pm.LauncherApps.PinItemRequest;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.view.DragEvent;
 import android.view.View;
 import android.widget.RemoteViews;
 
 import com.android.launcher3.DragSource;
 import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppWidgetProviderInfo;
 import com.android.launcher3.PendingAddItemInfo;
-import com.android.launcher3.Utilities;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.widget.PendingAddShortcutInfo;
 import com.android.launcher3.widget.PendingAddWidgetInfo;
@@ -46,9 +41,7 @@
  * in the source window and is passed on to the Launcher activity as an Intent extra.
  */
 @TargetApi(Build.VERSION_CODES.O)
-public class PinItemDragListener extends BaseItemDragListener implements Parcelable {
-
-    public static final String EXTRA_PIN_ITEM_DRAG_LISTENER = "pin_item_drag_listener";
+public class PinItemDragListener extends BaseItemDragListener {
 
     private final PinItemRequest mRequest;
 
@@ -58,22 +51,6 @@
         mRequest = request;
     }
 
-    private PinItemDragListener(Parcel parcel) {
-        super(parcel);
-        mRequest = PinItemRequest.CREATOR.createFromParcel(parcel);
-    }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel parcel, int i) {
-        super.writeToParcel(parcel, i);
-        mRequest.writeToParcel(parcel, i);
-    }
-
     @Override
     protected boolean onDragStart(DragEvent event) {
         if (!mRequest.isValid()) {
@@ -126,33 +103,4 @@
         }
         return null;
     }
-
-    public static boolean handleDragRequest(Launcher launcher, Intent intent) {
-        if (!Utilities.ATLEAST_OREO) {
-            return false;
-        }
-        if (intent == null || !Intent.ACTION_MAIN.equals(intent.getAction())) {
-            return false;
-        }
-        Parcelable dragExtra = intent.getParcelableExtra(EXTRA_PIN_ITEM_DRAG_LISTENER);
-        if (dragExtra instanceof PinItemDragListener) {
-            PinItemDragListener dragListener = (PinItemDragListener) dragExtra;
-            dragListener.setLauncher(launcher);
-
-            launcher.getDragLayer().setOnDragListener(dragListener);
-            return true;
-        }
-        return false;
-    }
-
-    public static final Parcelable.Creator<PinItemDragListener> CREATOR =
-            new Parcelable.Creator<PinItemDragListener>() {
-                public PinItemDragListener createFromParcel(Parcel source) {
-                    return new PinItemDragListener(source);
-                }
-
-                public PinItemDragListener[] newArray(int size) {
-                    return new PinItemDragListener[size];
-                }
-            };
 }
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
index 150522e..938955c 100644
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
@@ -27,7 +27,7 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.popup.BaseActionPopup;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -46,7 +46,7 @@
     public CustomActionsPopup(Launcher launcher, View icon) {
         mLauncher = launcher;
         mIcon = icon;
-        BaseActionPopup container = BaseActionPopup.getOpen(launcher);
+        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
         if (container != null) {
             mDelegate = container.getAccessibilityDelegate();
         } else {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 816c1d4..8640401 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -118,9 +118,9 @@
         deepShortcutMap.clear();
     }
 
-     public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
-             String[] args) {
-        if (args.length > 0 && TextUtils.equals(args[0], "--proto")) {
+    public synchronized void dump(String prefix, FileDescriptor fd, PrintWriter writer,
+            String[] args) {
+        if (Arrays.asList(args).contains("--proto")) {
             dumpProto(prefix, fd, writer, args);
             return;
         }
@@ -219,7 +219,7 @@
             targetList.addAll(workspaces.valueAt(i).getFlattenedList());
         }
 
-        if (args.length > 1 && TextUtils.equals(args[1], "--debug")) {
+        if (Arrays.asList(args).contains("--debug")) {
             for (int i = 0; i < targetList.size(); i++) {
                 writer.println(prefix + DumpTargetWrapper.getDumpTargetStr(targetList.get(i)));
             }
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
index 3cf3ff6..1216a27 100644
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -23,22 +23,18 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.popup.BaseActionPopup;
-import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.util.Themes;
 
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -61,11 +57,12 @@
     private final List<NotificationInfo> mNotifications = new ArrayList<>();
     private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>();
     private final boolean mRtl;
+    private final int mBackgroundColor;
 
     FrameLayout.LayoutParams mIconLayoutParams;
     private View mOverflowEllipsis;
     private LinearLayout mIconRow;
-    private int mBackgroundColor;
+    private NotificationItemView mContainer;
 
     public NotificationFooterLayout(Context context) {
         this(context, null, 0);
@@ -93,14 +90,19 @@
         int availableIconRowSpace = footerWidth - paddingEnd - ellipsisSpace
                 - iconSize * MAX_FOOTER_NOTIFICATIONS;
         mIconLayoutParams.setMarginStart(availableIconRowSpace / MAX_FOOTER_NOTIFICATIONS);
+
+        mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mOverflowEllipsis = findViewById(R.id.overflow);
-        mIconRow = (LinearLayout) findViewById(R.id.icon_row);
-        mBackgroundColor = ((ColorDrawable) getBackground()).getColor();
+        mIconRow = findViewById(R.id.icon_row);
+    }
+
+    void setContainer(NotificationItemView container) {
+        mContainer = container;
     }
 
     /**
@@ -198,25 +200,8 @@
         updateOverflowEllipsisVisibility();
         if (mIconRow.getChildCount() == 0) {
             // There are no more icons in the footer, so hide it.
-            BaseActionPopup popup = BaseActionPopup.getOpen(
-                    Launcher.getLauncher(getContext()));
-            if (popup instanceof PopupContainerWithArrow) {
-                final int newHeight = getResources().getDimensionPixelSize(
-                        R.dimen.notification_empty_footer_height);
-                Animator collapseFooter = ((PopupContainerWithArrow) popup)
-                        .reduceNotificationViewHeight(getHeight() - newHeight,
-                        getResources().getInteger(R.integer.config_removeNotificationViewDuration));
-                collapseFooter.addListener(new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        ((ViewGroup) getParent()).findViewById(R.id.divider).setVisibility(GONE);
-                        // Keep view around because gutter is aligned to it, but remove height to
-                        // both hide the view and keep calculations correct for last dismissal.
-                        getLayoutParams().height = newHeight;
-                        requestLayout();
-                    }
-                });
-                collapseFooter.start();
+            if (mContainer != null) {
+                mContainer.removeFooter();
             }
         }
     }
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index ab94c32..5bbd19c 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -16,117 +16,102 @@
 
 package com.android.launcher3.notification;
 
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.app.Notification;
 import android.content.Context;
 import android.graphics.Rect;
 import android.support.annotation.Nullable;
-import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
-import android.widget.FrameLayout;
+import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.TextView;
 
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
-import com.android.launcher3.popup.PopupItemView;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.touch.SwipeDetector;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.Themes;
 
 import java.util.List;
 
+import static com.android.launcher3.touch.SwipeDetector.HORIZONTAL;
+
 /**
- * A {@link FrameLayout} that contains a header, main view and a footer.
- * The main view contains the icon and text (title + subtext) of the first notification.
- * The footer contains: A list of just the icons of all the notifications past the first one.
- * @see NotificationFooterLayout
+ * Utility class to manage notification UI
  */
-public class NotificationItemView extends PopupItemView implements LogContainerProvider {
+public class NotificationItemView {
 
     private static final Rect sTempRect = new Rect();
 
-    private TextView mHeaderText;
-    private TextView mHeaderCount;
-    private NotificationMainView mMainView;
-    private NotificationFooterLayout mFooter;
-    private SwipeDetector mSwipeDetector;
+    private final Context mContext;
+    private final PopupContainerWithArrow mContainer;
+
+    private final TextView mHeaderText;
+    private final TextView mHeaderCount;
+    private final NotificationMainView mMainView;
+    private final NotificationFooterLayout mFooter;
+    private final SwipeDetector mSwipeDetector;
+    private final View mIconView;
+
+    private final View mHeader;
+    private final View mDivider;
+
+    private View mGutter;
+
+    private boolean mIgnoreTouch = false;
     private boolean mAnimatingNextIcon;
     private int mNotificationHeaderTextColor = Notification.COLOR_DEFAULT;
 
-    public NotificationItemView(Context context) {
-        this(context, null, 0);
-    }
+    public NotificationItemView(PopupContainerWithArrow container) {
+        mContainer = container;
+        mContext = container.getContext();
 
-    public NotificationItemView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
+        mHeaderText = container.findViewById(R.id.notification_text);
+        mHeaderCount = container.findViewById(R.id.notification_count);
+        mMainView = container.findViewById(R.id.main_view);
+        mFooter = container.findViewById(R.id.footer);
+        mIconView = container.findViewById(R.id.popup_item_icon);
 
-    public NotificationItemView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-    }
+        mHeader = container.findViewById(R.id.header);
+        mDivider = container.findViewById(R.id.divider);
 
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mHeaderText = findViewById(R.id.notification_text);
-        mHeaderCount = findViewById(R.id.notification_count);
-        mMainView = findViewById(R.id.main_view);
-        mFooter = findViewById(R.id.footer);
-
-        mSwipeDetector = new SwipeDetector(getContext(), mMainView, SwipeDetector.HORIZONTAL);
+        mSwipeDetector = new SwipeDetector(mContext, mMainView, HORIZONTAL);
         mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
         mMainView.setSwipeDetector(mSwipeDetector);
+        mFooter.setContainer(this);
     }
 
-    public NotificationMainView getMainView() {
-        return mMainView;
+    public void addGutter() {
+        if (mGutter == null) {
+            mGutter = mContainer.inflateAndAdd(R.layout.notification_gutter);
+        }
     }
 
-    /**
-     * This method is used to calculate the height to remove when dismissing the last notification.
-     * We subtract the height of the footer in this case since the footer should be gone or in the
-     * process of being removed.
-     * @return The height of the entire notification item, minus the footer if it still exists.
-     */
-    public int getHeightMinusFooter() {
-        if (mFooter.getParent() == null) {
-            return getHeight();
+    public void removeFooter() {
+        if (mContainer.indexOfChild(mFooter) >= 0) {
+            mContainer.removeView(mFooter);
+            mContainer.removeView(mDivider);
         }
-        int excessFooterHeight = mFooter.getHeight() - getResources().getDimensionPixelSize(
-                R.dimen.notification_empty_footer_height);
-        return getHeight() - excessFooterHeight;
     }
 
-    public Animator animateHeightRemoval(int heightToRemove, boolean shouldRemoveFromTop) {
-        AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
+    public void inverseGutterMargin() {
+        MarginLayoutParams lp = (MarginLayoutParams) mGutter.getLayoutParams();
+        int top = lp.topMargin;
+        lp.topMargin = lp.bottomMargin;
+        lp.bottomMargin = top;
+    }
 
-        Rect startRect = new Rect(mPillRect);
-        Rect endRect = new Rect(mPillRect);
-        if (shouldRemoveFromTop) {
-            endRect.top += heightToRemove;
-        } else {
-            endRect.bottom -= heightToRemove;
-        }
-        anim.play(new RoundedRectRevealOutlineProvider(getBackgroundRadius(), getBackgroundRadius(),
-                startRect, endRect, mRoundedCorners).createRevealAnimator(this, false));
+    public void removeAllViews() {
+        mContainer.removeView(mMainView);
+        mContainer.removeView(mHeader);
 
-        View bottomGutter = findViewById(R.id.gutter_bottom);
-        if (bottomGutter != null && bottomGutter.getVisibility() == VISIBLE) {
-            Animator translateGutter = ObjectAnimator.ofFloat(bottomGutter, TRANSLATION_Y,
-                    -heightToRemove);
-            translateGutter.addListener(new PropertyResetListener<>(TRANSLATION_Y, 0f));
-            anim.play(translateGutter);
+        if (mContainer.indexOfChild(mFooter) >= 0) {
+            mContainer.removeView(mFooter);
+            mContainer.removeView(mDivider);
         }
 
-        return anim;
+        if (mGutter != null) {
+            mContainer.removeView(mGutter);
+        }
     }
 
     public void updateHeader(int notificationCount, @Nullable IconPalette palette) {
@@ -134,32 +119,44 @@
         if (palette != null) {
             if (mNotificationHeaderTextColor == Notification.COLOR_DEFAULT) {
                 mNotificationHeaderTextColor =
-                        IconPalette.resolveContrastColor(getContext(), palette.dominantColor,
-                                Themes.getAttrColor(getContext(), R.attr.popupColorPrimary));
+                        IconPalette.resolveContrastColor(mContext, palette.dominantColor,
+                                Themes.getAttrColor(mContext, R.attr.popupColorPrimary));
             }
             mHeaderText.setTextColor(mNotificationHeaderTextColor);
             mHeaderCount.setTextColor(mNotificationHeaderTextColor);
         }
     }
 
-    @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            sTempRect.set(mMainView.getLeft(), mMainView.getTop(),
+                    mMainView.getRight(), mMainView.getBottom());
+            mIgnoreTouch = !sTempRect.contains((int) ev.getX(), (int) ev.getY());
+            if (!mIgnoreTouch) {
+                mContainer.getParent().requestDisallowInterceptTouchEvent(true);
+            }
+        }
+        if (mIgnoreTouch) {
+            return false;
+        }
         if (mMainView.getNotificationInfo() == null) {
             // The notification hasn't been populated yet.
             return false;
         }
-        getParent().requestDisallowInterceptTouchEvent(true);
+
         mSwipeDetector.onTouchEvent(ev);
         return mSwipeDetector.isDraggingOrSettling();
     }
 
-    @Override
     public boolean onTouchEvent(MotionEvent ev) {
+        if (mIgnoreTouch) {
+            return false;
+        }
         if (mMainView.getNotificationInfo() == null) {
             // The notification hasn't been populated yet.
             return false;
         }
-        return mSwipeDetector.onTouchEvent(ev) || super.onTouchEvent(ev);
+        return mSwipeDetector.onTouchEvent(ev);
     }
 
     public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
@@ -168,7 +165,7 @@
         }
 
         NotificationInfo mainNotification = notificationInfos.get(0);
-        mMainView.applyNotificationInfo(mainNotification, mIconView);
+        mMainView.applyNotificationInfo(mainNotification, false);
 
         for (int i = 1; i < notificationInfos.size(); i++) {
             mFooter.addNotificationInfo(notificationInfos.get(i));
@@ -182,29 +179,18 @@
         if (dismissedMainNotification && !mAnimatingNextIcon) {
             // Animate the next icon into place as the new main notification.
             mAnimatingNextIcon = true;
-            mMainView.setVisibility(INVISIBLE);
-            mMainView.setTranslationX(0);
+            mMainView.setContentVisibility(View.INVISIBLE);
+            mMainView.setContentTranslation(0);
             mIconView.getGlobalVisibleRect(sTempRect);
-            mFooter.animateFirstNotificationTo(sTempRect,
-                    new NotificationFooterLayout.IconAnimationEndListener() {
-                @Override
-                public void onIconAnimationEnd(NotificationInfo newMainNotification) {
-                    if (newMainNotification != null) {
-                        mMainView.applyNotificationInfo(newMainNotification, mIconView, true);
-                        mMainView.setVisibility(VISIBLE);
-                    }
-                    mAnimatingNextIcon = false;
+            mFooter.animateFirstNotificationTo(sTempRect, (newMainNotification) -> {
+                if (newMainNotification != null) {
+                    mMainView.applyNotificationInfo(newMainNotification, true);
+                    mMainView.setContentVisibility(View.VISIBLE);
                 }
+                mAnimatingNextIcon = false;
             });
         } else {
             mFooter.trimNotifications(notificationKeys);
         }
     }
-
-    @Override
-    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
-            LauncherLogProto.Target targetParent) {
-        target.itemType = LauncherLogProto.ItemType.NOTIFICATION;
-        targetParent.containerType = LauncherLogProto.ContainerType.DEEPSHORTCUTS;
-    }
 }
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index c8e1fdb..33caded 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -18,13 +18,17 @@
 
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
+import android.animation.Animator;
 import android.animation.ObjectAnimator;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -33,6 +37,7 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.touch.OverScroll;
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -42,13 +47,33 @@
  * A {@link android.widget.FrameLayout} that contains a single notification,
  * e.g. icon + title + text.
  */
+@TargetApi(Build.VERSION_CODES.N)
 public class NotificationMainView extends FrameLayout implements SwipeDetector.Listener {
 
+    private static FloatProperty<NotificationMainView> CONTENT_TRANSLATION =
+            new FloatProperty<NotificationMainView>("contentTranslation") {
+        @Override
+        public void setValue(NotificationMainView view, float v) {
+            view.setContentTranslation(v);
+        }
+
+        @Override
+        public Float get(NotificationMainView view) {
+            return view.mTextAndBackground.getTranslationX();
+        }
+    };
+
+    // This is used only to track the notification view, so that it can be properly logged.
+    public static final ItemInfo NOTIFICATION_ITEM_INFO = new ItemInfo();
+
+    private final ObjectAnimator mContentTranslateAnimator;
+
     private NotificationInfo mNotificationInfo;
     private ViewGroup mTextAndBackground;
     private int mBackgroundColor;
     private TextView mTitleView;
     private TextView mTextView;
+    private View mIconView;
 
     private SwipeDetector mSwipeDetector;
 
@@ -62,25 +87,24 @@
 
     public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
+
+        mContentTranslateAnimator = ObjectAnimator.ofFloat(this, CONTENT_TRANSLATION, 0);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mTextAndBackground = (ViewGroup) findViewById(R.id.text_and_background);
+        mTextAndBackground = findViewById(R.id.text_and_background);
         ColorDrawable colorBackground = (ColorDrawable) mTextAndBackground.getBackground();
         mBackgroundColor = colorBackground.getColor();
         RippleDrawable rippleBackground = new RippleDrawable(ColorStateList.valueOf(
                 Themes.getAttrColor(getContext(), android.R.attr.colorControlHighlight)),
                 colorBackground, null);
         mTextAndBackground.setBackground(rippleBackground);
-        mTitleView = (TextView) mTextAndBackground.findViewById(R.id.title);
-        mTextView = (TextView) mTextAndBackground.findViewById(R.id.text);
-    }
-
-    public void applyNotificationInfo(NotificationInfo mainNotification, View iconView) {
-        applyNotificationInfo(mainNotification, iconView, false);
+        mTitleView = mTextAndBackground.findViewById(R.id.title);
+        mTextView = mTextAndBackground.findViewById(R.id.text);
+        mIconView = findViewById(R.id.popup_item_icon);
     }
 
     public void setSwipeDetector(SwipeDetector swipeDetector) {
@@ -90,8 +114,7 @@
     /**
      * Sets the content of this view, animating it after a new icon shifts up if necessary.
      */
-    public void applyNotificationInfo(NotificationInfo mainNotification, View iconView,
-           boolean animate) {
+    public void applyNotificationInfo(NotificationInfo mainNotification, boolean animate) {
         mNotificationInfo = mainNotification;
         CharSequence title = mNotificationInfo.title;
         CharSequence text = mNotificationInfo.text;
@@ -103,20 +126,30 @@
             mTitleView.setText(TextUtils.isEmpty(title) ? text.toString() : title.toString());
             mTextView.setVisibility(GONE);
         }
-        iconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
+        mIconView.setBackground(mNotificationInfo.getIconForBackground(getContext(),
                 mBackgroundColor));
         if (mNotificationInfo.intent != null) {
             setOnClickListener(mNotificationInfo);
         }
-        setTranslationX(0);
+        setContentTranslation(0);
         // Add a dummy ItemInfo so that logging populates the correct container and item types
         // instead of DEFAULT_CONTAINERTYPE and DEFAULT_ITEMTYPE, respectively.
-        setTag(new ItemInfo());
+        setTag(NOTIFICATION_ITEM_INFO);
         if (animate) {
             ObjectAnimator.ofFloat(mTextAndBackground, ALPHA, 0, 1).setDuration(150).start();
         }
     }
 
+    public void setContentTranslation(float translation) {
+        mTextAndBackground.setTranslationX(translation);
+        mIconView.setTranslationX(translation);
+    }
+
+    public void setContentVisibility(int visibility) {
+        mTextAndBackground.setVisibility(visibility);
+        mIconView.setVisibility(visibility);
+    }
+
     public NotificationInfo getNotificationInfo() {
         return mNotificationInfo;
     }
@@ -143,9 +176,9 @@
 
     @Override
     public boolean onDrag(float displacement, float velocity) {
-        setTranslationX(canChildBeDismissed()
+        setContentTranslation(canChildBeDismissed()
                 ? displacement : OverScroll.dampedScroll(displacement, getWidth()));
-        animate().cancel();
+        mContentTranslateAnimator.cancel();
         return true;
     }
 
@@ -153,6 +186,7 @@
     public void onDragEnd(float velocity, boolean fling) {
         final boolean willExit;
         final float endTranslation;
+        final float startTranslation = mTextAndBackground.getTranslationX();
 
         if (!canChildBeDismissed()) {
             willExit = false;
@@ -160,28 +194,30 @@
         } else if (fling) {
             willExit = true;
             endTranslation = velocity < 0 ? - getWidth() : getWidth();
-        } else if (Math.abs(getTranslationX()) > getWidth() / 2) {
+        } else if (Math.abs(startTranslation) > getWidth() / 2) {
             willExit = true;
-            endTranslation = (getTranslationX() < 0 ? -getWidth() : getWidth());
+            endTranslation = (startTranslation < 0 ? -getWidth() : getWidth());
         } else {
             willExit = false;
             endTranslation = 0;
         }
 
         long duration = SwipeDetector.calculateDuration(velocity,
-                (endTranslation - getTranslationX()) / getWidth());
-        animate()
-                .setDuration(duration)
-                .setInterpolator(scrollInterpolatorForVelocity(velocity))
-                .translationX(endTranslation)
-                .withEndAction(new Runnable() {
-                    @Override
-                    public void run() {
-                        mSwipeDetector.finishedScrolling();
-                        if (willExit) {
-                            onChildDismissed();
-                        }
-                    }
-                }).start();
+                (endTranslation - startTranslation) / getWidth());
+
+        mContentTranslateAnimator.removeAllListeners();
+        mContentTranslateAnimator.setDuration(duration)
+                .setInterpolator(scrollInterpolatorForVelocity(velocity));
+        mContentTranslateAnimator.setFloatValues(startTranslation, endTranslation);
+        mContentTranslateAnimator.addListener(new AnimationSuccessListener() {
+            @Override
+            public void onAnimationSuccess(Animator animator) {
+                mSwipeDetector.finishedScrolling();
+                if (willExit) {
+                    onChildDismissed();
+                }
+            }
+        });
+        mContentTranslateAnimator.start();
     }
 }
diff --git a/src/com/android/launcher3/popup/BaseActionPopup.java b/src/com/android/launcher3/popup/BaseActionPopup.java
deleted file mode 100644
index 7ffe2ef..0000000
--- a/src/com/android/launcher3/popup/BaseActionPopup.java
+++ /dev/null
@@ -1,599 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.launcher3.popup;
-
-import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
-import android.os.Build;
-import android.support.annotation.IntDef;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.animation.AccelerateDecelerateInterpolator;
-import android.widget.TextView;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAnimUtils;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
-import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.dragndrop.DragLayer;
-import com.android.launcher3.graphics.TriangleShape;
-import com.android.launcher3.logging.LoggerUtils;
-import com.android.launcher3.notification.NotificationItemView;
-import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.shortcuts.ShortcutsItemView;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Themes;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.Set;
-
-/**
- * Base popup container for showing shortcuts to deep links within apps.
- */
-@TargetApi(Build.VERSION_CODES.N)
-public class BaseActionPopup<V extends TextView> extends AbstractFloatingView {
-
-    public static final int ROUNDED_TOP_CORNERS = 1 << 0;
-    public static final int ROUNDED_BOTTOM_CORNERS = 1 << 1;
-
-    @IntDef(flag = true, value = {
-            ROUNDED_TOP_CORNERS,
-            ROUNDED_BOTTOM_CORNERS
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public  @interface RoundedCornerFlags {}
-
-    protected final Launcher mLauncher;
-    protected final LauncherAccessibilityDelegate mAccessibilityDelegate;
-    private final boolean mIsRtl;
-
-    public ShortcutsItemView mShortcutsItemView;
-
-    protected V mOriginalIcon;
-    private final Rect mTempRect = new Rect();
-    private PointF mInterceptTouchDown = new PointF();
-    private boolean mIsLeftAligned;
-    protected boolean mIsAboveIcon;
-    protected View mArrow;
-    private int mGravity;
-
-    protected Animator mOpenCloseAnimator;
-    protected boolean mDeferContainerRemoval;
-    private final Rect mStartRect = new Rect();
-    private final Rect mEndRect = new Rect();
-
-    public BaseActionPopup(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
-
-        mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
-        mIsRtl = Utilities.isRtl(getResources());
-    }
-
-    public BaseActionPopup(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public BaseActionPopup(Context context) {
-        this(context, null, 0);
-    }
-
-    public LauncherAccessibilityDelegate getAccessibilityDelegate() {
-        return mAccessibilityDelegate;
-    }
-
-    protected PopupItemView getItemViewAt(int index) {
-        if (!mIsAboveIcon) {
-            // Opening down, so arrow is the first view.
-            index++;
-        }
-        return (PopupItemView) getChildAt(index);
-    }
-
-    protected int getItemCount() {
-        // All children except the arrow are items.
-        return getChildCount() - 1;
-    }
-
-    protected void animateOpen() {
-        setVisibility(View.VISIBLE);
-        mIsOpen = true;
-
-        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
-        final Resources res = getResources();
-        final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
-        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
-
-        // Rectangular reveal.
-        int itemsTotalHeight = 0;
-        for (int i = 0; i < getItemCount(); i++) {
-            itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
-        }
-        Point startPoint = computeAnimStartPoint(itemsTotalHeight);
-        int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
-        float radius = getItemViewAt(0).getBackgroundRadius();
-        mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
-        mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
-        final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider
-                (radius, radius, mStartRect, mEndRect).createRevealAnimator(this, false);
-        revealAnim.setDuration(revealDuration);
-        revealAnim.setInterpolator(revealInterpolator);
-
-        Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1);
-        fadeIn.setDuration(revealDuration);
-        fadeIn.setInterpolator(revealInterpolator);
-        openAnim.play(fadeIn);
-
-        // Animate the arrow.
-        mArrow.setScaleX(0);
-        mArrow.setScaleY(0);
-        Animator arrowScale = createArrowScaleAnim(1).setDuration(res.getInteger(
-                R.integer.config_popupArrowOpenDuration));
-
-        openAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator = null;
-                Utilities.sendCustomAccessibilityEvent(
-                        BaseActionPopup.this,
-                        AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                        getContext().getString(R.string.action_deep_shortcut));
-            }
-        });
-
-        mOpenCloseAnimator = openAnim;
-        openAnim.playSequentially(revealAnim, arrowScale);
-        openAnim.start();
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        enforceContainedWithinScreen(l, r);
-    }
-
-    private void enforceContainedWithinScreen(int left, int right) {
-        DragLayer dragLayer = mLauncher.getDragLayer();
-        if (getTranslationX() + left < 0 ||
-                getTranslationX() + right > dragLayer.getWidth()) {
-            // If we are still off screen, center horizontally too.
-            mGravity |= Gravity.CENTER_HORIZONTAL;
-        }
-
-        if (Gravity.isHorizontal(mGravity)) {
-            setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
-        }
-        if (Gravity.isVertical(mGravity)) {
-            setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
-        }
-    }
-
-    /**
-     * Returns the point at which the center of the arrow merges with the first popup item.
-     */
-    private Point computeAnimStartPoint(int itemsTotalHeight) {
-        int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
-                R.dimen.popup_arrow_horizontal_center_start:
-                R.dimen.popup_arrow_horizontal_center_end);
-        if (!mIsLeftAligned) {
-            arrowCenterX = getMeasuredWidth() - arrowCenterX;
-        }
-        int arrowHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
-                - itemsTotalHeight;
-        // The y-coordinate of edge between the arrow and the first popup item.
-        int arrowEdge = getPaddingTop() + (mIsAboveIcon ? itemsTotalHeight : arrowHeight);
-        return new Point(arrowCenterX, arrowEdge);
-    }
-
-    /**
-     * Orients this container above or below the given icon, aligning with the left or right.
-     *
-     * These are the preferred orientations, in order (RTL prefers right-aligned over left):
-     * - Above and left-aligned
-     * - Above and right-aligned
-     * - Below and left-aligned
-     * - Below and right-aligned
-     *
-     * So we always align left if there is enough horizontal space
-     * and align above if there is enough vertical space.
-     */
-    protected void orientAboutIcon(int arrowHeight) {
-        int width = getMeasuredWidth();
-        int height = getMeasuredHeight() + arrowHeight;
-
-        DragLayer dragLayer = mLauncher.getDragLayer();
-        dragLayer.getDescendantRectRelativeToSelf(mOriginalIcon, mTempRect);
-        Rect insets = dragLayer.getInsets();
-
-        // Align left (right in RTL) if there is room.
-        int leftAlignedX = mTempRect.left + mOriginalIcon.getPaddingLeft();
-        int rightAlignedX = mTempRect.right - width - mOriginalIcon.getPaddingRight();
-        int x = leftAlignedX;
-        boolean canBeLeftAligned = leftAlignedX + width + insets.left
-                < dragLayer.getRight() - insets.right;
-        boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
-        if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
-            x = rightAlignedX;
-        }
-        mIsLeftAligned = x == leftAlignedX;
-        if (mIsRtl) {
-            x -= dragLayer.getWidth() - width;
-        }
-
-        // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
-        int iconWidth = mOriginalIcon.getWidth()
-                - mOriginalIcon.getTotalPaddingLeft() - mOriginalIcon.getTotalPaddingRight();
-        iconWidth *= mOriginalIcon.getScaleX();
-        Resources resources = getResources();
-        int xOffset;
-        if (isAlignedWithStart()) {
-            // Aligning with the shortcut icon.
-            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
-            int shortcutPaddingStart = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_start);
-            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
-        } else {
-            // Aligning with the drag handle.
-            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
-                    R.dimen.deep_shortcut_drag_handle_size);
-            int shortcutPaddingEnd = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_end);
-            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
-        }
-        x += mIsLeftAligned ? xOffset : -xOffset;
-
-        // Open above icon if there is room.
-        int iconHeight = getIconHeightForPopupPlacement();
-        int y = mTempRect.top + mOriginalIcon.getPaddingTop() - height;
-        mIsAboveIcon = y > dragLayer.getTop() + insets.top;
-        if (!mIsAboveIcon) {
-            y = mTempRect.top + mOriginalIcon.getPaddingTop() + iconHeight;
-        }
-
-        // Insets are added later, so subtract them now.
-        if (mIsRtl) {
-            x += insets.right;
-        } else {
-            x -= insets.left;
-        }
-        y -= insets.top;
-
-        mGravity = 0;
-        if (y + height > dragLayer.getBottom() - insets.bottom) {
-            // The container is opening off the screen, so just center it in the drag layer instead.
-            mGravity = Gravity.CENTER_VERTICAL;
-            // Put the container next to the icon, preferring the right side in ltr (left in rtl).
-            int rightSide = leftAlignedX + iconWidth - insets.left;
-            int leftSide = rightAlignedX - iconWidth - insets.left;
-            if (!mIsRtl) {
-                if (rightSide + width < dragLayer.getRight()) {
-                    x = rightSide;
-                    mIsLeftAligned = true;
-                } else {
-                    x = leftSide;
-                    mIsLeftAligned = false;
-                }
-            } else {
-                if (leftSide > dragLayer.getLeft()) {
-                    x = leftSide;
-                    mIsLeftAligned = false;
-                } else {
-                    x = rightSide;
-                    mIsLeftAligned = true;
-                }
-            }
-            mIsAboveIcon = true;
-        }
-
-        setX(x);
-        setY(y);
-    }
-
-    protected int getIconHeightForPopupPlacement() {
-        return mOriginalIcon.getHeight();
-    }
-
-    protected boolean isAlignedWithStart() {
-        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
-    }
-
-    /**
-     * Adds an arrow view pointing at the original icon.
-     * @param horizontalOffset the horizontal offset of the arrow, so that it
-     *                              points at the center of the original icon
-     */
-    protected View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
-        LayoutParams layoutParams = new LayoutParams(width, height);
-        if (mIsLeftAligned) {
-            layoutParams.gravity = Gravity.LEFT;
-            layoutParams.leftMargin = horizontalOffset;
-        } else {
-            layoutParams.gravity = Gravity.RIGHT;
-            layoutParams.rightMargin = horizontalOffset;
-        }
-        if (mIsAboveIcon) {
-            layoutParams.topMargin = verticalOffset;
-        } else {
-            layoutParams.bottomMargin = verticalOffset;
-        }
-
-        View arrowView = new View(getContext());
-        if (Gravity.isVertical(mGravity)) {
-            // This is only true if there wasn't room for the container next to the icon,
-            // so we centered it instead. In that case we don't want to show the arrow.
-            arrowView.setVisibility(INVISIBLE);
-        } else {
-            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
-                    width, height, !mIsAboveIcon));
-            Paint arrowPaint = arrowDrawable.getPaint();
-            arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary));
-            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
-            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
-            arrowPaint.setPathEffect(new CornerPathEffect(radius));
-            arrowView.setBackground(arrowDrawable);
-            arrowView.setElevation(getElevation());
-        }
-        addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
-        return arrowView;
-    }
-
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mInterceptTouchDown.set(ev.getX(), ev.getY());
-            return false;
-        }
-        // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
-        return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
-                > ViewConfiguration.get(getContext()).getScaledTouchSlop();
-    }
-
-    protected ObjectAnimator createArrowScaleAnim(float scale) {
-        return LauncherAnimUtils.ofPropertyValuesHolder(
-                mArrow, new PropertyListBuilder().scale(scale).build());
-    }
-
-    @Override
-    protected void handleClose(boolean animate) {
-        if (animate) {
-            animateClose();
-        } else {
-            closeComplete();
-        }
-    }
-
-    protected void animateClose() {
-        if (!mIsOpen) {
-            return;
-        }
-        mEndRect.setEmpty();
-        if (mOpenCloseAnimator != null) {
-            Outline outline = new Outline();
-            getOutlineProvider().getOutline(this, outline);
-            outline.getRect(mEndRect);
-            mOpenCloseAnimator.cancel();
-        }
-        mIsOpen = false;
-
-        final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
-        prepareCloseAnimator(closeAnim);
-
-        closeAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator = null;
-                if (mDeferContainerRemoval) {
-                    setVisibility(INVISIBLE);
-                } else {
-                    closeComplete();
-                }
-            }
-        });
-        mOpenCloseAnimator = closeAnim;
-        closeAnim.start();
-    }
-
-    protected void prepareCloseAnimator(AnimatorSet closeAnim) {
-        final Resources res = getResources();
-        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
-
-        // Rectangular reveal (reversed).
-        int itemsTotalHeight = 0;
-        for (int i = 0; i < getItemCount(); i++) {
-            itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
-        }
-        Point startPoint = computeAnimStartPoint(itemsTotalHeight);
-        int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
-        float radius = getItemViewAt(0).getBackgroundRadius();
-        mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
-        if (mEndRect.isEmpty()) {
-            mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
-        }
-        final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider(
-                radius, radius, mStartRect, mEndRect).createRevealAnimator(this, true);
-        revealAnim.setInterpolator(revealInterpolator);
-        closeAnim.play(revealAnim);
-
-        Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
-        fadeOut.setInterpolator(revealInterpolator);
-        closeAnim.play(fadeOut);
-        closeAnim.setDuration((long) res.getInteger(R.integer.config_popupOpenCloseDuration));
-    }
-
-    /**
-     * Closes the folder without animation.
-     */
-    protected void closeComplete() {
-        if (mOpenCloseAnimator != null) {
-            mOpenCloseAnimator.cancel();
-            mOpenCloseAnimator = null;
-        }
-        mIsOpen = false;
-        mDeferContainerRemoval = false;
-        mLauncher.getDragLayer().removeView(this);
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_ACTION_POPUP) != 0;
-    }
-
-    /**
-     * Returns a DeepShortcutsContainer which is already open or null
-     */
-    public static BaseActionPopup getOpen(Launcher launcher) {
-        return getOpenView(launcher, TYPE_ACTION_POPUP);
-    }
-
-    @Override
-    public void logActionCommand(int command) {
-        mLauncher.getUserEventDispatcher().logActionCommand(
-                command, mOriginalIcon, ContainerType.DEEPSHORTCUTS);
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            DragLayer dl = mLauncher.getDragLayer();
-            if (!dl.isEventOverView(this, ev)) {
-                mLauncher.getUserEventDispatcher().logActionTapOutside(
-                        LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS));
-                close(true);
-
-                // We let touches on the original icon go through so that users can launch
-                // the app with one tap if they don't find a shortcut they want.
-                return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev);
-            }
-        }
-        return false;
-    }
-
-    public void populateAndShow(V originalIcon, PopupPopulator.Item[] itemsToPopulate) {
-        setVisibility(View.INVISIBLE);
-        mLauncher.getDragLayer().addView(this);
-
-        final Resources resources = getResources();
-        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
-        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
-        final int arrowVerticalOffset = resources.getDimensionPixelSize(
-                R.dimen.popup_arrow_vertical_offset);
-
-        mOriginalIcon = originalIcon;
-
-        // Add dummy views first, and populate with real info when ready.
-        addDummyViews(itemsToPopulate);
-
-        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-        orientAboutIcon(arrowHeight + arrowVerticalOffset);
-
-        boolean reverseOrder = mIsAboveIcon;
-        if (reverseOrder) {
-            removeAllViews();
-            mShortcutsItemView = null;
-            itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
-            addDummyViews(itemsToPopulate);
-
-            measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-            orientAboutIcon(arrowHeight + arrowVerticalOffset);
-        }
-
-        // Add the arrow.
-        final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ?
-                R.dimen.popup_arrow_horizontal_offset_start :
-                R.dimen.popup_arrow_horizontal_offset_end);
-        mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
-        mArrow.setPivotX(arrowWidth / 2);
-        mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
-
-        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-        animateOpen();
-    }
-
-    protected void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate) {
-        final LayoutInflater inflater = mLauncher.getLayoutInflater();
-        int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
-        int numItems = itemTypesToPopulate.length;
-        for (int i = 0; i < numItems; i++) {
-            PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
-            PopupPopulator.Item prevItemTypeToPopulate =
-                    i > 0 ? itemTypesToPopulate[i - 1] : null;
-            PopupPopulator.Item nextItemTypeToPopulate =
-                    i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
-            final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
-
-            boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null
-                    && itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut;
-            boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null
-                    && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
-
-            onViewInflated(item, itemTypeToPopulate,
-                    shouldUnroundTopCorners, shouldUnroundBottomCorners);
-
-            if (itemTypeToPopulate.isShortcut) {
-                if (mShortcutsItemView == null) {
-                    mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
-                            R.layout.shortcuts_item, this, false);
-                    addView(mShortcutsItemView);
-                    if (shouldUnroundTopCorners) {
-                        shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS;
-                    }
-                }
-                mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
-                if (shouldUnroundBottomCorners) {
-                    shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
-                }
-            } else {
-                addView(item);
-            }
-        }
-        int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
-        mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners);
-    }
-
-    protected void onViewInflated(View view, PopupPopulator.Item itemType,
-            boolean shouldUnroundTopCorners, boolean shouldUnroundBottomCorners) {
-
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 4435afb..3dc58a1 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -16,26 +16,39 @@
 
 package com.android.launcher3.popup;
 
-import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
+import android.animation.LayoutTransition;
 import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.CornerPathEffect;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.util.AttributeSet;
+import android.view.Gravity;
 import android.view.LayoutInflater;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.ImageView;
 
+import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
@@ -45,42 +58,103 @@
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.R;
-import com.android.launcher3.anim.PropertyResetListener;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.badge.BadgeInfo;
 import com.android.launcher3.dragndrop.DragController;
+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;
 import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.notification.NotificationKeyData;
-import com.android.launcher3.popup.PopupPopulator.Item;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.shortcuts.ShortcutsItemView;
+import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Themes;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static com.android.launcher3.notification.NotificationMainView.NOTIFICATION_ITEM_INFO;
+import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS;
+import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+
 /**
  * A container for shortcuts to deep links and notifications associated with an app.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> implements DragSource,
-        DragController.DragListener {
+public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
+        DragController.DragListener, View.OnLongClickListener,
+        View.OnTouchListener {
+
+    private final List<DeepShortcutView> mShortcuts = new ArrayList<>();
+    private final PointF mInterceptTouchDown = new PointF();
+    private final Rect mTempRect = new Rect();
+    private final Point mIconLastTouchPos = new Point();
 
     private final int mStartDragThreshold;
+    private final LayoutInflater mInflater;
+    private final float mOutlineRadius;
+    private final Launcher mLauncher;
+    private final LauncherAccessibilityDelegate mAccessibilityDelegate;
+    private final boolean mIsRtl;
 
+    private final int mArrayOffset;
+    private final View mArrow;
+
+    private BubbleTextView mOriginalIcon;
     private NotificationItemView mNotificationItemView;
-    private AnimatorSet mReduceHeightAnimatorSet;
+
+    private ViewGroup mSystemShortcutContainer;
+
+    private boolean mIsLeftAligned;
+    protected boolean mIsAboveIcon;
     private int mNumNotifications;
+    private int mGravity;
+
+    protected Animator mOpenCloseAnimator;
+    protected boolean mDeferContainerRemoval;
+    private final Rect mStartRect = new Rect();
+    private final Rect mEndRect = new Rect();
 
     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mStartDragThreshold = getResources().getDimensionPixelSize(
                 R.dimen.deep_shortcuts_start_drag_threshold);
+        mInflater = LayoutInflater.from(context);
+        mOutlineRadius = getResources().getDimension(R.dimen.bg_round_rect_radius);
+        mLauncher = Launcher.getLauncher(context);
+        mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
+        mIsRtl = Utilities.isRtl(getResources());
+
+        setClipToOutline(true);
+        setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius);
+            }
+        });
+
+        // Initialize arrow view
+        final Resources resources = getResources();
+        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
+        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
+        mArrow = new View(context);
+        mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight));
+        mArrayOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
     }
 
     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
@@ -91,6 +165,75 @@
         this(context, null, 0);
     }
 
+    public LauncherAccessibilityDelegate getAccessibilityDelegate() {
+        return mAccessibilityDelegate;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mInterceptTouchDown.set(ev.getX(), ev.getY());
+        }
+        if (mNotificationItemView != null
+                && mNotificationItemView.onInterceptTouchEvent(ev)) {
+            return true;
+        }
+        // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
+        return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
+                > ViewConfiguration.get(getContext()).getScaledTouchSlop();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mNotificationItemView != null) {
+            return mNotificationItemView.onTouchEvent(ev);
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ACTION_POPUP) != 0;
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        mLauncher.getUserEventDispatcher().logActionCommand(
+                command, mOriginalIcon, ContainerType.DEEPSHORTCUTS);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            DragLayer dl = mLauncher.getDragLayer();
+            if (!dl.isEventOverView(this, ev)) {
+                mLauncher.getUserEventDispatcher().logActionTapOutside(
+                        LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS));
+                close(true);
+
+                // We let touches on the original icon go through so that users can launch
+                // the app with one tap if they don't find a shortcut they want.
+                return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (animate) {
+            animateClose();
+        } else {
+            closeComplete();
+        }
+    }
+
+    public  <T extends View> T inflateAndAdd(int resId) {
+        View view = mInflater.inflate(resId, this, false);
+        addView(view);
+        return (T) view;
+    }
+
     /**
      * Shows the notifications and deep shortcuts associated with {@param icon}.
      * @return the container if shown or null.
@@ -124,188 +267,420 @@
     private void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
         mNumNotifications = notificationKeys.size();
-        PopupPopulator.Item[] itemsToPopulate = PopupPopulator
-                .getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts);
-        populateAndShow(originalIcon, itemsToPopulate);
 
-        ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
-        List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
-                ? Collections.EMPTY_LIST
-                : mShortcutsItemView.getDeepShortcutViews(mIsAboveIcon);
-        List<View> systemShortcutViews = mShortcutsItemView == null
-                ? Collections.EMPTY_LIST
-                : mShortcutsItemView.getSystemShortcutViews(mIsAboveIcon);
-        if (mNotificationItemView != null) {
+        setVisibility(View.INVISIBLE);
+        mLauncher.getDragLayer().addView(this);
+
+        mOriginalIcon = originalIcon;
+
+        // Add views
+        if (mNumNotifications > 0) {
+            // Add notification entries
+            View.inflate(getContext(), R.layout.notification_content, this);
+            mNotificationItemView = new NotificationItemView(this);
+            if (mNumNotifications == 1) {
+                mNotificationItemView.removeFooter();
+            }
             updateNotificationHeader();
         }
+        int viewsToFlip = getChildCount();
+        mSystemShortcutContainer = this;
 
-        int numShortcuts = shortcutViews.size() + systemShortcutViews.size();
-        int numNotifications = notificationKeys.size();
-        if (numNotifications == 0) {
+        if (!shortcutIds.isEmpty()) {
+            if (mNotificationItemView != null) {
+                mNotificationItemView.addGutter();
+            }
+
+            for (int i = shortcutIds.size(); i > 0; i--) {
+                mShortcuts.add(inflateAndAdd(R.layout.deep_shortcut));
+            }
+            updateHiddenShortcuts();
+
+            if (!systemShortcuts.isEmpty()) {
+                mSystemShortcutContainer = inflateAndAdd(R.layout.system_shortcut_icons);
+                for (SystemShortcut shortcut : systemShortcuts) {
+                    View view = mInflater.inflate(R.layout.system_shortcut_icon_only,
+                            mSystemShortcutContainer, false);
+                    mSystemShortcutContainer.addView(view);
+                    initializeSystemShortcut(view, shortcut);
+                }
+            }
+        } else if (!systemShortcuts.isEmpty()) {
+            if (mNotificationItemView != null) {
+                mNotificationItemView.addGutter();
+            }
+
+            for (SystemShortcut shortcut : systemShortcuts) {
+                initializeSystemShortcut(inflateAndAdd(R.layout.system_shortcut), shortcut);
+            }
+        }
+        orientAboutIcon();
+
+        boolean reverseOrder = mIsAboveIcon;
+        if (reverseOrder) {
+            int count = getChildCount();
+            ArrayList<View> allViews = new ArrayList<>(count);
+            for (int i = 0; i < count; i++) {
+                if (i == viewsToFlip) {
+                    Collections.reverse(allViews);
+                }
+                allViews.add(getChildAt(i));
+            }
+            Collections.reverse(allViews);
+            removeAllViews();
+            for (int i = 0; i < count; i++) {
+                addView(allViews.get(i));
+            }
+            if (mNotificationItemView != null) {
+                mNotificationItemView.inverseGutterMargin();
+            }
+
+            orientAboutIcon();
+        }
+        updateDividers();
+
+        // Add the arrow.
+        final int arrowHorizontalOffset = getResources().getDimensionPixelSize(isAlignedWithStart()
+                ? R.dimen.popup_arrow_horizontal_offset_start
+                : R.dimen.popup_arrow_horizontal_offset_end);
+        mLauncher.getDragLayer().addView(mArrow);
+        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
+        if (mIsLeftAligned) {
+            mArrow.setX(getX() + arrowHorizontalOffset);
+        } else {
+            mArrow.setX(getX() + getMeasuredWidth() - arrowHorizontalOffset);
+        }
+
+        if (Gravity.isVertical(mGravity)) {
+            // This is only true if there wasn't room for the container next to the icon,
+            // so we centered it instead. In that case we don't want to show the arrow.
+            mArrow.setVisibility(INVISIBLE);
+        } else {
+            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                    arrowLp.width, arrowLp.height, !mIsAboveIcon));
+            Paint arrowPaint = arrowDrawable.getPaint();
+            arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary));
+            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
+            arrowPaint.setPathEffect(new CornerPathEffect(radius));
+            mArrow.setBackground(arrowDrawable);
+            mArrow.setElevation(getElevation());
+        }
+
+        mArrow.setPivotX(arrowLp.width / 2);
+        mArrow.setPivotY(mIsAboveIcon ? 0 : arrowLp.height);
+
+        animateOpen();
+
+        ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
+        int numShortcuts = mShortcuts.size() + systemShortcuts.size();
+        if (mNumNotifications == 0) {
             setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
                     numShortcuts, originalIcon.getContentDescription().toString()));
         } else {
             setContentDescription(getContext().getString(
                     R.string.shortcuts_menu_with_notifications_description, numShortcuts,
-                    numNotifications, originalIcon.getContentDescription().toString()));
+                    mNumNotifications, originalIcon.getContentDescription().toString()));
         }
 
         mLauncher.getDragController().addDragListener(this);
         mOriginalIcon.forceHideBadge(true);
 
+        // All views are added. Animate layout from now on.
+        setLayoutTransition(new LayoutTransition());
+
         // Load the shortcuts on a background thread and update the container as it animates.
         final Looper workerLooper = LauncherModel.getWorkerLooper();
         new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
                 mLauncher, originalItemInfo, new Handler(Looper.getMainLooper()),
-                this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView,
-                systemShortcuts, systemShortcutViews));
+                this, shortcutIds, mShortcuts, notificationKeys));
     }
 
-    @Override
-    protected void addDummyViews(Item[] itemTypesToPopulate) {
-        mNotificationItemView = null;
-        super.addDummyViews(itemTypesToPopulate);
-        if (mNumNotifications > 0) {
-            mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS);
-        }
+    protected boolean isAlignedWithStart() {
+        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
     }
 
-    @Override
-    protected void onViewInflated(View view, Item itemType,
-            boolean shouldUnroundTopCorners, boolean shouldUnroundBottomCorners) {
-        if (itemType == PopupPopulator.Item.NOTIFICATION) {
-            mNotificationItemView = (NotificationItemView) view;
-            boolean notificationFooterHasIcons = mNumNotifications > 1;
-            int footerHeight = getResources().getDimensionPixelSize(
-                    notificationFooterHasIcons ? R.dimen.notification_footer_height
-                            : R.dimen.notification_empty_footer_height);
-            view.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
-            if (notificationFooterHasIcons) {
-                mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE);
-            }
+    /**
+     * Orients this container above or below the given icon, aligning with the left or right.
+     *
+     * These are the preferred orientations, in order (RTL prefers right-aligned over left):
+     * - Above and left-aligned
+     * - Above and right-aligned
+     * - Below and left-aligned
+     * - Below and right-aligned
+     *
+     * So we always align left if there is enough horizontal space
+     * and align above if there is enough vertical space.
+     */
+    protected void orientAboutIcon() {
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        int width = getMeasuredWidth();
+        int extraVerticalSpace = mArrow.getLayoutParams().height + mArrayOffset
+                + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
+        int height = getMeasuredHeight() + extraVerticalSpace;
 
-            int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
-            if (shouldUnroundTopCorners) {
-                roundedCorners &= ~ROUNDED_TOP_CORNERS;
-                mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE);
-            }
-            if (shouldUnroundBottomCorners) {
-                roundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
-                mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE);
-            }
-            int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary);
-            mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners);
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        dragLayer.getDescendantRectRelativeToSelf(mOriginalIcon, mTempRect);
+        Rect insets = dragLayer.getInsets();
 
-            mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate);
-        } else if (itemType == PopupPopulator.Item.SHORTCUT) {
-            view.setAccessibilityDelegate(mAccessibilityDelegate);
+        // Align left (right in RTL) if there is room.
+        int leftAlignedX = mTempRect.left + mOriginalIcon.getPaddingLeft();
+        int rightAlignedX = mTempRect.right - width - mOriginalIcon.getPaddingRight();
+        int x = leftAlignedX;
+        boolean canBeLeftAligned = leftAlignedX + width + insets.left
+                < dragLayer.getRight() - insets.right;
+        boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
+        if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
+            x = rightAlignedX;
+        }
+        mIsLeftAligned = x == leftAlignedX;
+        if (mIsRtl) {
+            x -= dragLayer.getWidth() - width;
         }
 
-        if (itemType != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON && itemType.isShortcut
-                && mNumNotifications > 0) {
-            int prevHeight = view.getLayoutParams().height;
-            // Condense shortcuts height when there are notifications.
-            view.getLayoutParams().height = getResources().getDimensionPixelSize(
-                    R.dimen.bg_popup_item_condensed_height);
-            if (view instanceof DeepShortcutView) {
-                float iconScale = (float) view.getLayoutParams().height / prevHeight;
-                ((DeepShortcutView) view).getIconView().setScaleX(iconScale);
-                ((DeepShortcutView) view).getIconView().setScaleY(iconScale);
-            }
+        // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
+        int iconWidth = mOriginalIcon.getWidth()
+                - mOriginalIcon.getTotalPaddingLeft() - mOriginalIcon.getTotalPaddingRight();
+        iconWidth *= mOriginalIcon.getScaleX();
+        Resources resources = getResources();
+        int xOffset;
+        if (isAlignedWithStart()) {
+            // Aligning with the shortcut icon.
+            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
+            int shortcutPaddingStart = resources.getDimensionPixelSize(
+                    R.dimen.popup_padding_start);
+            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
+        } else {
+            // Aligning with the drag handle.
+            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
+                    R.dimen.deep_shortcut_drag_handle_size);
+            int shortcutPaddingEnd = resources.getDimensionPixelSize(
+                    R.dimen.popup_padding_end);
+            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
         }
-    }
+        x += mIsLeftAligned ? xOffset : -xOffset;
 
-    private void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications) {
-        final Resources res = getResources();
-        final LayoutInflater inflater = mLauncher.getLayoutInflater();
+        // Open above icon if there is room.
+        int iconHeight = getIconHeightForPopupPlacement();
+        int y = mTempRect.top + mOriginalIcon.getPaddingTop() - height;
+        mIsAboveIcon = y > dragLayer.getTop() + insets.top;
+        if (!mIsAboveIcon) {
+            y = mTempRect.top + mOriginalIcon.getPaddingTop() + iconHeight + extraVerticalSpace;
+        }
 
-        int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
-        int numItems = itemTypesToPopulate.length;
-        for (int i = 0; i < numItems; i++) {
-            PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
-            PopupPopulator.Item prevItemTypeToPopulate =
-                    i > 0 ? itemTypesToPopulate[i - 1] : null;
-            PopupPopulator.Item nextItemTypeToPopulate =
-                    i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
-            final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
+        // Insets are added later, so subtract them now.
+        if (mIsRtl) {
+            x += insets.right;
+        } else {
+            x -= insets.left;
+        }
+        y -= insets.top;
 
-            boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null
-                    && itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut;
-            boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null
-                    && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
-
-            if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
-                mNotificationItemView = (NotificationItemView) item;
-                boolean notificationFooterHasIcons = numNotifications > 1;
-                int footerHeight = res.getDimensionPixelSize(
-                        notificationFooterHasIcons ? R.dimen.notification_footer_height
-                                : R.dimen.notification_empty_footer_height);
-                item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
-                if (notificationFooterHasIcons) {
-                    mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE);
-                }
-
-                int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
-                if (shouldUnroundTopCorners) {
-                    roundedCorners &= ~ROUNDED_TOP_CORNERS;
-                    mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE);
-                }
-                if (shouldUnroundBottomCorners) {
-                    roundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
-                    mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE);
-                }
-                int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary);
-                mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners);
-
-                mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate);
-            } else if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
-                item.setAccessibilityDelegate(mAccessibilityDelegate);
-            }
-
-            if (itemTypeToPopulate.isShortcut) {
-                if (mShortcutsItemView == null) {
-                    mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
-                            R.layout.shortcuts_item, this, false);
-                    addView(mShortcutsItemView);
-                    if (shouldUnroundTopCorners) {
-                        shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS;
-                    }
-                }
-                if (itemTypeToPopulate != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON
-                        && numNotifications > 0) {
-                    int prevHeight = item.getLayoutParams().height;
-                    // Condense shortcuts height when there are notifications.
-                    item.getLayoutParams().height = res.getDimensionPixelSize(
-                            R.dimen.bg_popup_item_condensed_height);
-                    if (item instanceof DeepShortcutView) {
-                        float iconScale = (float) item.getLayoutParams().height / prevHeight;
-                        ((DeepShortcutView) item).getIconView().setScaleX(iconScale);
-                        ((DeepShortcutView) item).getIconView().setScaleY(iconScale);
-                    }
-                }
-                mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
-                if (shouldUnroundBottomCorners) {
-                    shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
+        mGravity = 0;
+        if (y + height > dragLayer.getBottom() - insets.bottom) {
+            // The container is opening off the screen, so just center it in the drag layer instead.
+            mGravity = Gravity.CENTER_VERTICAL;
+            // Put the container next to the icon, preferring the right side in ltr (left in rtl).
+            int rightSide = leftAlignedX + iconWidth - insets.left;
+            int leftSide = rightAlignedX - iconWidth - insets.left;
+            if (!mIsRtl) {
+                if (rightSide + width < dragLayer.getRight()) {
+                    x = rightSide;
+                    mIsLeftAligned = true;
+                } else {
+                    x = leftSide;
+                    mIsLeftAligned = false;
                 }
             } else {
-                addView(item);
+                if (leftSide > dragLayer.getLeft()) {
+                    x = leftSide;
+                    mIsLeftAligned = false;
+                } else {
+                    x = rightSide;
+                    mIsLeftAligned = true;
+                }
             }
+            mIsAboveIcon = true;
         }
-        int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
-        mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners);
-        if (numNotifications > 0) {
-            mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS);
+
+        setX(x);
+        if (Gravity.isVertical(mGravity)) {
+            return;
+        }
+
+        DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
+        DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams();
+        if (mIsAboveIcon) {
+            arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
+            lp.bottomMargin =
+                    mLauncher.getDragLayer().getHeight() - y - getMeasuredHeight() - insets.top;
+            arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrayOffset - insets.bottom;
+        } else {
+            arrowLp.gravity = lp.gravity = Gravity.TOP;
+            lp.topMargin = y + insets.top;
+            arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrayOffset;
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        // enforce contained is within screen
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        if (getTranslationX() + l < 0 ||
+                getTranslationX() + r > dragLayer.getWidth()) {
+            // If we are still off screen, center horizontally too.
+            mGravity |= Gravity.CENTER_HORIZONTAL;
+        }
+
+        if (Gravity.isHorizontal(mGravity)) {
+            setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
+            mArrow.setVisibility(INVISIBLE);
+        }
+        if (Gravity.isVertical(mGravity)) {
+            setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
+        }
+    }
+
+    protected void animateOpen() {
+        setVisibility(View.VISIBLE);
+        mIsOpen = true;
+
+        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
+        final Resources res = getResources();
+        final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
+        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
+
+        // Rectangular reveal.
+        final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, false);
+        revealAnim.setDuration(revealDuration);
+        revealAnim.setInterpolator(revealInterpolator);
+
+        Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1);
+        fadeIn.setDuration(revealDuration);
+        fadeIn.setInterpolator(revealInterpolator);
+        openAnim.play(fadeIn);
+
+        // Animate the arrow.
+        mArrow.setScaleX(0);
+        mArrow.setScaleY(0);
+        Animator arrowScale = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 1)
+                .setDuration(res.getInteger(R.integer.config_popupArrowOpenDuration));
+
+        openAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenCloseAnimator = null;
+                Utilities.sendCustomAccessibilityEvent(
+                        PopupContainerWithArrow.this,
+                        AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+                        getContext().getString(R.string.action_deep_shortcut));
+            }
+        });
+
+        mOpenCloseAnimator = openAnim;
+        openAnim.playSequentially(revealAnim, arrowScale);
+        openAnim.start();
+    }
+
+    public void applyNotificationInfos(List<NotificationInfo> notificationInfos) {
+        mNotificationItemView.applyNotificationInfos(notificationInfos);
+    }
+
+    private void updateHiddenShortcuts() {
+        int allowedCount = mNotificationItemView != null
+                ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS;
+        int originalHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height);
+        int itemHeight = mNotificationItemView != null ?
+                getResources().getDimensionPixelSize(R.dimen.bg_popup_item_condensed_height)
+                : originalHeight;
+        float iconScale = ((float) itemHeight) / originalHeight;
+
+        int total = mShortcuts.size();
+        for (int i = 0; i < total; i++) {
+            DeepShortcutView view = mShortcuts.get(i);
+            view.setVisibility(i >= allowedCount ? GONE : VISIBLE);
+            view.getLayoutParams().height = itemHeight;
+            view.getIconView().setScaleX(iconScale);
+            view.getIconView().setScaleY(iconScale);
+        }
+    }
+
+    private void updateDividers() {
+        int count = getChildCount();
+        DeepShortcutView lastView = null;
+        for (int i = 0; i < count; i++) {
+            View view = getChildAt(i);
+            if (view.getVisibility() == VISIBLE && view instanceof DeepShortcutView) {
+                if (lastView != null) {
+                    lastView.setDividerVisibility(VISIBLE);
+                }
+                lastView = (DeepShortcutView) view;
+                lastView.setDividerVisibility(INVISIBLE);
+            }
         }
     }
 
     @Override
     protected void onWidgetsBound() {
-        if (mShortcutsItemView != null) {
-            mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
+        ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
+        SystemShortcut widgetInfo = new SystemShortcut.Widgets();
+        View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo);
+        View widgetsView = null;
+        int count = mSystemShortcutContainer.getChildCount();
+        for (int i = 0; i < count; i++) {
+            View systemShortcutView = mSystemShortcutContainer.getChildAt(i);
+            if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
+                widgetsView = systemShortcutView;
+                break;
+            }
+        }
+
+        if (onClickListener != null && widgetsView == null) {
+            // We didn't have any widgets cached but now there are some, so enable the shortcut.
+            if (mSystemShortcutContainer != this) {
+                View view = mInflater.inflate(R.layout.system_shortcut_icon_only,
+                        mSystemShortcutContainer, false);
+                mSystemShortcutContainer.addView(view);
+                initializeSystemShortcut(view, widgetInfo);
+            } else {
+                // If using the expanded system shortcut (as opposed to just the icon), we need to
+                // 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);
+                PopupContainerWithArrow.showForIcon(mOriginalIcon);
+            }
+        } else if (onClickListener == null && widgetsView != null) {
+            // No widgets exist, but we previously added the shortcut so remove it.
+            if (mSystemShortcutContainer != this) {
+                mSystemShortcutContainer.removeView(widgetsView);
+            } else {
+                ((PopupContainerWithArrow) getParent()).close(false);
+                PopupContainerWithArrow.showForIcon(mOriginalIcon);
+            }
         }
     }
 
-    @Override
+    private void initializeSystemShortcut(View view, SystemShortcut info) {
+        if (view instanceof DeepShortcutView) {
+            // Expanded system shortcut, with both icon and text shown on white background.
+            final DeepShortcutView shortcutView = (DeepShortcutView) view;
+            shortcutView.getIconView().setBackgroundResource(info.iconResId);
+            shortcutView.getBubbleText().setText(info.labelResId);
+        } else if (view instanceof ImageView) {
+            // Only the system shortcut icon shows on a gray background header.
+            final ImageView shortcutIcon = (ImageView) view;
+            shortcutIcon.setImageResource(info.iconResId);
+            shortcutIcon.setContentDescription(getContext().getText(info.labelResId));
+        }
+        view.setTag(info);
+        view.setOnClickListener(info.getOnClickListener(mLauncher,
+                (ItemInfo) mOriginalIcon.getTag()));
+    }
+
     protected int getIconHeightForPopupPlacement() {
         return mOriginalIcon.getIcon() != null
                 ? mOriginalIcon.getIcon().getBounds().height()
@@ -383,108 +758,15 @@
         ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
         BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
         if (badgeInfo == null || badgeInfo.getNotificationKeys().size() == 0) {
-            // There are no more notifications, so create an animation to remove
-            // the notifications view and expand the shortcuts view (if possible).
-            AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
-            int hiddenShortcutsHeight = 0;
-            if (mShortcutsItemView != null) {
-                hiddenShortcutsHeight = mShortcutsItemView.getHiddenShortcutsHeight();
-                int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
-                // With notifications gone, all corners of shortcuts item should be rounded.
-                mShortcutsItemView.setBackgroundWithCorners(backgroundColor,
-                        ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS);
-                removeNotification.play(mShortcutsItemView.showAllShortcuts(mIsAboveIcon));
-            }
-            final int duration = getResources().getInteger(
-                    R.integer.config_removeNotificationViewDuration);
-            removeNotification.play(adjustItemHeights(mNotificationItemView.getHeightMinusFooter(),
-                    hiddenShortcutsHeight, duration));
-            Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
-                    .setDuration(duration);
-            fade.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(Animator animation) {
-                    removeView(mNotificationItemView);
-                    mNotificationItemView = null;
-                    if (getItemCount() == 0) {
-                        close(false);
-                    }
-                }
-            });
-            removeNotification.play(fade);
-            final long arrowScaleDuration = getResources().getInteger(
-                    R.integer.config_popupArrowOpenDuration);
-            Animator hideArrow = createArrowScaleAnim(0).setDuration(arrowScaleDuration);
-            hideArrow.setStartDelay(0);
-            Animator showArrow = createArrowScaleAnim(1).setDuration(arrowScaleDuration);
-            showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
-            removeNotification.playSequentially(hideArrow, showArrow);
-            removeNotification.start();
-            return;
+            // No more notifications, remove the notification views and expand all shortcuts.
+            mNotificationItemView.removeAllViews();
+            mNotificationItemView = null;
+            updateHiddenShortcuts();
+            updateDividers();
+        } else {
+            mNotificationItemView.trimNotifications(
+                    NotificationKeyData.extractKeysOnly(badgeInfo.getNotificationKeys()));
         }
-        mNotificationItemView.trimNotifications(NotificationKeyData.extractKeysOnly(
-                badgeInfo.getNotificationKeys()));
-    }
-
-    public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
-        return adjustItemHeights(heightToRemove, 0, duration);
-    }
-
-    /**
-     * Animates the height of the notification item and the translationY of other items accordingly.
-     */
-    public Animator adjustItemHeights(int notificationHeightToRemove, int shortcutHeightToAdd,
-            int duration) {
-        if (mReduceHeightAnimatorSet != null) {
-            mReduceHeightAnimatorSet.cancel();
-        }
-        final int translateYBy = mIsAboveIcon ? notificationHeightToRemove - shortcutHeightToAdd
-                : -notificationHeightToRemove;
-        mReduceHeightAnimatorSet = LauncherAnimUtils.createAnimatorSet();
-        boolean removingNotification =
-                notificationHeightToRemove == mNotificationItemView.getHeightMinusFooter();
-        boolean shouldRemoveNotificationHeightFromTop = mIsAboveIcon && removingNotification;
-        mReduceHeightAnimatorSet.play(mNotificationItemView.animateHeightRemoval(
-                notificationHeightToRemove, shouldRemoveNotificationHeightFromTop));
-        PropertyResetListener<View, Float> resetTranslationYListener
-                = new PropertyResetListener<>(TRANSLATION_Y, 0f);
-        boolean itemIsAfterShortcuts = false;
-        for (int i = 0; i < getItemCount(); i++) {
-            final PopupItemView itemView = getItemViewAt(i);
-            if (itemIsAfterShortcuts) {
-                // Every item after the shortcuts item needs to adjust for the new height.
-                itemView.setTranslationY(itemView.getTranslationY() - shortcutHeightToAdd);
-            }
-            if (itemView == mNotificationItemView && (!mIsAboveIcon || removingNotification)) {
-                // The notification view is already in the right place.
-                continue;
-            }
-            ValueAnimator translateItem = ObjectAnimator.ofFloat(itemView, TRANSLATION_Y,
-                    itemView.getTranslationY() + translateYBy).setDuration(duration);
-            translateItem.addListener(resetTranslationYListener);
-            mReduceHeightAnimatorSet.play(translateItem);
-            if (itemView == mShortcutsItemView) {
-                itemIsAfterShortcuts = true;
-            }
-        }
-        if (mIsAboveIcon) {
-            // We also need to adjust the arrow position to account for the new shortcuts height.
-            mArrow.setTranslationY(mArrow.getTranslationY() - shortcutHeightToAdd);
-        }
-        mReduceHeightAnimatorSet.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                if (mIsAboveIcon) {
-                    // All the items, including the notification item, translated down, but the
-                    // container itself did not. This means the items would jump back to their
-                    // original translation unless we update the container's translationY here.
-                    setTranslationY(getTranslationY() + translateYBy);
-                    mArrow.setTranslationY(0);
-                }
-                mReduceHeightAnimatorSet = null;
-            }
-        });
-        return mReduceHeightAnimatorSet;
     }
 
     @Override
@@ -515,25 +797,146 @@
 
     @Override
     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
-        target.itemType = ItemType.DEEPSHORTCUT;
+        if (info == NOTIFICATION_ITEM_INFO) {
+            target.itemType = ItemType.NOTIFICATION;
+        } else {
+            target.itemType = ItemType.DEEPSHORTCUT;
+            target.rank = info.rank;
+        }
         targetParent.containerType = ContainerType.DEEPSHORTCUTS;
     }
 
-    @Override
-    protected void prepareCloseAnimator(AnimatorSet closeAnim) {
+    protected void animateClose() {
+        if (!mIsOpen) {
+            return;
+        }
+        mEndRect.setEmpty();
+        if (mOpenCloseAnimator != null) {
+            Outline outline = new Outline();
+            getOutlineProvider().getOutline(this, outline);
+            outline.getRect(mEndRect);
+            mOpenCloseAnimator.cancel();
+        }
+        mIsOpen = false;
+
+        final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
+        // Hide the arrow
+        closeAnim.play(ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0));
+        closeAnim.play(ObjectAnimator.ofFloat(mArrow, ALPHA, 0));
+
         // Animate original icon's text back in.
         closeAnim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
-
         mOriginalIcon.forceHideBadge(false);
-        super.prepareCloseAnimator(closeAnim);
+
+        final Resources res = getResources();
+        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
+
+        // Rectangular reveal (reversed).
+        final ValueAnimator revealAnim = createOpenCloseOutlineProvider()
+                .createRevealAnimator(this, true);
+        revealAnim.setInterpolator(revealInterpolator);
+        closeAnim.play(revealAnim);
+
+        Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
+        fadeOut.setInterpolator(revealInterpolator);
+        closeAnim.play(fadeOut);
+        closeAnim.setDuration((long) res.getInteger(R.integer.config_popupOpenCloseDuration));
+
+        closeAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenCloseAnimator = null;
+                if (mDeferContainerRemoval) {
+                    setVisibility(INVISIBLE);
+                } else {
+                    closeComplete();
+                }
+            }
+        });
+        mOpenCloseAnimator = closeAnim;
+        closeAnim.start();
     }
 
-    @Override
-    protected void closeComplete() {
+    private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
+        int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
+                R.dimen.popup_arrow_horizontal_center_start:
+                R.dimen.popup_arrow_horizontal_center_end);
+        if (!mIsLeftAligned) {
+            arrowCenterX = getMeasuredWidth() - arrowCenterX;
+        }
+        int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0;
+
+        mStartRect.set(arrowCenterX, arrowCenterY, arrowCenterX, arrowCenterY);
+        if (mEndRect.isEmpty()) {
+            mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+        }
+
+        return new RoundedRectRevealOutlineProvider
+                (mOutlineRadius, mOutlineRadius, mStartRect, mEndRect);
+    }
+
+    /**
+     * Closes the popup without animation.
+     */
+    private void closeComplete() {
         mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
         mOriginalIcon.forceHideBadge(false);
 
         mLauncher.getDragController().removeDragListener(this);
-        super.closeComplete();
+        if (mOpenCloseAnimator != null) {
+            mOpenCloseAnimator.cancel();
+            mOpenCloseAnimator = null;
+        }
+        mIsOpen = false;
+        mDeferContainerRemoval = false;
+        mLauncher.getDragLayer().removeView(this);
+        mLauncher.getDragLayer().removeView(mArrow);
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        // Touched a shortcut, update where it was touched so we can drag from there on long click.
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        // Return early if not the correct view
+        if (!(v.getParent() instanceof DeepShortcutView)) return false;
+        // Return early if global dragging is not enabled
+        if (!mLauncher.isDraggingEnabled()) return false;
+        // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts)
+        if (mLauncher.getDragController().isDragging()) return false;
+
+        // Long clicked on a shortcut.
+        DeepShortcutView sv = (DeepShortcutView) v.getParent();
+        sv.setWillDrawIcon(false);
+
+        // Move the icon to align with the center-top of the touch point
+        Point iconShift = new Point();
+        iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
+        iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
+
+        DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(),
+                this, sv.getFinalInfo(),
+                new ShortcutDragPreviewProvider(sv.getIconView(), iconShift), new DragOptions());
+        dv.animateShift(-iconShift.x, -iconShift.y);
+
+        // TODO: support dragging from within folder without having to close it
+        AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
+        return false;
+    }
+
+    /**
+     * Returns a PopupContainerWithArrow which is already open or null
+     */
+    public static PopupContainerWithArrow getOpen(Launcher launcher) {
+        return getOpenView(launcher, TYPE_ACTION_POPUP);
     }
 }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 070ac39..abc186b 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -148,9 +148,9 @@
     }
 
     private void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
-        BaseActionPopup openContainer = BaseActionPopup.getOpen(mLauncher);
-        if (openContainer instanceof PopupContainerWithArrow) {
-            ((PopupContainerWithArrow) openContainer).trimNotifications(updatedBadges);
+        PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
+        if (openContainer != null) {
+            openContainer.trimNotifications(updatedBadges);
         }
     }
 
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
deleted file mode 100644
index 75c3f26..0000000
--- a/src/com/android/launcher3/popup/PopupItemView.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.launcher3.popup;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
-import android.graphics.drawable.shapes.RoundRectShape;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.popup.BaseActionPopup.RoundedCornerFlags;
-
-import static com.android.launcher3.popup.PopupContainerWithArrow.ROUNDED_BOTTOM_CORNERS;
-import static com.android.launcher3.popup.PopupContainerWithArrow.ROUNDED_TOP_CORNERS;
-
-/**
- * An abstract {@link FrameLayout} that contains content for {@link PopupContainerWithArrow}.
- */
-public abstract class PopupItemView extends FrameLayout {
-
-    protected final Rect mPillRect;
-    protected  @RoundedCornerFlags int mRoundedCorners;
-    protected final boolean mIsRtl;
-    protected View mIconView;
-
-    private final Paint mBackgroundClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
-    private final Matrix mMatrix = new Matrix();
-    private Bitmap mRoundedCornerBitmap;
-
-    public PopupItemView(Context context) {
-        this(context, null, 0);
-    }
-
-    public PopupItemView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public PopupItemView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        mPillRect = new Rect();
-
-        // Initialize corner clipping Bitmap and Paint.
-        int radius = (int) getBackgroundRadius();
-        mRoundedCornerBitmap = Bitmap.createBitmap(radius, radius, Bitmap.Config.ALPHA_8);
-        Canvas canvas = new Canvas();
-        canvas.setBitmap(mRoundedCornerBitmap);
-        canvas.drawArc(0, 0, radius*2, radius*2, 180, 90, true, mBackgroundClipPaint);
-        mBackgroundClipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
-
-        mIsRtl = Utilities.isRtl(getResources());
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mIconView = findViewById(R.id.popup_item_icon);
-    }
-
-    @Override
-    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
-    }
-
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        if (mRoundedCorners == 0) {
-            super.dispatchDraw(canvas);
-            return;
-        }
-
-        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
-        super.dispatchDraw(canvas);
-
-        // Clip children to this item's rounded corners.
-        int cornerWidth = mRoundedCornerBitmap.getWidth();
-        int cornerHeight = mRoundedCornerBitmap.getHeight();
-        int cornerCenterX = Math.round(cornerWidth / 2f);
-        int cornerCenterY = Math.round(cornerHeight / 2f);
-        if ((mRoundedCorners & ROUNDED_TOP_CORNERS) != 0) {
-            // Clip top left corner.
-            mMatrix.reset();
-            canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
-            // Clip top right corner.
-            mMatrix.setRotate(90, cornerCenterX, cornerCenterY);
-            mMatrix.postTranslate(canvas.getWidth() - cornerWidth, 0);
-            canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
-        }
-        if ((mRoundedCorners & ROUNDED_BOTTOM_CORNERS) != 0) {
-            // Clip bottom right corner.
-            mMatrix.setRotate(180, cornerCenterX, cornerCenterY);
-            mMatrix.postTranslate(canvas.getWidth() - cornerWidth, canvas.getHeight() - cornerHeight);
-            canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
-            // Clip bottom left corner.
-            mMatrix.setRotate(270, cornerCenterX, cornerCenterY);
-            mMatrix.postTranslate(0, canvas.getHeight() - cornerHeight);
-            canvas.drawBitmap(mRoundedCornerBitmap, mMatrix, mBackgroundClipPaint);
-        }
-
-        canvas.restoreToCount(saveCount);
-    }
-
-    /**
-     * Creates a round rect drawable (with the specified corners unrounded)
-     * and sets it as this View's background.
-     */
-    public void setBackgroundWithCorners(int color, @RoundedCornerFlags int roundedCorners) {
-        mRoundedCorners = roundedCorners;
-        float rTop = (roundedCorners & ROUNDED_TOP_CORNERS) == 0 ? 0 : getBackgroundRadius();
-        float rBot = (roundedCorners & ROUNDED_BOTTOM_CORNERS) == 0 ? 0 : getBackgroundRadius();
-        float[] radii = new float[] {rTop, rTop, rTop, rTop, rBot, rBot, rBot, rBot};
-        ShapeDrawable roundRectBackground = new ShapeDrawable(new RoundRectShape(radii, null, null));
-        roundRectBackground.getPaint().setColor(color);
-        setBackground(roundRectBackground);
-    }
-
-    protected float getBackgroundRadius() {
-        return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius);
-    }
-}
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index 0dc1ca0..6c83d12 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -17,23 +17,17 @@
 package com.android.launcher3.popup;
 
 import android.content.ComponentName;
-import android.content.Context;
 import android.os.Handler;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
-import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.VisibleForTesting;
-import android.view.View;
-import android.widget.ImageView;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.graphics.LauncherIcons;
 import com.android.launcher3.notification.NotificationInfo;
-import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.notification.NotificationKeyData;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -56,55 +50,6 @@
     @VisibleForTesting static final int NUM_DYNAMIC = 2;
     public static final int MAX_SHORTCUTS_IF_NOTIFICATIONS = 2;
 
-    public enum Item {
-        SHORTCUT(R.layout.deep_shortcut, true),
-        NOTIFICATION(R.layout.notification, false),
-        SYSTEM_SHORTCUT(R.layout.system_shortcut, true),
-        SYSTEM_SHORTCUT_ICON(R.layout.system_shortcut_icon_only, true);
-
-        public final int layoutId;
-        public final boolean isShortcut;
-
-        Item(int layoutId, boolean isShortcut) {
-            this.layoutId = layoutId;
-            this.isShortcut = isShortcut;
-        }
-    }
-
-    public static @NonNull Item[] getItemsToPopulate(@NonNull List<String> shortcutIds,
-            @NonNull List<NotificationKeyData> notificationKeys,
-            @NonNull List<SystemShortcut> systemShortcuts) {
-        boolean hasNotifications = notificationKeys.size() > 0;
-        int numNotificationItems = hasNotifications ? 1 : 0;
-        int numShortcuts = shortcutIds.size();
-        int numItems = Math.min(MAX_SHORTCUTS, numShortcuts) + numNotificationItems
-                + systemShortcuts.size();
-        Item[] items = new Item[numItems];
-        for (int i = 0; i < numItems; i++) {
-            items[i] = Item.SHORTCUT;
-        }
-        if (hasNotifications) {
-            // The notification layout is always first.
-            items[0] = Item.NOTIFICATION;
-        }
-        // The system shortcuts are always last.
-        boolean iconsOnly = !shortcutIds.isEmpty();
-        for (int i = 0; i < systemShortcuts.size(); i++) {
-            items[numItems - 1 - i] = iconsOnly ? Item.SYSTEM_SHORTCUT_ICON : Item.SYSTEM_SHORTCUT;
-        }
-        return items;
-    }
-
-    public static Item[] reverseItems(Item[] items) {
-        if (items == null) return null;
-        int numItems = items.length;
-        Item[] reversedArray = new Item[numItems];
-        for (int i = 0; i < numItems; i++) {
-            reversedArray[i] = items[numItems - i - 1];
-        }
-        return reversedArray;
-    }
-
     /**
      * Sorts shortcuts in rank order, with manifest shortcuts coming before dynamic shortcuts.
      */
@@ -179,137 +124,42 @@
     public static Runnable createUpdateRunnable(final Launcher launcher, final ItemInfo originalInfo,
             final Handler uiHandler, final PopupContainerWithArrow container,
             final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews,
-            final List<NotificationKeyData> notificationKeys,
-            final NotificationItemView notificationView, final List<SystemShortcut> systemShortcuts,
-            final List<View> systemShortcutViews) {
+            final List<NotificationKeyData> notificationKeys) {
         final ComponentName activity = originalInfo.getTargetComponent();
         final UserHandle user = originalInfo.user;
-        return new Runnable() {
-            @Override
-            public void run() {
-                if (notificationView != null) {
-                    List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
-                            .getStatusBarNotificationsForKeys(notificationKeys);
-                    List<NotificationInfo> infos = new ArrayList<>(notifications.size());
-                    for (int i = 0; i < notifications.size(); i++) {
-                        StatusBarNotification notification = notifications.get(i);
-                        infos.add(new NotificationInfo(launcher, notification));
-                    }
-                    uiHandler.post(new UpdateNotificationChild(notificationView, infos));
+        return () -> {
+            if (!notificationKeys.isEmpty()) {
+                List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
+                        .getStatusBarNotificationsForKeys(notificationKeys);
+                List<NotificationInfo> infos = new ArrayList<>(notifications.size());
+                for (int i = 0; i < notifications.size(); i++) {
+                    StatusBarNotification notification = notifications.get(i);
+                    infos.add(new NotificationInfo(launcher, notification));
                 }
-
-                List<ShortcutInfoCompat> shortcuts = DeepShortcutManager.getInstance(launcher)
-                        .queryForShortcutsContainer(activity, shortcutIds, user);
-                String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null
-                        : notificationKeys.get(0).shortcutId;
-                shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe);
-                for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
-                    final ShortcutInfoCompat shortcut = shortcuts.get(i);
-                    ShortcutInfo si = new ShortcutInfo(shortcut, launcher);
-                    // Use unbadged icon for the menu.
-                    si.iconBitmap = LauncherIcons.createShortcutIcon(
-                            shortcut, launcher, false /* badged */);
-                    si.rank = i;
-                    uiHandler.post(new UpdateShortcutChild(container, shortcutViews.get(i),
-                            si, shortcut));
-                }
-
-                // This ensures that mLauncher.getWidgetsForPackageUser()
-                // doesn't return null (it puts all the widgets in memory).
-                for (int i = 0; i < systemShortcuts.size(); i++) {
-                    final SystemShortcut systemShortcut = systemShortcuts.get(i);
-                    uiHandler.post(new UpdateSystemShortcutChild(container,
-                            systemShortcutViews.get(i), systemShortcut, launcher, originalInfo));
-                }
-                uiHandler.post(new Runnable() {
-                    @Override
-                    public void run() {
-                        launcher.refreshAndBindWidgetsForPackageUser(
-                                PackageUserKey.fromItemInfo(originalInfo));
-                    }
-                });
+                uiHandler.post(() -> container.applyNotificationInfos(infos));
             }
+
+            List<ShortcutInfoCompat> shortcuts = DeepShortcutManager.getInstance(launcher)
+                    .queryForShortcutsContainer(activity, shortcutIds, user);
+            String shortcutIdToDeDupe = notificationKeys.isEmpty() ? null
+                    : notificationKeys.get(0).shortcutId;
+            shortcuts = PopupPopulator.sortAndFilterShortcuts(shortcuts, shortcutIdToDeDupe);
+            for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
+                final ShortcutInfoCompat shortcut = shortcuts.get(i);
+                final ShortcutInfo si = new ShortcutInfo(shortcut, launcher);
+                // Use unbadged icon for the menu.
+                si.iconBitmap = LauncherIcons.createShortcutIcon(
+                        shortcut, launcher, false /* badged */);
+                si.rank = i;
+
+                final DeepShortcutView view = shortcutViews.get(i);
+                uiHandler.post(() -> view.applyShortcutInfo(si, shortcut, container));
+            }
+
+            // This ensures that mLauncher.getWidgetsForPackageUser()
+            // doesn't return null (it puts all the widgets in memory).
+            uiHandler.post(() -> launcher.refreshAndBindWidgetsForPackageUser(
+                    PackageUserKey.fromItemInfo(originalInfo)));
         };
     }
-
-    /** Updates the shortcut child of this container based on the given shortcut info. */
-    private static class UpdateShortcutChild implements Runnable {
-        private final PopupContainerWithArrow mContainer;
-        private final DeepShortcutView mShortcutChild;
-        private final ShortcutInfo mShortcutChildInfo;
-        private final ShortcutInfoCompat mDetail;
-
-        public UpdateShortcutChild(PopupContainerWithArrow container, DeepShortcutView shortcutChild,
-                ShortcutInfo shortcutChildInfo, ShortcutInfoCompat detail) {
-            mContainer = container;
-            mShortcutChild = shortcutChild;
-            mShortcutChildInfo = shortcutChildInfo;
-            mDetail = detail;
-        }
-
-        @Override
-        public void run() {
-            mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail,
-                    mContainer.mShortcutsItemView);
-        }
-    }
-
-    /** Updates the notification child based on the given notification info. */
-    private static class UpdateNotificationChild implements Runnable {
-        private NotificationItemView mNotificationView;
-        private List<NotificationInfo> mNotificationInfos;
-
-        public UpdateNotificationChild(NotificationItemView notificationView,
-                List<NotificationInfo> notificationInfos) {
-            mNotificationView = notificationView;
-            mNotificationInfos = notificationInfos;
-        }
-
-        @Override
-        public void run() {
-            mNotificationView.applyNotificationInfos(mNotificationInfos);
-        }
-    }
-
-    /** Updates the system shortcut child based on the given shortcut info. */
-    private static class UpdateSystemShortcutChild implements Runnable {
-
-        private final PopupContainerWithArrow mContainer;
-        private final View mSystemShortcutChild;
-        private final SystemShortcut mSystemShortcutInfo;
-        private final Launcher mLauncher;
-        private final ItemInfo mItemInfo;
-
-        public UpdateSystemShortcutChild(PopupContainerWithArrow container, View systemShortcutChild,
-                SystemShortcut systemShortcut, Launcher launcher, ItemInfo originalInfo) {
-            mContainer = container;
-            mSystemShortcutChild = systemShortcutChild;
-            mSystemShortcutInfo = systemShortcut;
-            mLauncher = launcher;
-            mItemInfo = originalInfo;
-        }
-
-        @Override
-        public void run() {
-            final Context context = mSystemShortcutChild.getContext();
-            initializeSystemShortcut(context, mSystemShortcutChild, mSystemShortcutInfo);
-            mSystemShortcutChild.setOnClickListener(mSystemShortcutInfo
-                    .getOnClickListener(mLauncher, mItemInfo));
-        }
-    }
-
-    public static void initializeSystemShortcut(Context context, View view, SystemShortcut info) {
-        if (view instanceof DeepShortcutView) {
-            // Expanded system shortcut, with both icon and text shown on white background.
-            final DeepShortcutView shortcutView = (DeepShortcutView) view;
-            shortcutView.getIconView().setBackground(info.getIcon(context));
-            shortcutView.getBubbleText().setText(info.getLabel(context));
-        } else if (view instanceof ImageView) {
-            // Only the system shortcut icon shows on a gray background header.
-            final ImageView shortcutIcon = (ImageView) view;
-            shortcutIcon.setImageDrawable(info.getIcon(context));
-            shortcutIcon.setContentDescription(info.getLabel(context));
-        }
-        view.setTag(info);
-    }
 }
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index e709b93..c398aaa 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -1,9 +1,7 @@
 package com.android.launcher3.popup;
 
-import android.content.Context;
 import android.content.Intent;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.view.View;
 
@@ -31,20 +29,12 @@
  * Example system shortcuts, defined as inner classes, include Widgets and AppInfo.
  */
 public abstract class SystemShortcut extends ItemInfo {
-    private final int mIconResId;
-    private final int mLabelResId;
+    public final int iconResId;
+    public final int labelResId;
 
     public SystemShortcut(int iconResId, int labelResId) {
-        mIconResId = iconResId;
-        mLabelResId = labelResId;
-    }
-
-    public Drawable getIcon(Context context) {
-        return context.getResources().getDrawable(mIconResId, context.getTheme());
-    }
-
-    public String getLabel(Context context) {
-        return context.getString(mLabelResId);
+        this.iconResId = iconResId;
+        this.labelResId = labelResId;
     }
 
     public abstract View.OnClickListener getOnClickListener(final Launcher launcher,
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index 75a4886..450a690 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -29,6 +29,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 
 /**
  * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
@@ -42,6 +43,7 @@
 
     private BubbleTextView mBubbleText;
     private View mIconView;
+    private View mDivider;
 
     private ShortcutInfo mInfo;
     private ShortcutInfoCompat mDetail;
@@ -65,6 +67,11 @@
         super.onFinishInflate();
         mBubbleText = findViewById(R.id.bubble_text);
         mIconView = findViewById(R.id.icon);
+        mDivider = findViewById(R.id.divider);
+    }
+
+    public void setDividerVisibility(int visibility) {
+        mDivider.setVisibility(visibility);
     }
 
     public BubbleTextView getBubbleText() {
@@ -98,7 +105,7 @@
 
     /** package private **/
     public void applyShortcutInfo(ShortcutInfo info, ShortcutInfoCompat detail,
-            ShortcutsItemView container) {
+            PopupContainerWithArrow container) {
         mInfo = info;
         mDetail = detail;
         mBubbleText.applyFromShortcutInfo(info);
diff --git a/src/com/android/launcher3/shortcuts/ShortcutsItemView.java b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
deleted file mode 100644
index b4fa04e..0000000
--- a/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
+++ /dev/null
@@ -1,340 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.launcher3.shortcuts;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAnimUtils;
-import com.android.launcher3.R;
-import com.android.launcher3.anim.PropertyListBuilder;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.popup.PopupItemView;
-import com.android.launcher3.popup.PopupPopulator;
-import com.android.launcher3.popup.SystemShortcut;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * A {@link PopupItemView} that contains all of the {@link DeepShortcutView}s for an app,
- * as well as the system shortcuts such as Widgets and App Info.
- */
-public class ShortcutsItemView extends PopupItemView implements View.OnLongClickListener,
-        View.OnTouchListener, LogContainerProvider {
-
-    private static final String TAG = "ShortcutsItem";
-
-    private Launcher mLauncher;
-    private LinearLayout mContent;
-    private LinearLayout mShortcutsLayout;
-    private LinearLayout mSystemShortcutIcons;
-    private final Point mIconShift = new Point();
-    private final Point mIconLastTouchPos = new Point();
-    private final List<DeepShortcutView> mDeepShortcutViews = new ArrayList<>();
-    private final List<View> mSystemShortcutViews = new ArrayList<>();
-
-    private int mHiddenShortcutsHeight;
-
-    public ShortcutsItemView(Context context) {
-        this(context, null, 0);
-    }
-
-    public ShortcutsItemView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public ShortcutsItemView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        mLauncher = Launcher.getLauncher(context);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mContent = findViewById(R.id.content);
-        mShortcutsLayout = findViewById(R.id.shortcuts);
-    }
-
-    @Override
-    public boolean onTouch(View v, MotionEvent ev) {
-        // Touched a shortcut, update where it was touched so we can drag from there on long click.
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-            case MotionEvent.ACTION_MOVE:
-                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
-                break;
-        }
-        return false;
-    }
-
-    @Override
-    public boolean onLongClick(View v) {
-        // Return early if not the correct view
-        if (!(v.getParent() instanceof DeepShortcutView)) return false;
-        // Return early if global dragging is not enabled
-        if (!mLauncher.isDraggingEnabled()) return false;
-        // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts)
-        if (mLauncher.getDragController().isDragging()) return false;
-
-        // Long clicked on a shortcut.
-        DeepShortcutView sv = (DeepShortcutView) v.getParent();
-        sv.setWillDrawIcon(false);
-
-        // Move the icon to align with the center-top of the touch point
-        mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
-        mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
-
-        DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(),
-                (PopupContainerWithArrow) getParent(), sv.getFinalInfo(),
-                new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions());
-        dv.animateShift(-mIconShift.x, -mIconShift.y);
-
-        // TODO: support dragging from within folder without having to close it
-        AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
-        return false;
-    }
-
-    public void addShortcutView(View shortcutView, PopupPopulator.Item shortcutType) {
-        addShortcutView(shortcutView, shortcutType, -1);
-    }
-
-    private void addShortcutView(View shortcutView, PopupPopulator.Item shortcutType, int index) {
-        if (shortcutType == PopupPopulator.Item.SHORTCUT) {
-            mDeepShortcutViews.add((DeepShortcutView) shortcutView);
-        } else {
-            mSystemShortcutViews.add(shortcutView);
-        }
-        if (shortcutType == PopupPopulator.Item.SYSTEM_SHORTCUT_ICON) {
-            // System shortcut icons are added to a header that is separate from the full shortcuts.
-            if (mSystemShortcutIcons == null) {
-                mSystemShortcutIcons = (LinearLayout) mLauncher.getLayoutInflater().inflate(
-                        R.layout.system_shortcut_icons, mContent, false);
-                boolean iconsAreBelowShortcuts = mShortcutsLayout.getChildCount() > 0;
-                mContent.addView(mSystemShortcutIcons, iconsAreBelowShortcuts ? -1 : 0);
-            }
-            mSystemShortcutIcons.addView(shortcutView, index);
-        } else {
-            if (mShortcutsLayout.getChildCount() > 0) {
-                View prevChild = mShortcutsLayout.getChildAt(mShortcutsLayout.getChildCount() - 1);
-                if (prevChild instanceof DeepShortcutView) {
-                    prevChild.findViewById(R.id.divider).setVisibility(VISIBLE);
-                }
-            }
-            mShortcutsLayout.addView(shortcutView, index);
-        }
-    }
-
-    public List<DeepShortcutView> getDeepShortcutViews(boolean reverseOrder) {
-        if (reverseOrder) {
-            Collections.reverse(mDeepShortcutViews);
-        }
-        return mDeepShortcutViews;
-    }
-
-    public List<View> getSystemShortcutViews(boolean reverseOrder) {
-        // Always reverse system shortcut icons (in the header)
-        // so they are in priority order from right to left.
-        if (reverseOrder || mSystemShortcutIcons != null) {
-            Collections.reverse(mSystemShortcutViews);
-        }
-        return mSystemShortcutViews;
-    }
-
-    /**
-     * Hides shortcuts until only {@param maxShortcuts} are showing. Also sets
-     * {@link #mHiddenShortcutsHeight} to be the amount of extra space that shortcuts will
-     * require when {@link #showAllShortcuts(boolean)} is called.
-     */
-    public void hideShortcuts(boolean hideFromTop, int maxShortcuts) {
-        // When shortcuts are shown, they get more space allocated to them.
-        final int oldHeight = mShortcutsLayout.getChildAt(0).getLayoutParams().height;
-        final int newHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height);
-        mHiddenShortcutsHeight = (newHeight - oldHeight) * mShortcutsLayout.getChildCount();
-
-        int numToHide = mShortcutsLayout.getChildCount() - maxShortcuts;
-        if (numToHide <= 0) {
-            return;
-        }
-        final int numShortcuts = mShortcutsLayout.getChildCount();
-        final int dir = hideFromTop ? 1 : -1;
-        for (int i = hideFromTop ? 0 : numShortcuts - 1; 0 <= i && i < numShortcuts; i += dir) {
-            View child = mShortcutsLayout.getChildAt(i);
-            if (child instanceof DeepShortcutView) {
-                mHiddenShortcutsHeight += child.getLayoutParams().height;
-                child.setVisibility(GONE);
-                int prev = i + dir;
-                if (!hideFromTop && 0 <= prev && prev < numShortcuts) {
-                    // When hiding views from the bottom, make sure to hide the last divider.
-                    mShortcutsLayout.getChildAt(prev).findViewById(R.id.divider).setVisibility(GONE);
-                }
-                numToHide--;
-                if (numToHide == 0) {
-                    break;
-                }
-            }
-        }
-    }
-
-    public int getHiddenShortcutsHeight() {
-        return mHiddenShortcutsHeight;
-    }
-
-    /**
-     * Sets all shortcuts in {@link #mShortcutsLayout} to VISIBLE, then creates an
-     * animation to reveal the newly shown shortcuts.
-     *
-     * @see #hideShortcuts(boolean, int)
-     */
-    public Animator showAllShortcuts(boolean showFromTop) {
-        // First set all the shortcuts to VISIBLE.
-        final int numShortcuts = mShortcutsLayout.getChildCount();
-        if (numShortcuts == 0) {
-            Log.w(TAG, "Tried to show all shortcuts but there were no shortcuts to show");
-            return null;
-        }
-        final int oldHeight = mShortcutsLayout.getChildAt(0).getLayoutParams().height;
-        final int newHeight = getResources().getDimensionPixelSize(R.dimen.bg_popup_item_height);
-        for (int i = 0; i < numShortcuts; i++) {
-            DeepShortcutView view = (DeepShortcutView) mShortcutsLayout.getChildAt(i);
-            view.getLayoutParams().height = newHeight;
-            view.requestLayout();
-            view.setVisibility(VISIBLE);
-            if (i < numShortcuts - 1) {
-                view.findViewById(R.id.divider).setVisibility(VISIBLE);
-            }
-        }
-
-        // Now reveal the newly shown shortcuts.
-        AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
-
-        if (showFromTop) {
-            // The new shortcuts pushed the original shortcuts down, but we want to animate them
-            // to that position. So we revert the translation and animate to the new.
-            animation.play(translateYFrom(mShortcutsLayout, -mHiddenShortcutsHeight));
-        } else if (mSystemShortcutIcons != null) {
-            // When adding the shortcuts from the bottom, things are a little trickier, since
-            // that means they push the icons header down. To account for this, we do the same
-            // translation trick as above, but on the header. Since this means leaving behind
-            // a blank area where the header was, we also need to clip the background.
-            animation.play(translateYFrom(mSystemShortcutIcons, -mHiddenShortcutsHeight));
-            // mPillRect is the bounds of this view before the new shortcuts were shown.
-            Rect backgroundStartRect = new Rect(mPillRect);
-            Rect backgroundEndRect = new Rect(mPillRect);
-            backgroundEndRect.bottom += mHiddenShortcutsHeight;
-            animation.play(new RoundedRectRevealOutlineProvider(getBackgroundRadius(),
-                    getBackgroundRadius(), backgroundStartRect, backgroundEndRect, mRoundedCorners)
-                    .createRevealAnimator(this, false));
-        }
-        for (int i = 0; i < numShortcuts; i++) {
-            // Animate each shortcut to its new height.
-            DeepShortcutView shortcut = (DeepShortcutView) mShortcutsLayout.getChildAt(i);
-            int heightDiff = newHeight - oldHeight;
-            int heightAdjustmentIndex = showFromTop ? numShortcuts - i - 1 : i;
-            int fromDir = showFromTop ? 1 : -1;
-            animation.play(translateYFrom(shortcut, heightDiff * heightAdjustmentIndex * fromDir));
-            // Make sure the text and icon stay centered in the shortcut.
-            animation.play(translateYFrom(shortcut.getBubbleText(), heightDiff / 2 * fromDir));
-            animation.play(translateYFrom(shortcut.getIconView(), heightDiff / 2 * fromDir));
-            // Scale icons back up to full size.
-            animation.play(LauncherAnimUtils.ofPropertyValuesHolder(shortcut.getIconView(),
-                    new PropertyListBuilder().scale(1f).build()));
-        }
-        return animation;
-    }
-
-    /**
-     * Animates the translationY of the view from the given offset to the view's current translation
-     * @return an Animator, which should be started by the caller.
-     */
-    private Animator translateYFrom(View v, int diff) {
-        float finalY = v.getTranslationY();
-        return ObjectAnimator.ofFloat(v, TRANSLATION_Y, finalY + diff, finalY);
-    }
-
-    /**
-     * Adds a {@link SystemShortcut.Widgets} item if there are widgets for the given ItemInfo.
-     */
-    public void enableWidgetsIfExist(final BubbleTextView originalIcon) {
-        ItemInfo itemInfo = (ItemInfo) originalIcon.getTag();
-        SystemShortcut widgetInfo = new SystemShortcut.Widgets();
-        View.OnClickListener onClickListener = widgetInfo.getOnClickListener(mLauncher, itemInfo);
-        View widgetsView = null;
-        for (View systemShortcutView : mSystemShortcutViews) {
-            if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
-                widgetsView = systemShortcutView;
-                break;
-            }
-        }
-        final PopupPopulator.Item widgetsItem = mSystemShortcutIcons == null
-                ? PopupPopulator.Item.SYSTEM_SHORTCUT
-                : PopupPopulator.Item.SYSTEM_SHORTCUT_ICON;
-        if (onClickListener != null && widgetsView == null) {
-            // We didn't have any widgets cached but now there are some, so enable the shortcut.
-            widgetsView = mLauncher.getLayoutInflater().inflate(widgetsItem.layoutId, this, false);
-            PopupPopulator.initializeSystemShortcut(getContext(), widgetsView, widgetInfo);
-            widgetsView.setOnClickListener(onClickListener);
-            if (widgetsItem == PopupPopulator.Item.SYSTEM_SHORTCUT_ICON) {
-                addShortcutView(widgetsView, widgetsItem, 0);
-            } else {
-                // If using the expanded system shortcut (as opposed to just the icon), we need to
-                // 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);
-                PopupContainerWithArrow.showForIcon(originalIcon);
-            }
-        } else if (onClickListener == null && widgetsView != null) {
-            // No widgets exist, but we previously added the shortcut so remove it.
-            if (widgetsItem == PopupPopulator.Item.SYSTEM_SHORTCUT_ICON) {
-                mSystemShortcutViews.remove(widgetsView);
-                mSystemShortcutIcons.removeView(widgetsView);
-            } else {
-                ((PopupContainerWithArrow) getParent()).close(false);
-                PopupContainerWithArrow.showForIcon(originalIcon);
-            }
-        }
-    }
-
-    @Override
-    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
-            LauncherLogProto.Target targetParent) {
-        target.itemType = LauncherLogProto.ItemType.DEEPSHORTCUT;
-        target.rank = info.rank;
-        targetParent.containerType = LauncherLogProto.ContainerType.DEEPSHORTCUTS;
-    }
-}
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
index c6feefc..f084fd2 100644
--- a/src/com/android/launcher3/states/InternalStateHandler.java
+++ b/src/com/android/launcher3/states/InternalStateHandler.java
@@ -17,42 +17,52 @@
 
 import android.content.Intent;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.IBinder;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Launcher.OnResumeCallback;
 
 /**
- * Utility class to sending state handling logic to Launcher from within the same process
+ * Utility class to sending state handling logic to Launcher from within the same process.
+ *
+ * Extending {@link Binder} ensures that the platform maintains a single instance of each object
+ * which allows this object to safely navigate the system process.
  */
 public abstract class InternalStateHandler extends Binder implements OnResumeCallback {
 
     public static final String EXTRA_STATE_HANDLER = "launcher.state_handler";
 
-    public abstract void onCreate(Launcher launcher);
-    public abstract void onNewIntent(Launcher launcher, boolean alreadyOnHome);
+    protected abstract void init(Launcher launcher, boolean alreadyOnHome);
 
-    public static void handleCreate(Launcher launcher, Intent intent) {
-        if (intent.getExtras() != null) {
-            IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
-            if (stateBinder instanceof InternalStateHandler) {
-                InternalStateHandler handler = (InternalStateHandler) stateBinder;
-                launcher.setOnResumeCallback(handler);
-                handler.onCreate(launcher);
-            }
-            intent.getExtras().remove(EXTRA_STATE_HANDLER);
-        }
+    public final Intent addToIntent(Intent intent) {
+        Bundle extras = new Bundle();
+        extras.putBinder(EXTRA_STATE_HANDLER, this);
+        intent.putExtras(extras);
+        return intent;
     }
 
-    public static void handleNewIntent(Launcher launcher, Intent intent, boolean alreadyOnHome) {
-        if (intent.getExtras() != null) {
+    public static boolean handleCreate(Launcher launcher, Intent intent) {
+        return handleIntent(launcher, intent, false);
+    }
+
+    public static boolean handleNewIntent(Launcher launcher, Intent intent, boolean alreadyOnHome) {
+        return handleIntent(launcher, intent, alreadyOnHome);
+    }
+
+    private static boolean handleIntent(
+            Launcher launcher, Intent intent, boolean alreadyOnHome) {
+        boolean result = false;
+        if (intent != null && intent.getExtras() != null) {
             IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
             if (stateBinder instanceof InternalStateHandler) {
                 InternalStateHandler handler = (InternalStateHandler) stateBinder;
                 launcher.setOnResumeCallback(handler);
-                handler.onNewIntent(launcher, alreadyOnHome);
+                handler.init(launcher, alreadyOnHome);
+                result = true;
             }
             intent.getExtras().remove(EXTRA_STATE_HANDLER);
         }
+        return result;
     }
 }
diff --git a/src/com/android/launcher3/util/TraceHelper.java b/src/com/android/launcher3/util/TraceHelper.java
index 5b66fcd..0f3ac57 100644
--- a/src/com/android/launcher3/util/TraceHelper.java
+++ b/src/com/android/launcher3/util/TraceHelper.java
@@ -33,9 +33,8 @@
 
     private static final boolean ENABLED = FeatureFlags.IS_DOGFOOD_BUILD;
 
-    private static final boolean SYSTEM_TRACE = true;
-    private static final ArrayMap<String, MutableLong> sUpTimes =
-            ENABLED ? new ArrayMap<String, MutableLong>() : null;
+    private static final boolean SYSTEM_TRACE = false;
+    private static final ArrayMap<String, MutableLong> sUpTimes = ENABLED ? new ArrayMap<>() : null;
 
     public static void beginSection(String sectionName) {
         if (ENABLED) {
diff --git a/src/com/android/launcher3/views/SlidingTabStrip.java b/src/com/android/launcher3/views/SlidingTabStrip.java
new file mode 100644
index 0000000..45c6261
--- /dev/null
+++ b/src/com/android/launcher3/views/SlidingTabStrip.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+package com.android.launcher3.views;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class SlidingTabStrip extends LinearLayout {
+
+    private final Paint mSelectedIndicatorPaint;
+    private int mSelectedIndicatorHeight;
+    private int mIndicatorLeft = -1;
+    private int mIndicatorRight = -1;
+    private int mSelectedPosition = -1;
+    private float mSelectionOffset;
+
+    public SlidingTabStrip(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        setOrientation(HORIZONTAL);
+        setWillNotDraw(false);
+        mSelectedIndicatorPaint = new Paint();
+        mSelectedIndicatorPaint.setColor(Themes.getAttrColor(context, android.R.attr.colorAccent));
+        mSelectedIndicatorHeight = getResources()
+                .getDimensionPixelSize(R.dimen.all_apps_tabs_indicator_height);
+    }
+
+    public void updateIndicatorPosition(int position, float positionOffset) {
+        mSelectedPosition = position;
+        mSelectionOffset = positionOffset;
+        updateIndicatorPosition();
+    }
+
+    public void updateTabTextColor(int pos) {
+        for (int i=0; i < getChildCount(); i++) {
+            Button tab = (Button) getChildAt(i);
+            tab.setSelected(i == pos);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        updateTabTextColor(0);
+        updateIndicatorPosition(0, 0);
+    }
+
+    private void updateIndicatorPosition() {
+        final View tab = getChildAt(mSelectedPosition);
+        int left, right;
+
+        if (tab != null && tab.getWidth() > 0) {
+            left = tab.getLeft();
+            right = tab.getRight();
+
+            if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
+                // Draw the selection partway between the tabs
+                View nextTitle = getChildAt(mSelectedPosition + 1);
+                left = (int) (mSelectionOffset * nextTitle.getLeft() +
+                        (1.0f - mSelectionOffset) * left);
+                right = (int) (mSelectionOffset * nextTitle.getRight() +
+                        (1.0f - mSelectionOffset) * right);
+            }
+        } else {
+            left = right = -1;
+        }
+
+        setIndicatorPosition(left, right);
+    }
+
+    private void setIndicatorPosition(int left, int right) {
+        if (left != mIndicatorLeft || right != mIndicatorRight) {
+            mIndicatorLeft = left;
+            mIndicatorRight = right;
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
+                mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
+    }
+}
\ No newline at end of file
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
index c339634..dcf7453 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/OverviewState.java
@@ -34,7 +34,7 @@
 public class OverviewState extends LauncherState {
 
     // The percent to shrink the workspace during overview mode
-    public static final float SCALE_FACTOR = 0.7f;
+    private static final float SCALE_FACTOR = 0.7f;
 
     private static final int STATE_FLAGS = FLAG_SHOW_SCRIM | FLAG_MULTI_PAGE;
 
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
index 8521334..51cf661 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/UiFactory.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.uioverrides;
 
+import static com.android.launcher3.LauncherState.OVERVIEW;
+
 import android.view.View.AccessibilityDelegate;
 
 import com.android.launcher3.Launcher;
@@ -39,4 +41,8 @@
                 (OverviewPanel) launcher.getOverviewPanel(),
                 launcher.getAllAppsController(), launcher.getWorkspace() };
     }
+
+    public static void onWorkspaceLongPress(Launcher launcher) {
+        launcher.getStateManager().goToState(OVERVIEW);
+    }
 }