Merge "Refactor InsettableFrameLayout/DragLayer and add StateListener interface." into ub-launcher3-master
diff --git a/quickstep/libs/sysui_shared.jar b/quickstep/libs/sysui_shared.jar
index f9ce6e0..ef50ac4 100644
--- a/quickstep/libs/sysui_shared.jar
+++ b/quickstep/libs/sysui_shared.jar
Binary files differ
diff --git a/quickstep/res/drawable/task_thumbnail_background.xml b/quickstep/res/drawable/task_thumbnail_background.xml
new file mode 100644
index 0000000..603380e
--- /dev/null
+++ b/quickstep/res/drawable/task_thumbnail_background.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.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <solid android:color="#FF000000" />
+    <corners android:radius="2dp" />
+</shape>
diff --git a/quickstep/res/layout/overview_panel.xml b/quickstep/res/layout/overview_panel.xml
index 466470f..a8b91c5 100644
--- a/quickstep/res/layout/overview_panel.xml
+++ b/quickstep/res/layout/overview_panel.xml
@@ -19,30 +19,8 @@
     android:theme="@style/HomeScreenElementTheme"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layout_gravity="center_horizontal|bottom"
-    android:gravity="top">
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:orientation="horizontal">
-
-        <View
-            android:layout_width="300dp"
-            android:layout_height="match_parent"
-            android:background="#44FF0000"
-            android:layout_marginEnd="10dp"/>
-
-        <View
-            android:layout_width="300dp"
-            android:layout_height="match_parent"
-            android:background="#4400FF00"
-            android:layout_marginEnd="10dp"/>
-
-        <View
-            android:layout_width="300dp"
-            android:layout_height="match_parent"
-            android:background="#440000FF" />
-    </LinearLayout>
-
-</com.android.quickstep.RecentsView>
\ No newline at end of file
+    android:layout_gravity="center"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:alpha="0.0"
+    android:visibility="invisible" />
\ No newline at end of file
diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml
new file mode 100644
index 0000000..fdf1adc
--- /dev/null
+++ b/quickstep/res/layout/task.xml
@@ -0,0 +1,36 @@
+<?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.quickstep.TaskView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.android.quickstep.TaskThumbnailView
+        android:id="@+id/snapshot"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="24dp"
+        android:scaleType="matrix"
+        android:background="@drawable/task_thumbnail_background"
+        android:elevation="4dp" />
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="48dp"
+        android:layout_height="48dp"
+        android:layout_gravity="top|center_horizontal"
+        android:elevation="5dp"/>
+</com.android.quickstep.TaskView>
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
index 9bdd7a3..26f5d5b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java
@@ -46,12 +46,14 @@
 
     @Override
     public void onStateEnabled(Launcher launcher) {
-        ((RecentsView) launcher.getOverviewPanel()).setViewVisible(true);
+        RecentsView rv = launcher.getOverviewPanel();
+        rv.setOverviewStateEnabled(true);
     }
 
     @Override
     public void onStateDisabled(Launcher launcher) {
-        ((RecentsView) launcher.getOverviewPanel()).setViewVisible(false);
+        RecentsView rv = launcher.getOverviewPanel();
+        rv.setOverviewStateEnabled(false);
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
new file mode 100644
index 0000000..1a8ae2a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/NavBarSwipeInteractionHandler.java
@@ -0,0 +1,296 @@
+/*
+ * 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.ObjectAnimator;
+import android.animation.RectEvaluator;
+import android.annotation.TargetApi;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Build;
+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 com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+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.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.Task.TaskKey;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+@TargetApi(Build.VERSION_CODES.O)
+public class NavBarSwipeInteractionHandler extends InternalStateHandler implements FrameCallback {
+
+    private static FloatProperty<NavBarSwipeInteractionHandler> SHIFT =
+            new FloatProperty<NavBarSwipeInteractionHandler>("currentShift") {
+        @Override
+        public void setValue(NavBarSwipeInteractionHandler handler, float v) {
+            handler.setShift(v);
+        }
+
+        @Override
+        public Float get(NavBarSwipeInteractionHandler handler) {
+            return handler.mCurrentShift;
+        }
+    };
+
+    // 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;
+
+    private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
+
+    private final Rect mStableInsets = new Rect();
+    private final Rect mSourceRect = new Rect();
+    private final Rect mTargetRect = new Rect();
+    private final Rect mCurrentRect = new Rect();
+    private final RectEvaluator mRectEvaluator = new RectEvaluator(mCurrentRect);
+
+    private final Bitmap mTaskSnapshot;
+    private final 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;
+
+    // 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;
+
+    NavBarSwipeInteractionHandler(Bitmap taskSnapShot, RunningTaskInfo runningTaskInfo) {
+        mTaskSnapshot = taskSnapShot;
+        mRunningTaskId = runningTaskInfo.id;
+        WindowManagerWrapper.getInstance().getStableInsets(mStableInsets);
+    }
+
+    @Override
+    public void onLauncherResume() {
+        mStartDelta = mCurrentDisplacement;
+        mLastDelta = mStartDelta;
+        mChoreographer = Choreographer.getInstance();
+
+        scheduleNextFrame();
+    }
+
+    @Override
+    public void onCreate(Launcher launcher) {
+        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
+        mRecentsView.update(consumeLastLoadPlan());
+        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, false /* animate */);
+    }
+
+    @Override
+    public void onNewIntent(Launcher launcher, boolean alreadyOnHome) {
+        mLauncher = launcher;
+        mDragView = new SnapshotDragView(mLauncher, mTaskSnapshot);
+        mLauncher.getDragLayer().addView(mDragView);
+        mDragView.setPivotX(0);
+        mDragView.setPivotY(0);
+        mRecentsView = mLauncher.getOverviewPanel();
+        mHotseat = mLauncher.getHotseat();
+
+        // Optimization
+        mLauncher.getAppsView().setVisibility(View.GONE);
+
+        // Launch overview, animate if already on home
+        mRecentsView.update(consumeLastLoadPlan());
+        mLauncher.getStateManager().goToState(LauncherState.OVERVIEW, alreadyOnHome);
+    }
+
+    /**
+     * 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
+    public void updateDisplacement(float displacement) {
+        mCurrentDisplacement = displacement;
+    }
+
+    @BinderThread
+    public void endTouch(float endVelocity) {
+        mEndVelocity = endVelocity;
+        mTouchEnded = true;
+    }
+
+    @UiThread
+    private void scheduleNextFrame() {
+        if (!mTouchEnded) {
+            mChoreographer.postFrameCallback(this);
+        } else {
+            animateToFinalShift();
+        }
+    }
+
+    @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) {
+        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());
+        }
+
+        if (!mSourceRect.isEmpty()) {
+            mCurrentShift = shift;
+            int hotseatHeight = mHotseat.getHeight();
+            mHotseat.setTranslationY((1 - shift) * hotseatHeight);
+
+            mRectEvaluator.evaluate(shift, mSourceRect, mTargetRect);
+
+            float scale = (float) mCurrentRect.width() / mSourceRect.width();
+            mDragView.setTranslationX(mCurrentRect.left - mStableInsets.left * scale * shift);
+            mDragView.setTranslationY(mCurrentRect.top - mStableInsets.top * scale * shift);
+            mDragView.setScaleX(scale);
+            mDragView.setScaleY(scale);
+            mDragView.getViewBounds().setClipTop((int) (mStableInsets.top * shift));
+            mDragView.getViewBounds().setClipBottom((int) (mStableInsets.bottom * shift));
+        }
+    }
+
+    void setLastLoadPlan(Future<RecentsTaskLoadPlan> futureLoadPlan) {
+        if (mFutureLoadPlan != null) {
+            mFutureLoadPlan.cancel(true);
+        }
+        mFutureLoadPlan = futureLoadPlan;
+    }
+
+    private RecentsTaskLoadPlan consumeLastLoadPlan() {
+        try {
+            if (mFutureLoadPlan != null) {
+                return mFutureLoadPlan.get();
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            e.printStackTrace();
+        } finally {
+            mFutureLoadPlan = null;
+        }
+        return null;
+    }
+
+    @UiThread
+    private void animateToFinalShift() {
+        float flingThreshold = Utilities.pxFromDp(FLING_THRESHOLD_VELOCITY,
+                    mLauncher.getResources().getDisplayMetrics());
+        boolean isFling = Math.abs(mEndVelocity) > flingThreshold;
+
+        long duration = 200;
+        final float endShift;
+        if (!isFling) {
+            endShift = mCurrentShift >= 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();
+
+                // 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));
+            }
+        }
+
+        ObjectAnimator anim = ObjectAnimator.ofFloat(this, SHIFT, 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);
+                }
+            }
+        });
+        anim.start();
+    }
+
+    @UiThread
+    private void resumeLastTask() {
+        // 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);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 8f75980..a0340b6 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -40,7 +40,7 @@
         plan.preloadPlan(new RecentsTaskLoader(this, 1, 1, 0), -1, UserHandle.myUserId());
 
         mAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
-        mAdapter.addAll(plan.getTaskStack().getStackTasks());
+        mAdapter.addAll(plan.getTaskStack().getTasks());
         setListAdapter(mAdapter);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index d85de8f..528b11d 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -19,17 +19,44 @@
 import android.content.Context;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.widget.HorizontalScrollView;
+import android.view.LayoutInflater;
 
 import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.Insettable;
 import com.android.launcher3.Launcher;
+import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.RecentsTaskLoader;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.TaskStack;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.WindowManagerWrapper;
+
+import java.util.ArrayList;
 
 /**
- * A placeholder view for recents
+ * A list of recent tasks.
  */
-public class RecentsView extends HorizontalScrollView implements Insettable {
+public class RecentsView extends PagedView {
+
+    private boolean mOverviewStateEnabled;
+    private boolean mTaskStackListenerRegistered;
+
+    private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
+        @Override
+        public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
+            for (int i = 0; i < getChildCount(); i++) {
+                final TaskView taskView = (TaskView) getChildAt(i);
+                if (taskView.getTask().key.id == taskId) {
+                    taskView.getThumbnail().setThumbnail(snapshot);
+                    return;
+                }
+            }
+        }
+    };
+
     public RecentsView(Context context) {
         this(context, null);
     }
@@ -40,24 +67,103 @@
 
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        setAlpha(0);
-        setVisibility(INVISIBLE);
+        setWillNotDraw(false);
+        setPageSpacing((int) getResources().getDimension(R.dimen.recents_page_spacing));
     }
 
-    public void setViewVisible(boolean isVisible) { }
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        // TODO: These are rough calculations which currently use the stable insets
+        DeviceProfile profile = Launcher.getLauncher(getContext()).getDeviceProfile();
+        Rect stableInsets = new Rect();
+        WindowManagerWrapper.getInstance().getStableInsets(stableInsets);
+        Rect padding = profile.getWorkspacePadding(null);
+        float taskWidth = profile.getCurrentWidth() - stableInsets.left - stableInsets.right;
+        float taskHeight = profile.getCurrentHeight() - stableInsets.top - stableInsets.bottom;
+        float overviewHeight = profile.availableHeightPx - padding.top - padding.bottom
+                - stableInsets.top;
+        float overviewWidth = taskWidth * overviewHeight / taskHeight;
+        padding.left = padding.right = (int) ((profile.availableWidthPx - overviewWidth) / 2);
+        setPadding(padding.left, padding.top, padding.right, padding.bottom);
+    }
 
     @Override
-    public void setInsets(Rect insets) {
-        MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
-        lp.topMargin = insets.top;
-        lp.bottomMargin = insets.bottom;
-        lp.leftMargin = insets.left;
-        lp.rightMargin = insets.right;
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        updateTaskStackListenerState();
+    }
 
-        DeviceProfile dp = Launcher.getLauncher(getContext()).getDeviceProfile();
-        if (!dp.isVerticalBarLayout()) {
-             lp.bottomMargin += dp.hotseatBarSizePx + getResources().getDimensionPixelSize(
-                     R.dimen.dynamic_grid_min_page_indicator_size);
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        updateTaskStackListenerState();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        updateTaskStackListenerState();
+    }
+
+    public void setOverviewStateEnabled(boolean enabled) {
+        mOverviewStateEnabled = enabled;
+        updateTaskStackListenerState();
+    }
+
+    public void update(RecentsTaskLoadPlan loadPlan) {
+        final RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+        setCurrentPage(0);
+        TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
+        if (stack == null) {
+            removeAllViews();
+            return;
+        }
+
+        // Ensure there are as many views as there are tasks in the stack (adding and trimming as
+        // necessary)
+        final LayoutInflater inflater = LayoutInflater.from(getContext());
+        final ArrayList<Task> tasks = stack.getTasks();
+        for (int i = getChildCount(); i < tasks.size(); i++) {
+            final TaskView taskView = (TaskView) inflater.inflate(R.layout.task, this, false);
+            addView(taskView);
+        }
+        while (getChildCount() > tasks.size()) {
+            final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1);
+            removeView(taskView);
+            loader.unloadTaskData(taskView.getTask());
+        }
+
+        // Rebind all task views
+        for (int i = tasks.size() - 1; i >= 0; i--) {
+            final Task task = tasks.get(i);
+            final TaskView taskView = (TaskView) getChildAt(tasks.size() - i - 1);
+            taskView.bind(task);
+            loader.loadTaskData(task);
+        }
+    }
+
+    public void launchTaskWithId(int taskId) {
+        for (int i = 0; i < getChildCount(); i++) {
+            final TaskView taskView = (TaskView) getChildAt(i);
+            if (taskView.getTask().key.id == taskId) {
+                taskView.launchTask(false /* animate */);
+                return;
+            }
+        }
+    }
+
+    private void updateTaskStackListenerState() {
+        boolean registerStackListener = mOverviewStateEnabled && isAttachedToWindow()
+                && getWindowVisibility() == VISIBLE;
+        if (registerStackListener != mTaskStackListenerRegistered) {
+            if (registerStackListener) {
+                ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
+            } else {
+                ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
+            }
+            mTaskStackListenerRegistered = registerStackListener;
         }
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SimpleTaskView.java b/quickstep/src/com/android/quickstep/SimpleTaskView.java
new file mode 100644
index 0000000..8425fa3
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SimpleTaskView.java
@@ -0,0 +1,52 @@
+/*
+ * 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.content.Context;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * A simple view which keeps its size proportional to the display size
+ */
+public class SimpleTaskView extends View {
+
+    private static final Point sTempPoint = new Point();
+
+    public SimpleTaskView(Context context) {
+        super(context);
+    }
+
+    public SimpleTaskView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SimpleTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+        getContext().getSystemService(WindowManager.class)
+                .getDefaultDisplay().getRealSize(sTempPoint);
+
+        int width = (int) ((float) height * sTempPoint.x / sTempPoint.y);
+        setMeasuredDimension(width, height);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/SnapshotDragView.java b/quickstep/src/com/android/quickstep/SnapshotDragView.java
new file mode 100644
index 0000000..2ef3942
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/SnapshotDragView.java
@@ -0,0 +1,92 @@
+/*
+ * 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.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.systemui.shared.recents.view.AnimateableViewBounds;
+
+/**
+ * Floating view which shows the task snapshot allowing it to be dragged and placed.
+ */
+public class SnapshotDragView extends AbstractFloatingView implements Insettable {
+
+    private final Launcher mLauncher;
+    private final Bitmap mSnapshot;
+    private final AnimateableViewBounds mViewBounds;
+
+    public SnapshotDragView(Launcher launcher, Bitmap snapshot) {
+        super(launcher, null);
+        mLauncher = launcher;
+        mSnapshot = snapshot;
+        mViewBounds = new AnimateableViewBounds(this, 0);
+        setWillNotDraw(false);
+        setClipToOutline(true);
+        setOutlineProvider(mViewBounds);
+    }
+
+    AnimateableViewBounds getViewBounds() {
+        return mViewBounds;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mSnapshot != null) {
+            setMeasuredDimension(mSnapshot.getWidth(), mSnapshot.getHeight());
+        } else {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    @Override
+    public void setInsets(Rect insets) {
+
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mSnapshot != null) {
+            canvas.drawBitmap(mSnapshot, 0, 0, null);
+        }
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        // We dont suupport animate.
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        // We should probably log the weather
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_QUICKSTEP_PREVIEW) != 0;
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
new file mode 100644
index 0000000..6e8bbeb
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -0,0 +1,179 @@
+/*
+ * 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.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LightingColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * A task in the Recents view.
+ */
+public class TaskThumbnailView extends FrameLayout {
+
+    private ThumbnailData mThumbnailData;
+
+    private Rect mThumbnailRect = new Rect();
+    private float mThumbnailScale;
+
+    private Matrix mMatrix = new Matrix();
+    private Paint mDrawPaint = new Paint();
+    protected Paint mBgFillPaint = new Paint();
+    protected BitmapShader mBitmapShader;
+
+    private float mDimAlpha = 1f;
+    private LightingColorFilter mLightingColorFilter = new LightingColorFilter(Color.WHITE, 0);
+
+    public TaskThumbnailView(Context context) {
+        this(context, null);
+    }
+
+    public TaskThumbnailView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        setWillNotDraw(false);
+        setClipToOutline(true);
+    }
+
+    /**
+     * Updates this thumbnail.
+     */
+    public void setThumbnail(ThumbnailData thumbnailData) {
+        if (thumbnailData != null && thumbnailData.thumbnail != null) {
+            Bitmap bm = thumbnailData.thumbnail;
+            bm.prepareToDraw();
+            mThumbnailScale = thumbnailData.scale;
+            mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+            mDrawPaint.setShader(mBitmapShader);
+            mThumbnailRect.set(0, 0,
+                    bm.getWidth() - thumbnailData.insets.left - thumbnailData.insets.right,
+                    bm.getHeight() - thumbnailData.insets.top - thumbnailData.insets.bottom);
+            mThumbnailData = thumbnailData;
+            updateThumbnailMatrix();
+            updateThumbnailPaintFilter();
+        } else {
+            mBitmapShader = null;
+            mDrawPaint.setShader(null);
+            mThumbnailRect.setEmpty();
+            mThumbnailData = null;
+        }
+    }
+
+    /**
+     * Sets the alpha of the dim layer on top of this view.
+     */
+    public void setDimAlpha(float dimAlpha) {
+        mDimAlpha = dimAlpha;
+        updateThumbnailPaintFilter();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        int viewWidth = getMeasuredWidth();
+        int viewHeight = getMeasuredHeight();
+        int thumbnailWidth = Math.min(viewWidth,
+                (int) (mThumbnailRect.width() * mThumbnailScale));
+        int thumbnailHeight = Math.min(viewHeight,
+                (int) (mThumbnailRect.height() * mThumbnailScale));
+
+        if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
+            // Draw the background, there will be some small overdraw with the thumbnail
+            if (thumbnailWidth < viewWidth) {
+                // Portrait thumbnail on a landscape task view
+                canvas.drawRect(Math.max(0, thumbnailWidth), 0, viewWidth, viewHeight,
+                        mBgFillPaint);
+            }
+            if (thumbnailHeight < viewHeight) {
+                // Landscape thumbnail on a portrait task view
+                canvas.drawRect(0, Math.max(0, thumbnailHeight), viewWidth, viewHeight,
+                        mBgFillPaint);
+            }
+
+            // Draw the thumbnail
+            canvas.drawRect(0, 0, thumbnailWidth, thumbnailHeight, mDrawPaint);
+        } else {
+            canvas.drawRect(0, 0, viewWidth, viewHeight, mBgFillPaint);
+        }
+    }
+
+    private void updateThumbnailPaintFilter() {
+        int mul = (int) (mDimAlpha * 255);
+        if (mBitmapShader != null) {
+            mLightingColorFilter = new LightingColorFilter(Color.argb(255, mul, mul, mul), 0);
+            mDrawPaint.setColorFilter(mLightingColorFilter);
+            mDrawPaint.setColor(0xFFffffff);
+            mBgFillPaint.setColorFilter(mLightingColorFilter);
+        } else {
+            mDrawPaint.setColorFilter(null);
+            mDrawPaint.setColor(Color.argb(255, mul, mul, mul));
+        }
+        invalidate();
+    }
+
+    private void updateThumbnailMatrix() {
+        mThumbnailScale = 1f;
+        if (mBitmapShader != null && mThumbnailData != null) {
+            if (getMeasuredWidth() == 0) {
+                // If we haven't measured , skip the thumbnail drawing and only draw the background
+                // color
+                mThumbnailScale = 0f;
+            } else {
+                float invThumbnailScale = 1f / mThumbnailScale;
+                final Configuration configuration =
+                        getContext().getApplicationContext().getResources().getConfiguration();
+                final DeviceProfile profile = Launcher.getLauncher(getContext()).getDeviceProfile();
+                if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
+                    if (mThumbnailData.orientation == Configuration.ORIENTATION_PORTRAIT) {
+                        // If we are in the same orientation as the screenshot, just scale it to the
+                        // width of the task view
+                        mThumbnailScale = (float) getMeasuredWidth() / mThumbnailRect.width();
+                    } else {
+                        // Scale the landscape thumbnail up to app size, then scale that to the task
+                        // view size to match other portrait screenshots
+                        mThumbnailScale = invThumbnailScale *
+                                ((float) getMeasuredWidth() / profile.getCurrentWidth());
+                    }
+                } else {
+                    // Otherwise, scale the screenshot to fit 1:1 in the current orientation
+                    mThumbnailScale = invThumbnailScale;
+                }
+            }
+            mMatrix.setTranslate(-mThumbnailData.insets.left, -mThumbnailData.insets.top);
+            mMatrix.postScale(mThumbnailScale, mThumbnailScale);
+            mBitmapShader.setLocalMatrix(mMatrix);
+        }
+        invalidate();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TaskView.java b/quickstep/src/com/android/quickstep/TaskView.java
new file mode 100644
index 0000000..029afd6
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/TaskView.java
@@ -0,0 +1,136 @@
+/*
+ * 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.app.ActivityOptions;
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.launcher3.R;
+import com.android.launcher3.uioverrides.OverviewState;
+import com.android.systemui.shared.recents.model.Task;
+import com.android.systemui.shared.recents.model.Task.TaskCallbacks;
+import com.android.systemui.shared.recents.model.ThumbnailData;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
+import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
+import com.android.systemui.shared.recents.view.RecentsTransition;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A task in the Recents view.
+ */
+public class TaskView extends FrameLayout implements TaskCallbacks {
+
+    private Task mTask;
+    private TaskThumbnailView mSnapshotView;
+    private ImageView mIconView;
+
+    public TaskView(Context context) {
+        this(context, null);
+    }
+
+    public TaskView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        setOnClickListener((view) -> {
+            launchTask(true /* animate */);
+        });
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mSnapshotView = findViewById(R.id.snapshot);
+        mIconView = findViewById(R.id.icon);
+    }
+
+    /**
+     * Updates this task view to the given {@param task}.
+     */
+    public void bind(Task task) {
+        if (mTask != null) {
+            mTask.removeCallback(this);
+        }
+        mTask = task;
+        task.addCallback(this);
+    }
+
+    public Task getTask() {
+        return mTask;
+    }
+
+    public TaskThumbnailView getThumbnail() {
+        return mSnapshotView;
+    }
+
+    public void launchTask(boolean animate) {
+        if (mTask != null) {
+            final ActivityOptions opts;
+            if (animate) {
+                // Calculate the bounds of the thumbnail to animate from
+                final Rect bounds = new Rect();
+                final int[] pos = new int[2];
+                mSnapshotView.getLocationInWindow(pos);
+                bounds.set(pos[0], pos[1],
+                        pos[0] + mSnapshotView.getWidth(),
+                        pos[1] + mSnapshotView.getHeight());
+                AppTransitionAnimationSpecsFuture animFuture =
+                        new AppTransitionAnimationSpecsFuture(getHandler()) {
+                            @Override
+                            public List<AppTransitionAnimationSpecCompat> composeSpecs() {
+                                ArrayList<AppTransitionAnimationSpecCompat> specs =
+                                        new ArrayList<>();
+                                specs.add(new AppTransitionAnimationSpecCompat(mTask.key.id, null,
+                                        bounds));
+                                return specs;
+                            }
+                        };
+                opts = RecentsTransition.createAspectScaleAnimation(
+                        getContext(), getHandler(), true /* scaleUp */, animFuture, null);
+            } else {
+                opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0);
+            }
+            ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
+                    opts, null, null);
+        }
+    }
+
+    @Override
+    public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
+        mSnapshotView.setThumbnail(thumbnailData);
+        mIconView.setImageDrawable(task.icon);
+    }
+
+    @Override
+    public void onTaskDataUnloaded() {
+        mSnapshotView.setThumbnail(null);
+        mIconView.setImageDrawable(null);
+    }
+
+    @Override
+    public void onTaskWindowingModeChanged() {
+        // Do nothing
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 091ab54..adaa785 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -15,17 +15,246 @@
  */
 package com.android.quickstep;
 
+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;
+import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.PointF;
+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.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.R;
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.recents.ISystemUiProxy;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan.Options;
+import com.android.systemui.shared.recents.model.RecentsTaskLoader;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.BackgroundExecutor;
+
+import java.util.concurrent.Future;
 
 /**
  * Service connected by system-UI for handling touch interaction.
  */
 public class TouchInteractionService extends Service {
 
+    private static final String TAG = "TouchInteractionService";
+
+    private static RecentsTaskLoader sRecentsTaskLoader;
+
+    private final IBinder mMyBinder = new IOverviewProxy.Stub() {
+
+        @Override
+        public void onMotionEvent(MotionEvent ev) {
+            handleMotionEvent(ev);
+        }
+
+        @Override
+        public void onBind(ISystemUiProxy iSystemUiProxy) throws RemoteException {
+            mISystemUiProxy = iSystemUiProxy;
+        }
+    };
+
+    private ActivityManagerWrapper mAM;
+    private RunningTaskInfo mRunningTask;
+    private Intent mHomeIntent;
+    private ComponentName mLauncher;
+
+    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;
+    private VelocityTracker mVelocityTracker;
+    private int mTouchSlop;
+    private NavBarSwipeInteractionHandler mInteractionHandler;
+
+    private ISystemUiProxy mISystemUiProxy;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mAM = ActivityManagerWrapper.getInstance();
+
+        mHomeIntent = new Intent(Intent.ACTION_MAIN)
+                .addCategory(Intent.CATEGORY_HOME)
+                .setPackage(getPackageName())
+                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        ResolveInfo info = getPackageManager().resolveActivity(mHomeIntent, 0);
+        mLauncher = new ComponentName(getPackageName(), info.activityInfo.name);
+        mHomeIntent.setComponent(mLauncher);
+
+        Resources res = getResources();
+        if (sRecentsTaskLoader == null) {
+            sRecentsTaskLoader = new RecentsTaskLoader(this,
+                    res.getInteger(R.integer.config_recentsMaxThumbnailCacheSize),
+                    res.getInteger(R.integer.config_recentsMaxIconCacheSize), 0);
+            sRecentsTaskLoader.startLoader(this);
+        }
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
-        return null;
+        Log.d(TAG, "Touch service connected");
+        return mMyBinder;
+    }
+
+    public static RecentsTaskLoader getRecentsTaskLoader() {
+        return sRecentsTaskLoader;
+    }
+
+    private void handleMotionEvent(MotionEvent ev) {
+        if (ev.getActionMasked() != MotionEvent.ACTION_DOWN && mVelocityTracker == null) {
+            return;
+        }
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                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)) {
+                    // TODO: We could drive all-apps in this case. For now just ignore swipe.
+                    break;
+                }
+
+                if (mVelocityTracker == null) {
+                    mVelocityTracker = VelocityTracker.obtain();
+                } else {
+                    mVelocityTracker.clear();
+                }
+                mVelocityTracker.addMovement(ev);
+                if (mInteractionHandler != null) {
+                    mInteractionHandler.endTouch(0);
+                    mInteractionHandler = null;
+                }
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                int ptrIdx = ev.getActionIndex();
+                int ptrId = ev.getPointerId(ptrIdx);
+                if (ptrId == mActivePointerId) {
+                    final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                    mDownPos.set(
+                            ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                            ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                    mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                    mActivePointerId = ev.getPointerId(newPointerIdx);
+                    mVelocityTracker.clear();
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == INVALID_POINTER_ID) {
+                    break;
+                }
+                mVelocityTracker.addMovement(ev);
+                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+
+                float displacement = ev.getY(pointerIndex) - mDownPos.y;
+                if (mInteractionHandler == null) {
+                    if (Math.abs(displacement) >= mTouchSlop) {
+                        startTouchTracking();
+                    }
+                } else {
+                    // Move
+                    mInteractionHandler.updateDisplacement(displacement);
+                }
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL:
+                // TODO: Should be different than ACTION_UP
+            case MotionEvent.ACTION_UP: {
+
+                endInteraction();
+                break;
+            }
+        }
+    }
+
+    private void startTouchTracking() {
+        // Create the shared handler
+        mInteractionHandler = new NavBarSwipeInteractionHandler(getCurrentTaskSnapshot(),
+                mRunningTask);
+
+        // Preload and start the recents activity on a background thread
+        final Context context = this;
+        final RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(context);
+        Future<RecentsTaskLoadPlan> loadPlanFuture = BackgroundExecutor.get().submit(() -> {
+            // Preload the plan
+            RecentsTaskLoader loader = TouchInteractionService.getRecentsTaskLoader();
+            loadPlan.preloadPlan(loader, mRunningTask.id, UserHandle.myUserId());
+
+            // Pass the
+            Bundle extras = new Bundle();
+            extras.putBinder(EXTRA_STATE_HANDLER, mInteractionHandler);
+
+            // Start the activity
+            Intent homeIntent = new Intent(mHomeIntent);
+            homeIntent.putExtras(extras);
+            startActivity(homeIntent, ActivityOptions.makeCustomAnimation(this, 0, 0).toBundle());
+            /*
+            ActivityManagerWrapper.getInstance().startRecentsActivity(null, options,
+                    ActivityOptions.makeCustomAnimation(this, 0, 0), UserHandle.myUserId(),
+                    null, null);
+             */
+        }, loadPlan);
+
+        mInteractionHandler.setLastLoadPlan(loadPlanFuture);
+    }
+
+    private void endInteraction() {
+        if (mInteractionHandler != null) {
+            mVelocityTracker.computeCurrentVelocity(1000,
+                    ViewConfiguration.get(this).getScaledMaximumFlingVelocity());
+
+            mInteractionHandler.endTouch(mVelocityTracker.getXVelocity(mActivePointerId));
+            mInteractionHandler = null;
+        }
+        mVelocityTracker.recycle();
+        mVelocityTracker = null;
+    }
+
+    private Bitmap getCurrentTaskSnapshot() {
+        if (mISystemUiProxy == null) {
+            Log.e(TAG, "Never received systemUIProxy");
+            return null;
+        }
+
+        // TODO: We are using some hardcoded layers for now, to best approximate the activity layers
+        try {
+            return mISystemUiProxy.screenshot(new Rect(), mDisplaySize.x, mDisplaySize.y, 0, 100000,
+                    false, mDisplayRotation);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error capturing snapshot", e);
+            return null;
+        }
     }
 }
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index 6f837ae..05f509f 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -27,25 +27,52 @@
     android:focusableInTouchMode="true"
     android:saveEnabled="false" >
 
-    <!-- DO NOT CHANGE THE ID -->
-    <com.android.launcher3.allapps.AllAppsRecyclerView
-        android:id="@+id/apps_list_view"
+    <include layout="@layout/all_apps_rv_layout" />
+
+    <include layout="@layout/all_apps_fast_scroller" />
+
+    <RelativeLayout
+        android:id="@+id/all_apps_header"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_below="@id/search_container_all_apps"
-        android:layout_gravity="center_horizontal|top"
-        android:clipToPadding="false"
-        android:descendantFocusability="afterDescendants"
-        android:focusable="true"
-        android:overScrollMode="never" />
+        android:layout_height="wrap_content"
+        android:clickable="true"
+        android:paddingTop="30dp"
+        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"/>
+
+        <LinearLayout
+            android:id="@+id/tab_layout"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/all_apps_header_tab_height"
+            android:layout_below="@id/header_content"
+            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:background="?android:attr/selectableItemBackground"/>
+            <Button
+                android:id="@+id/tab_work"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:text="@string/all_apps_work_tab"
+                android:background="?android:attr/selectableItemBackground"/>
+        </LinearLayout>
+
+    </RelativeLayout>
 
     <!-- Note: we are reusing/repurposing a system attribute for search layout, because of a
      platform bug, which prevents using custom attributes in <include> tag -->
     <include
         android:id="@id/search_container_all_apps"
-        layout="?android:attr/keyboardLayout" />
-
-    <include layout="@layout/all_apps_fast_scroller" />
+        layout="?android:attr/keyboardLayout"/>
 
     <View
         android:id="@+id/nav_bar_bg"
diff --git a/res/layout/all_apps_rv_layout.xml b/res/layout/all_apps_rv_layout.xml
new file mode 100644
index 0000000..3c19f8c
--- /dev/null
+++ b/res/layout/all_apps_rv_layout.xml
@@ -0,0 +1,26 @@
+<?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.allapps.AllAppsRecyclerView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/apps_list_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_below="@id/search_container_all_apps"
+    android:clipToPadding="false"
+    android:descendantFocusability="afterDescendants"
+    android:focusable="true"
+    android:overScrollMode="never" />
diff --git a/res/layout/all_apps_tabs.xml b/res/layout/all_apps_tabs.xml
new file mode 100644
index 0000000..fa1d591
--- /dev/null
+++ b/res/layout/all_apps_tabs.xml
@@ -0,0 +1,34 @@
+<?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.allapps.InterceptingViewPager
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/all_apps_tabs_view_pager"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_below="@id/search_container_all_apps"
+    android:layout_gravity="center_horizontal|top"
+    android:layout_marginTop="@dimen/all_apps_header_tab_height"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:descendantFocusability="afterDescendants"
+    android:paddingTop="30dp">
+
+    <include layout="@layout/all_apps_rv_layout" />
+
+    <include layout="@layout/all_apps_rv_layout" />
+
+</com.android.launcher3.allapps.InterceptingViewPager>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 54328e1..0ff0d75 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -134,4 +134,8 @@
     <item type="id" name="search_container_hotseat" />
     <item type="id" name="search_container_all_apps" />
 
+<!-- Recents -->
+    <integer name="config_recentsMaxThumbnailCacheSize">6</integer>
+    <integer name="config_recentsMaxIconCacheSize">12</integer>
+
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 957ec19..94db0cc 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -90,6 +90,7 @@
     <dimen name="all_apps_background_canvas_width">700dp</dimen>
     <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>
 
 <!-- Search bar in All Apps -->
     <dimen name="all_apps_header_max_elevation">3dp</dimen>
@@ -232,4 +233,7 @@
     <dimen name="horizontal_ellipsis_offset">19dp</dimen>
     <dimen name="popup_item_divider_height">0.5dp</dimen>
     <dimen name="swipe_helper_falsing_threshold">70dp</dimen>
+
+<!-- Recents -->
+    <dimen name="recents_page_spacing">10dp</dimen>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e9b00f6..fdd4d8d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -318,4 +318,10 @@
     <!-- Accessibility confirmation for notification being dismissed. -->
     <string name="notification_dismissed">Notification dismissed</string>
 
+    <!-- Label of tab to indicate personal apps -->
+    <string name="all_apps_personal_tab">Personal</string>
+
+    <!-- Label of tab to indicate work apps -->
+    <string name="all_apps_work_tab">Work</string>
+
 </resources>
diff --git a/res/xml/device_profiles.xml b/res/xml/device_profiles.xml
index c582fc5..30c1c54 100644
--- a/res/xml/device_profiles.xml
+++ b/res/xml/device_profiles.xml
@@ -15,7 +15,7 @@
      limitations under the License.
 -->
 
-<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3" >
+<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
 
     <profile
         launcher:name="Super Short Stubby"
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 62e0fb1..26024e5 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -20,7 +20,6 @@
 import android.content.Context;
 import android.support.annotation.IntDef;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.widget.LinearLayout;
@@ -42,7 +41,8 @@
             TYPE_ACTION_POPUP,
             TYPE_WIDGETS_BOTTOM_SHEET,
             TYPE_WIDGET_RESIZE_FRAME,
-            TYPE_WIDGETS_FULL_SHEET
+            TYPE_WIDGETS_FULL_SHEET,
+            TYPE_QUICKSTEP_PREVIEW
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
@@ -51,9 +51,11 @@
     public static final int TYPE_WIDGETS_BOTTOM_SHEET = 1 << 2;
     public static final int TYPE_WIDGET_RESIZE_FRAME = 1 << 3;
     public static final int TYPE_WIDGETS_FULL_SHEET = 1 << 4;
+    public static final int TYPE_QUICKSTEP_PREVIEW = 1 << 5;
 
     public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
-            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET;
+            | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
+            | TYPE_QUICKSTEP_PREVIEW;
 
     protected boolean mIsOpen;
 
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index afb83be..b315980 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -61,9 +61,13 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
-        ViewGroup parent = (ViewGroup) getParent();
+        bindFastScrollbar();
+    }
+
+    public void bindFastScrollbar() {
+        ViewGroup parent = (ViewGroup) getParent().getParent();
         mScrollbar = parent.findViewById(R.id.fast_scroller);
-        mScrollbar.setRecyclerView(this, (TextView) parent.findViewById(R.id.fast_scroller_popup));
+        mScrollbar.setRecyclerView(this, parent.findViewById(R.id.fast_scroller_popup));
     }
 
     /**
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 6030e08..c6226f4 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -27,11 +27,11 @@
 import android.util.DisplayMetrics;
 import android.view.Gravity;
 import android.view.View;
-import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.FrameLayout;
 
 import com.android.launcher3.CellLayout.ContainerType;
+import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.badge.BadgeRenderer;
 
 import java.util.ArrayList;
@@ -669,10 +669,9 @@
         }
 
         // Layout the AllAppsRecyclerView
-        View view = launcher.findViewById(R.id.apps_list_view);
+        AllAppsContainerView appsView = launcher.findViewById(R.id.apps_view);
         int paddingLeftRight = desiredWorkspaceLeftRightMarginPx + cellLayoutPaddingLeftRightPx;
-        view.setPadding(paddingLeftRight, view.getPaddingTop(), paddingLeftRight,
-                view.getPaddingBottom());
+        appsView.setRecyclerViewSidePadding(paddingLeftRight, paddingLeftRight);
 
         if (notifyListeners) {
             for (int i = mListeners.size() - 1; i >= 0; i--) {
@@ -681,13 +680,13 @@
         }
     }
 
-    private int getCurrentWidth() {
+    public int getCurrentWidth() {
         return isLandscape
                 ? Math.max(widthPx, heightPx)
                 : Math.min(widthPx, heightPx);
     }
 
-    private int getCurrentHeight() {
+    public int getCurrentHeight() {
         return isLandscape
                 ? Math.min(widthPx, heightPx)
                 : Math.max(widthPx, heightPx);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index fa4a1c8..60c00fb 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -123,6 +123,7 @@
 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.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
@@ -222,15 +223,12 @@
     @Thunk DragLayer mDragLayer;
     private DragController mDragController;
 
-    public View mWeightWatcher;
-
     private AppWidgetManagerCompat mAppWidgetManager;
     private LauncherAppWidgetHost mAppWidgetHost;
 
     private final int[] mTmpAddItemCellCoordinates = new int[2];
 
     @Thunk Hotseat mHotseat;
-    private ViewGroup mOverviewPanel;
 
     private View mAllAppsButton;
 
@@ -240,6 +238,9 @@
     @Thunk AllAppsContainerView mAppsView;
     AllAppsTransitionController mAllAppsController;
 
+    // UI and state for the overview panel
+    private ViewGroup mOverviewPanel;
+
     // We need to store the orientation Launcher was created with, due to a bug (b/64916689)
     // that results in widgets being inflated in the wrong orientation.
     private int mOrientation;
@@ -360,6 +361,8 @@
 
         restoreState(savedInstanceState);
 
+        InternalStateHandler.handleCreate(this, getIntent());
+
         // We only load the page synchronously if the user rotates (or triggers a
         // configuration change) while launcher is in the foreground
         int currentScreen = PagedView.INVALID_RESTORE_PAGE;
@@ -1307,8 +1310,8 @@
         return mHotseat;
     }
 
-    public ViewGroup getOverviewPanel() {
-        return mOverviewPanel;
+    public <T extends ViewGroup> T getOverviewPanel() {
+        return (T) mOverviewPanel;
     }
 
     public DropTargetBar getDropTargetBar() {
@@ -1345,7 +1348,6 @@
         // Check this condition before handling isActionMain, as this will get reset.
         boolean shouldMoveToDefaultScreen = alreadyOnHome && isInState(NORMAL)
                 && AbstractFloatingView.getTopOpenView(this) == null;
-
         boolean isActionMain = Intent.ACTION_MAIN.equals(intent.getAction());
         if (isActionMain) {
             if (mWorkspace == null) {
@@ -1405,6 +1407,7 @@
                 });
             }
         }
+        InternalStateHandler.handleNewIntent(this, intent, alreadyOnHome);
 
         TraceHelper.endSection("NEW_INTENT");
     }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 685dcee..93fe17c 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -296,7 +296,6 @@
 
         mLauncher = Launcher.getLauncher(context);
         mStateTransitionAnimation = new WorkspaceStateTransitionAnimation(mLauncher, this);
-        final Resources res = getResources();
         DeviceProfile grid = mLauncher.getDeviceProfile();
         mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
         mWallpaperManager = WallpaperManager.getInstance(context);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 81f5842..271a133 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -18,11 +18,18 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Rect;
+import android.os.Process;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
 import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
@@ -47,9 +54,14 @@
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.ComponentKey;
+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 java.util.HashMap;
 import java.util.List;
 import java.util.Set;
 
@@ -63,23 +75,26 @@
     protected final Rect mBasePadding = new Rect();
 
     private final Launcher mLauncher;
-    private final AlphabeticalAppsList mApps;
-    private final AllAppsGridAdapter mAdapter;
-    private final LinearLayoutManager mLayoutManager;
+    private final AdapterHolder[] mAH;
     private final ClickShadowView mTouchFeedbackView;
+    private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle());
+    private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher);
 
-    private AllAppsRecyclerView mAppsRecyclerView;
     private SearchUiManager mSearchUiManager;
     private View mSearchContainer;
+    private InterceptingViewPager mViewPager;
+    private ViewGroup mHeader;
+    private FloatingHeaderHandler mFloatingHeaderHandler;
 
     private SpannableStringBuilder mSearchQueryBuilder = null;
 
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
 
-    private SpringAnimationHandler mSpringAnimationHandler;
-
     private TransformingTouchDelegate mTouchDelegate;
+    private boolean mUsingTabs;
+
+    private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
 
     public AllAppsContainerView(Context context) {
         this(context, null);
@@ -93,11 +108,7 @@
         super(context, attrs, defStyleAttr);
 
         mLauncher = Launcher.getLauncher(context);
-        mApps = new AlphabeticalAppsList(context);
-        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
-        mSpringAnimationHandler = mAdapter.getSpringAnimationHandler();
-        mApps.setAdapter(mAdapter);
-        mLayoutManager = mAdapter.getLayoutManager();
+
         mSearchQueryBuilder = new SpannableStringBuilder();
 
         Selection.setSelection(mSearchQueryBuilder, 0);
@@ -107,6 +118,10 @@
         int size = mLauncher.getDeviceProfile().allAppsIconSizePx
                 + mTouchFeedbackView.getExtraSize();
         addView(mTouchFeedbackView, size, size);
+
+        mAH = new AdapterHolder[2];
+        mAH[AdapterHolder.MAIN] = new AdapterHolder();
+        mAH[AdapterHolder.WORK] = new AdapterHolder();
     }
 
     @Override
@@ -116,7 +131,17 @@
         DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
         grid.addLauncherLayoutChangedListener(this);
 
-        mTouchDelegate = new TransformingTouchDelegate(mAppsRecyclerView);
+        applyTouchDelegate();
+    }
+
+    private void applyTouchDelegate() {
+        RecyclerView rv = getActiveRecyclerView();
+        mTouchDelegate = new TransformingTouchDelegate(rv);
+        mTouchDelegate.setBounds(
+                rv.getLeft() - mBasePadding.left,
+                rv.getTop() - mBasePadding.top,
+                rv.getRight() + mBasePadding.right,
+                rv.getBottom() + mBasePadding.bottom);
         setTouchDelegate(mTouchDelegate);
     }
 
@@ -148,11 +173,7 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        mTouchDelegate.setBounds(
-                mAppsRecyclerView.getLeft() - mBasePadding.left,
-                mAppsRecyclerView.getTop() - mBasePadding.top,
-                mAppsRecyclerView.getRight() + mBasePadding.right,
-                mAppsRecyclerView.getBottom() + mBasePadding.bottom);
+        applyTouchDelegate();
     }
 
     @Override
@@ -164,36 +185,69 @@
      * Sets the current set of apps.
      */
     public void setApps(List<AppInfo> apps) {
-        mApps.setApps(apps);
+        boolean hasWorkProfileApp = hasWorkProfileApp(apps);
+        if (mUsingTabs != hasWorkProfileApp) {
+            rebindAdapters(hasWorkProfileApp);
+        }
+        mComponentToAppMap.clear();
+        addOrUpdateApps(apps);
     }
 
     /**
      * Adds or updates existing apps in the list
      */
     public void addOrUpdateApps(List<AppInfo> apps) {
-        mApps.addOrUpdateApps(apps);
-        mSearchUiManager.refreshSearchResult();
-    }
-
-    public void updatePromiseAppProgress(PromiseAppInfo app) {
-        int childCount = mAppsRecyclerView.getChildCount();
-        for (int i = 0; i < childCount; i++) {
-            View child = mAppsRecyclerView.getChildAt(i);
-            if (child instanceof BubbleTextView && child.getTag() == app) {
-                BubbleTextView bubbleTextView = (BubbleTextView) child;
-                bubbleTextView.applyProgressLevel(app.level);
-            }
+        for (AppInfo app : apps) {
+            mComponentToAppMap.put(app.toComponentKey(), app);
         }
+        onAppsUpdated();
+        mSearchUiManager.refreshSearchResult();
     }
 
     /**
      * Removes some apps from the list.
      */
     public void removeApps(List<AppInfo> apps) {
-        mApps.removeApps(apps);
+        for (AppInfo app : apps) {
+            mComponentToAppMap.remove(app.toComponentKey());
+        }
+        onAppsUpdated();
         mSearchUiManager.refreshSearchResult();
     }
 
+    private void onAppsUpdated() {
+        for (int i = 0; i < getNumOfAdapters(); i++) {
+            mAH[i].appsList.onAppsUpdated();
+        }
+    }
+
+    private int getNumOfAdapters() {
+        return mUsingTabs ? mAH.length : 1;
+    }
+
+    public void updatePromiseAppProgress(PromiseAppInfo app) {
+        for (int i = 0; i < mAH.length; i++) {
+            updatePromiseAppProgress(app, mAH[i].recyclerView);
+        }
+        if (mFloatingHeaderHandler != null) {
+            updatePromiseAppProgress(app, mFloatingHeaderHandler.getContentView());
+        }
+    }
+
+    private void updatePromiseAppProgress(PromiseAppInfo app, ViewGroup parent) {
+        if (parent == null) {
+            return;
+        }
+        int childCount = parent.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View child = parent.getChildAt(i);
+            if (child instanceof BubbleTextView && child.getTag() == app) {
+                BubbleTextView bubbleTextView = (BubbleTextView) child;
+                bubbleTextView.applyProgressLevel(app.level);
+            }
+        }
+    }
+
     /**
      * Returns whether the view itself will handle the touch event or not.
      */
@@ -203,15 +257,31 @@
         if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) {
             return true;
         }
-        return mAppsRecyclerView.shouldContainerScroll(ev, mLauncher.getDragLayer());
+        if (mUsingTabs && mLauncher.getDragLayer().isEventOverView(mHeader, ev)) {
+            return true;
+        }
+        AllAppsRecyclerView rv = getActiveRecyclerView();
+        return rv == null || rv.shouldContainerScroll(ev, mLauncher.getDragLayer());
+    }
+
+    public AllAppsRecyclerView getActiveRecyclerView() {
+        if (!mUsingTabs || mViewPager.getCurrentItem() == 0) {
+            return mAH[AdapterHolder.MAIN].recyclerView;
+        } else {
+            return mAH[AdapterHolder.WORK].recyclerView;
+        }
     }
 
     /**
      * Resets the state of AllApps.
      */
     public void reset() {
+        for (int i = 0; i < mAH.length; i++) {
+            if (mAH[i].recyclerView != null) {
+                mAH[i].recyclerView.scrollToTop();
+            }
+        }
         // Reset the search bar and base recycler view after transitioning home
-        mAppsRecyclerView.scrollToTop();
         mSearchUiManager.reset();
     }
 
@@ -224,33 +294,18 @@
         setOnFocusChangeListener(new View.OnFocusChangeListener() {
             @Override
             public void onFocusChange(View v, boolean hasFocus) {
-                if (hasFocus) {
-                    mAppsRecyclerView.requestFocus();
+                if (hasFocus && getActiveRecyclerView() != null) {
+                    getActiveRecyclerView().requestFocus();
                 }
             }
         });
 
-        // Load the all apps recycler view
-        mAppsRecyclerView = findViewById(R.id.apps_list_view);
-        mAppsRecyclerView.setApps(mApps);
-        mAppsRecyclerView.setLayoutManager(mLayoutManager);
-        mAppsRecyclerView.setAdapter(mAdapter);
-        mAppsRecyclerView.setHasFixedSize(true);
-        // No animations will occur when changes occur to the items in this RecyclerView.
-        mAppsRecyclerView.setItemAnimator(null);
-        if (FeatureFlags.LAUNCHER3_PHYSICS) {
-            mAppsRecyclerView.setSpringAnimationHandler(mSpringAnimationHandler);
-        }
+        mHeader = findViewById(R.id.all_apps_header);
+        rebindAdapters(mUsingTabs);
 
         mSearchContainer = findViewById(R.id.search_container_all_apps);
         mSearchUiManager = (SearchUiManager) mSearchContainer;
-        mSearchUiManager.initialize(mApps, mAppsRecyclerView);
-
-
-        FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView);
-        mAppsRecyclerView.addItemDecoration(focusedItemDecorator);
-        mAppsRecyclerView.preMeasureViews(mAdapter);
-        mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
+        mSearchUiManager.initialize(this);
 
         onLauncherLayoutChanged();
     }
@@ -269,10 +324,9 @@
                 mNumPredictedAppsPerRow != grid.inv.numColumns) {
             mNumAppsPerRow = grid.inv.numColumns;
             mNumPredictedAppsPerRow = grid.inv.numColumns;
-
-            mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow);
-            mAdapter.setNumAppsPerRow(mNumAppsPerRow);
-            mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+            for (int i = 0; i < mAH.length; i++) {
+                mAH[i].applyNumsPerRow();
+            }
         }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
@@ -325,10 +379,10 @@
     @Override
     public void setInsets(Rect insets) {
         DeviceProfile grid = mLauncher.getDeviceProfile();
-        mAppsRecyclerView.setPadding(
-                mAppsRecyclerView.getPaddingLeft(), mAppsRecyclerView.getPaddingTop(),
-                mAppsRecyclerView.getPaddingRight(), insets.bottom);
-
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].padding.bottom = insets.bottom;
+            mAH[i].applyPadding();
+        }
         if (grid.isVerticalBarLayout()) {
             ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
             mlp.leftMargin = insets.left;
@@ -345,20 +399,300 @@
 
     public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
         final PackageUserKey packageUserKey = new PackageUserKey(null, null);
-        final int n = mAppsRecyclerView.getChildCount();
-        for (int i = 0; i < n; i++) {
-            View child = mAppsRecyclerView.getChildAt(i);
-            if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) {
-                continue;
-            }
-            ItemInfo info = (ItemInfo) child.getTag();
-            if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) {
-                ((BubbleTextView) child).applyBadgeState(info, true /* animate */);
+        for (int j = 0; j < mAH.length; j++) {
+            if (mAH[j].recyclerView != null) {
+                final int n = mAH[j].recyclerView.getChildCount();
+                for (int i = 0; i < n; i++) {
+                    View child = mAH[j].recyclerView.getChildAt(i);
+                    if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) {
+                        continue;
+                    }
+                    ItemInfo info = (ItemInfo) child.getTag();
+                    if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) {
+                        ((BubbleTextView) child).applyBadgeState(info, true /* animate */);
+                    }
+                }
             }
         }
     }
 
     public SpringAnimationHandler getSpringAnimationHandler() {
-        return mSpringAnimationHandler;
+        return mUsingTabs ? null : mAH[AdapterHolder.MAIN].animationHandler;
     }
+
+    private void rebindAdapters(boolean showTabs) {
+        if (showTabs != mUsingTabs) {
+            replaceRVContainer(showTabs);
+        }
+        mUsingTabs = showTabs;
+
+        if (mUsingTabs) {
+            mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher);
+            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);
+        }
+
+        applyTouchDelegate();
+    }
+
+    private boolean hasWorkProfileApp(List<AppInfo> apps) {
+        if (FeatureFlags.ALL_APPS_TABS_ENABLED) {
+            for (AppInfo app : apps) {
+                if (mWorkMatcher.matches(app, null)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private void replaceRVContainer(boolean showTabs) {
+        for (int i = 0; i < mAH.length; i++) {
+            if (mAH[i].recyclerView != null) {
+                mAH[i].recyclerView.setLayoutManager(null);
+            }
+        }
+        View oldView = getRecyclerViewContainer();
+        int index = indexOfChild(oldView);
+        removeView(oldView);
+        int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout;
+        View newView = LayoutInflater.from(getContext()).inflate(layout, this, false);
+        addView(newView, index);
+        mViewPager = showTabs ? (InterceptingViewPager) newView : null;
+    }
+
+    public View getRecyclerViewContainer() {
+        return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view);
+    }
+
+    private void setupWorkProfileTabs() {
+        mViewPager.setAdapter(new TabsPagerAdapter());
+        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+
+            boolean mVisible = true;
+
+            @Override
+            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+                if (positionOffset == 0 && !mVisible || positionOffset > 0 && mVisible) {
+                    mVisible = positionOffset == 0;
+                    for (int i = 0; i < mAH.length; i++) {
+                        if (mAH[i].recyclerView != null) {
+                            mAH[i].recyclerView.getScrollbar().setAlpha(mVisible ? 1 : 0);
+                        }
+                    }
+                }
+            }
+
+            @Override
+            public void onPageSelected(int pos) {
+                mFloatingHeaderHandler.setMainActive(pos == 0);
+                applyTouchDelegate();
+                if (mAH[pos].recyclerView != null) {
+                    mAH[pos].recyclerView.bindFastScrollbar();
+                }
+            }
+
+            @Override
+            public void onPageScrollStateChanged(int state) {
+            }
+        });
+
+        findViewById(R.id.tab_personal)
+                .setOnClickListener((View view) -> mViewPager.setCurrentItem(0));
+        findViewById(R.id.tab_work)
+                .setOnClickListener((View view) -> mViewPager.setCurrentItem(1));
+    }
+
+    public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
+        if (mUsingTabs) {
+            mFloatingHeaderHandler.getContentView().setPredictedApps(apps);
+        }
+        mAH[AdapterHolder.MAIN].appsList.setPredictedApps(apps);
+    }
+
+    public AppInfo findApp(ComponentKeyMapper<AppInfo> mapper) {
+        return mapper.getItem(mComponentToAppMap);
+    }
+
+    public AlphabeticalAppsList getApps() {
+        return mAH[AdapterHolder.MAIN].appsList;
+    }
+
+    public boolean isUsingTabs() {
+        return mUsingTabs;
+    }
+
+    public FloatingHeaderHandler getFloatingHeaderHandler() {
+        return mFloatingHeaderHandler;
+    }
+
+    private void setupHeader() {
+        int contentHeight = mLauncher.getDeviceProfile().allAppsCellHeightPx;
+        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);
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].paddingTopForTabs = contentHeight;
+            mAH[i].applyPadding();
+        }
+    }
+
+    public void setLastSearchQuery(String query) {
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].adapter.setLastSearchQuery(query);
+        }
+    }
+
+    public void onSearchResultsChanged() {
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].recyclerView.onSearchResultsChanged();
+        }
+    }
+
+    public void setRecyclerViewPaddingTop(int top) {
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].padding.top = top;
+            mAH[i].applyPadding();
+        }
+    }
+
+    public void setRecyclerViewSidePadding(int left, int right) {
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].padding.left = left;
+            mAH[i].padding.right = right;
+            mAH[i].applyPadding();
+        }
+    }
+
+    public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) {
+        for (int i = 0; i < mAH.length; i++) {
+            mAH[i].applyVerticalFadingEdgeEnabled(enabled);
+        }
+    }
+
+    public void addElevationController(RecyclerView.OnScrollListener scrollListener) {
+        if (!mUsingTabs) {
+            mAH[AdapterHolder.MAIN].recyclerView.addOnScrollListener(scrollListener);
+        }
+    }
+
+    public List<AppInfo> getPredictedApps() {
+        if (mUsingTabs) {
+            return mFloatingHeaderHandler.getContentView().getPredictedApps();
+        } else {
+            return mAH[AdapterHolder.MAIN].appsList.getPredictedApps();
+        }
+    }
+
+    public class AdapterHolder {
+        public static final int MAIN = 0;
+        public static final int WORK = 1;
+
+        final AllAppsGridAdapter adapter;
+        final LinearLayoutManager layoutManager;
+        final SpringAnimationHandler animationHandler;
+        final AlphabeticalAppsList appsList;
+        final Rect padding = new Rect();
+        int paddingTopForTabs;
+        AllAppsRecyclerView recyclerView;
+        boolean verticalFadingEdge;
+
+        AdapterHolder() {
+            appsList = new AlphabeticalAppsList(mLauncher, mComponentToAppMap);
+            adapter = new AllAppsGridAdapter(mLauncher, appsList, mLauncher,
+                    AllAppsContainerView.this, true);
+            appsList.setAdapter(adapter);
+            animationHandler = adapter.getSpringAnimationHandler();
+            layoutManager = adapter.getLayoutManager();
+        }
+
+        void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) {
+            appsList.updateItemFilter(matcher);
+            recyclerView = (AllAppsRecyclerView) rv;
+            recyclerView.setApps(appsList, mUsingTabs);
+            recyclerView.setLayoutManager(layoutManager);
+            recyclerView.setAdapter(adapter);
+            recyclerView.setHasFixedSize(true);
+            // No animations will occur when changes occur to the items in this RecyclerView.
+            recyclerView.setItemAnimator(null);
+            if (FeatureFlags.LAUNCHER3_PHYSICS && animationHandler != null) {
+                recyclerView.setSpringAnimationHandler(animationHandler);
+            }
+            FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView);
+            recyclerView.addItemDecoration(focusedItemDecorator);
+            recyclerView.preMeasureViews(adapter);
+            adapter.setIconFocusListener(focusedItemDecorator.getFocusListener());
+            applyVerticalFadingEdgeEnabled(verticalFadingEdge);
+            applyPadding();
+            applyNumsPerRow();
+        }
+
+        void applyPadding() {
+            if (recyclerView != null) {
+                int paddingTop = mUsingTabs ? paddingTopForTabs : padding.top;
+                recyclerView.setPadding(padding.left, paddingTop, padding.right, padding.bottom);
+            }
+        }
+
+        void applyNumsPerRow() {
+            if (mNumAppsPerRow > 0) {
+                if (recyclerView != null) {
+                    recyclerView.setNumAppsPerRow(mLauncher.getDeviceProfile(), mNumAppsPerRow);
+                }
+                adapter.setNumAppsPerRow(mNumAppsPerRow);
+                appsList.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+                if (mUsingTabs && mFloatingHeaderHandler != null) {
+                    mFloatingHeaderHandler.getContentView()
+                            .setNumAppsPerRow(mNumPredictedAppsPerRow);
+                }
+            }
+        }
+
+        public void applyVerticalFadingEdgeEnabled(boolean enabled) {
+            verticalFadingEdge = enabled;
+            mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs
+                    && verticalFadingEdge);
+        }
+    }
+
+    private class TabsPagerAdapter extends PagerAdapter {
+        @Override
+        public int getCount() {
+            return 2;
+        }
+
+        @Override
+        public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
+            return view == object;
+        }
+
+        @NonNull
+        @Override
+        public Object instantiateItem(@NonNull ViewGroup container, int position) {
+            if (position == 0) {
+                return mAH[AdapterHolder.MAIN].recyclerView;
+            } else {
+                return mAH[AdapterHolder.WORK].recyclerView;
+            }
+        }
+
+        @Nullable
+        @Override
+        public CharSequence getPageTitle(int position) {
+            if (position == 0) {
+                return getResources().getString(R.string.all_apps_personal_tab);
+            } else {
+                return getResources().getString(R.string.all_apps_work_tab);
+            }
+        }
+    }
+
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index ed5bf9f..ac8d367 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -206,10 +206,10 @@
     // The intent to send off to the market app, updated each time the search query changes.
     private Intent mMarketSearchIntent;
 
-    private SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
+    private final SpringAnimationHandler<ViewHolder> mSpringAnimationHandler;
 
     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener
-            iconClickListener, View.OnLongClickListener iconLongClickListener) {
+            iconClickListener, View.OnLongClickListener iconLongClickListener, boolean springAnim) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -220,9 +220,11 @@
         mLayoutInflater = LayoutInflater.from(launcher);
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
-        if (FeatureFlags.LAUNCHER3_PHYSICS) {
+        if (FeatureFlags.LAUNCHER3_PHYSICS && springAnim) {
             mSpringAnimationHandler = new SpringAnimationHandler<>(
                     SpringAnimationHandler.Y_DIRECTION, new AllAppsSpringAnimationFactory());
+        } else {
+            mSpringAnimationHandler = null;
         }
     }
 
@@ -377,7 +379,7 @@
     @Override
     public void onViewAttachedToWindow(ViewHolder holder) {
         int type = holder.getItemViewType();
-        if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
+        if (mSpringAnimationHandler != null && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
             mSpringAnimationHandler.add(holder.itemView, holder);
         }
     }
@@ -385,7 +387,7 @@
     @Override
     public void onViewDetachedFromWindow(ViewHolder holder) {
         int type = holder.getItemViewType();
-        if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
+        if (mSpringAnimationHandler != null && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) {
             mSpringAnimationHandler.remove(holder.itemView);
         }
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 494cd5a..09357f7 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.anim.SpringAnimationHandler;
 import com.android.launcher3.config.FeatureFlags;
@@ -40,6 +41,7 @@
 import com.android.launcher3.touch.SwipeDetector;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.views.RecyclerViewFastScroller;
 
 import java.util.List;
 
@@ -51,6 +53,7 @@
     private AlphabeticalAppsList mApps;
     private AllAppsFastScrollHelper mFastScrollHelper;
     private int mNumAppsPerRow;
+    private int mUserProfileTabContentHeight;
 
     // The specific view heights that we use to calculate scroll
     private SparseIntArray mViewHeights = new SparseIntArray();
@@ -63,7 +66,6 @@
     private SpringAnimationHandler mSpringAnimationHandler;
     private OverScrollHelper mOverScrollHelper;
     private SwipeDetector mPullDetector;
-
     private float mContentTranslationY = 0;
     public static final Property<AllAppsRecyclerView, Float> CONTENT_TRANS_Y =
             new Property<AllAppsRecyclerView, Float>(Float.class, "appsRecyclerViewContentTransY") {
@@ -122,9 +124,11 @@
     /**
      * Sets the list of apps in this view, used to determine the fastscroll position.
      */
-    public void setApps(AlphabeticalAppsList apps) {
+    public void setApps(AlphabeticalAppsList apps, boolean usingTabs) {
         mApps = apps;
         mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
+        mUserProfileTabContentHeight = usingTabs
+                ? Launcher.getLauncher(getContext()).getDeviceProfile().allAppsCellHeightPx : 0;;
     }
 
     public AlphabeticalAppsList getApps() {
@@ -136,7 +140,6 @@
      */
     public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
         mNumAppsPerRow = numAppsPerRow;
-
         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
@@ -169,7 +172,6 @@
                 AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET);
         putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
                 AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH);
-
         if (FeatureFlags.DISCOVERY_ENABLED) {
             putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
                     AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER);
@@ -485,8 +487,23 @@
      */
     @Override
     protected int getAvailableScrollHeight() {
-        return getPaddingTop() + getCurrentScrollY(mApps.getAdapterItems().size(), 0)
-                - getHeight() + getPaddingBottom();
+        return getPaddingTop() + getCurrentScrollY(getAdapter().getItemCount(), 0)
+                - getHeight() + getPaddingBottom() + mUserProfileTabContentHeight;
+    }
+
+    public int getScrollBarTop() {
+        return super.getScrollBarTop() + mUserProfileTabContentHeight;
+    }
+
+    /**
+     * Returns the height of the fast scroll bar
+     */
+    public int getScrollbarTrackHeight() {
+        return super.getScrollbarTrackHeight() + mUserProfileTabContentHeight;
+    }
+
+    public RecyclerViewFastScroller getScrollbar() {
+        return mScrollbar;
     }
 
     /**
@@ -587,7 +604,7 @@
         private void reset(boolean shouldSpring) {
             float y = getContentTranslationY();
             if (Float.compare(y, 0) != 0) {
-                if (FeatureFlags.LAUNCHER3_PHYSICS && shouldSpring) {
+                if (mSpringAnimationHandler != null && shouldSpring) {
                     // We calculate our own velocity to give the springs the desired effect.
                     float velocity = y / getDampedOverScroll(getHeight()) * MAX_RELEASE_VELOCITY;
                     // We want to negate the velocity because we are moving to 0 from -1 due to the
@@ -614,4 +631,5 @@
             return OverScroll.dampedScroll(y, getHeight());
         }
     }
+
 }
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index f0b650b..f9dde2f 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -30,6 +30,7 @@
 import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.ComponentKeyMapper;
+import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LabelComparator;
 
 import java.util.ArrayList;
@@ -165,7 +166,7 @@
 
     // The set of apps from the system not including predictions
     private final List<AppInfo> mApps = new ArrayList<>();
-    private final HashMap<ComponentKey, AppInfo> mComponentToAppMap = new HashMap<>();
+    private final HashMap<ComponentKey, AppInfo> mComponentToAppMap;
 
     // The set of filtered apps with the current filter
     private final List<AppInfo> mFilteredApps = new ArrayList<>();
@@ -188,13 +189,20 @@
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
     private int mNumAppRowsInAdapter;
+    private ItemInfoMatcher mItemFilter;
 
-    public AlphabeticalAppsList(Context context) {
+    public AlphabeticalAppsList(Context context, HashMap<ComponentKey, AppInfo> componentToAppMap) {
+        mComponentToAppMap = componentToAppMap;
         mLauncher = Launcher.getLauncher(context);
         mIndexer = new AlphabeticIndexCompat(context);
         mAppNameComparator = new AppInfoComparator(context);
     }
 
+    public void updateItemFilter(ItemInfoMatcher itemFilter) {
+        this.mItemFilter = itemFilter;
+        onAppsUpdated();
+    }
+
     /**
      * Sets the number of apps per row.
      */
@@ -374,40 +382,18 @@
     }
 
     /**
-     * Sets the current set of apps.
-     */
-    public void setApps(List<AppInfo> apps) {
-        mComponentToAppMap.clear();
-        addOrUpdateApps(apps);
-    }
-
-    /**
-     * Adds or updates existing apps in the list
-     */
-    public void addOrUpdateApps(List<AppInfo> apps) {
-        for (AppInfo app : apps) {
-            mComponentToAppMap.put(app.toComponentKey(), app);
-        }
-        onAppsUpdated();
-    }
-
-    /**
-     * Removes some apps from the list.
-     */
-    public void removeApps(List<AppInfo> apps) {
-        for (AppInfo app : apps) {
-            mComponentToAppMap.remove(app.toComponentKey());
-        }
-        onAppsUpdated();
-    }
-
-    /**
      * Updates internals when the set of apps are updated.
      */
-    private void onAppsUpdated() {
+    void onAppsUpdated() {
         // Sort the list of apps
         mApps.clear();
-        mApps.addAll(mComponentToAppMap.values());
+
+        for (AppInfo app : mComponentToAppMap.values()) {
+            if (mItemFilter == null || mItemFilter.matches(app, null)) {
+                mApps.add(app);
+            }
+        }
+
         Collections.sort(mApps, mAppNameComparator);
 
         // As a special case for some languages (currently only Simplified Chinese), we may need to
@@ -474,42 +460,45 @@
         mFastScrollerSections.clear();
         mAdapterItems.clear();
 
-        if (DEBUG_PREDICTIONS) {
-            if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
-                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle())));
-                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle())));
-                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle())));
-                mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
-                        Process.myUserHandle())));
-            }
-        }
-
-        // Process the predicted app components
-        mPredictedApps.clear();
-        if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
-            mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
-
-            if (!mPredictedApps.isEmpty()) {
-                // Add a section for the predictions
-                lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
-                mFastScrollerSections.add(lastFastScrollerSectionInfo);
-
-                // Add the predicted app items
-                for (AppInfo info : mPredictedApps) {
-                    AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info,
-                            appIndex++);
-                    if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
-                        lastFastScrollerSectionInfo.fastScrollToItem = appItem;
-                    }
-                    mAdapterItems.add(appItem);
-                    mFilteredApps.add(info);
+        if (!FeatureFlags.ALL_APPS_TABS_ENABLED) {
+            if (DEBUG_PREDICTIONS) {
+                if (mPredictedAppComponents.isEmpty() && !mApps.isEmpty()) {
+                    mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                            Process.myUserHandle())));
+                    mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                            Process.myUserHandle())));
+                    mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                            Process.myUserHandle())));
+                    mPredictedAppComponents.add(new ComponentKeyMapper<AppInfo>(new ComponentKey(mApps.get(0).componentName,
+                            Process.myUserHandle())));
                 }
-
-                mAdapterItems.add(AdapterItem.asPredictionDivider(position++));
             }
+
+            // Process the predicted app components
+            mPredictedApps.clear();
+            if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
+                mPredictedApps.addAll(processPredictedAppComponents(mPredictedAppComponents));
+
+                if (!mPredictedApps.isEmpty()) {
+                    // Add a section for the predictions
+                    lastFastScrollerSectionInfo = new FastScrollSectionInfo("");
+                    mFastScrollerSections.add(lastFastScrollerSectionInfo);
+
+                    // Add the predicted app items
+                    for (AppInfo info : mPredictedApps) {
+                        AdapterItem appItem = AdapterItem.asPredictedApp(position++, "", info,
+                                appIndex++);
+                        if (lastFastScrollerSectionInfo.fastScrollToItem == null) {
+                            lastFastScrollerSectionInfo.fastScrollToItem = appItem;
+                        }
+                        mAdapterItems.add(appItem);
+                        mFilteredApps.add(info);
+                    }
+
+                    mAdapterItems.add(AdapterItem.asPredictionDivider(position++));
+                }
+            }
+
         }
 
         // Recreate the filtered and sectioned apps (for convenience for the grid layout) from the
@@ -626,7 +615,6 @@
         if (mSearchResults == null) {
             return mApps;
         }
-
         ArrayList<AppInfo> result = new ArrayList<>();
         for (ComponentKey key : mSearchResults) {
             AppInfo match = mComponentToAppMap.get(key);
@@ -648,10 +636,6 @@
         return result;
     }
 
-    public AppInfo findApp(ComponentKeyMapper<AppInfo> mapper) {
-        return mapper.getItem(mComponentToAppMap);
-    }
-
     /**
      * Returns the cached section name for the given title, recomputing and updating the cache if
      * the title has no cached section name.
diff --git a/src/com/android/launcher3/allapps/FloatingHeaderHandler.java b/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
new file mode 100644
index 0000000..984966b
--- /dev/null
+++ b/src/com/android/launcher3/allapps/FloatingHeaderHandler.java
@@ -0,0 +1,139 @@
+/*
+ * 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.allapps;
+
+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 com.android.launcher3.R;
+
+public class FloatingHeaderHandler extends RecyclerView.OnScrollListener {
+
+    private final int mMaxTranslation;
+    private final View mHeaderView;
+    private final PredictionRowView mContentView;
+    private final RecyclerView mMainRV;
+    private final RecyclerView mWorkRV;
+    private final Rect mClip = new Rect(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+    private boolean mHeaderHidden;
+    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) {
+        mHeaderView = header;
+        mContentView = mHeaderView.findViewById(R.id.header_content);
+        mContentView.getLayoutParams().height = contentHeight;
+        mMaxTranslation = contentHeight;
+        mMainRV = personalRV;
+        mMainRV.addOnScrollListener(this);
+        mWorkRV = workRV;
+        if (workRV != null) {
+            workRV.addOnScrollListener(this);
+        }
+        setMainActive(true);
+    }
+
+    public void setMainActive(boolean active) {
+        mMainRVActive = active;
+        mSnappedScrolledY = getCurrentScroll() - mMaxTranslation;
+        setExpanded(true);
+    }
+
+    public View getHeaderView() {
+        return mHeaderView;
+    }
+
+    public PredictionRowView getContentView() {
+        return mContentView;
+    }
+
+    @Override
+    public void onScrolled(RecyclerView rv, int dx, int dy) {
+        boolean isMainRV = rv == mMainRV;
+        if (isMainRV != mMainRVActive) {
+            return;
+        }
+
+        int current = isMainRV
+                ? (mMainScrolledY -= dy)
+                : (mWorkScrolledY -= dy);
+
+        if (dy == 0) {
+            setExpanded(true);
+        } else {
+            moved(current);
+            apply();
+        }
+    }
+
+    private void moved(final int currentScrollY) {
+        if (mHeaderHidden) {
+            if (currentScrollY <= mSnappedScrolledY) {
+                mSnappedScrolledY = currentScrollY;
+            } else {
+                mHeaderHidden = false;
+            }
+            mTranslationY = currentScrollY;
+        } else {
+            mTranslationY = currentScrollY - mSnappedScrolledY - mMaxTranslation;
+
+            // update state vars
+            if (mTranslationY >= 0) { // expanded: must not move down further
+                mTranslationY = 0;
+                mSnappedScrolledY = currentScrollY - mMaxTranslation;
+            } else if (mTranslationY <= -mMaxTranslation) { // hide or stay hidden
+                mHeaderHidden = true;
+                mSnappedScrolledY = currentScrollY;
+            }
+        }
+    }
+
+    private void apply() {
+        mTranslationY = Math.max(mTranslationY, -mMaxTranslation);
+        mHeaderView.setTranslationY(mTranslationY);
+        mClip.top = mMaxTranslation + mTranslationY;
+        mMainRV.setClipBounds(mClip);
+        if (mWorkRV != null) {
+            mWorkRV.setClipBounds(mClip);
+        }
+    }
+
+    private void setExpanded(boolean expand) {
+        int translateTo = expand ? 0 : -mMaxTranslation;
+        mTranslationY = translateTo;
+        apply();
+
+        mHeaderHidden = !expand;
+        mSnappedScrolledY = expand ? getCurrentScroll() - mMaxTranslation : getCurrentScroll();
+    }
+
+    public boolean isExpanded() {
+        return !mHeaderHidden;
+    }
+
+    private int getCurrentScroll() {
+        return mMainRVActive ? mMainScrolledY : mWorkScrolledY;
+    }
+
+}
diff --git a/src/com/android/launcher3/allapps/InterceptingViewPager.java b/src/com/android/launcher3/allapps/InterceptingViewPager.java
new file mode 100644
index 0000000..3524ca9
--- /dev/null
+++ b/src/com/android/launcher3/allapps/InterceptingViewPager.java
@@ -0,0 +1,91 @@
+/*
+ * 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.allapps;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.touch.SwipeDetector;
+
+import static android.view.MotionEvent.INVALID_POINTER_ID;
+
+
+public class InterceptingViewPager extends ViewPager {
+
+
+    private final PointF mDownPos = new PointF();
+    private final PointF mLastPos = new PointF();
+    private final int mSlop;
+    private int mActivePointerId = INVALID_POINTER_ID;
+
+    public InterceptingViewPager(@NonNull Context context) {
+        super(context);
+        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+    }
+
+    public InterceptingViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        mSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        boolean result = super.onInterceptTouchEvent(ev);
+        if (!result) {
+            switch (ev.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    mActivePointerId = ev.getPointerId(0);
+                    mDownPos.set(ev.getX(), ev.getY());
+                    mLastPos.set(mDownPos);
+                    break;
+                case MotionEvent.ACTION_POINTER_UP:
+                    int ptrIdx = ev.getActionIndex();
+                    int ptrId = ev.getPointerId(ptrIdx);
+                    if (ptrId == mActivePointerId) {
+                        final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
+                        mDownPos.set(
+                                ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
+                                ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
+                        mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
+                        mActivePointerId = ev.getPointerId(newPointerIdx);
+                    }
+                    break;
+                case MotionEvent.ACTION_MOVE:
+                    int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                    if (pointerIndex == INVALID_POINTER_ID) {
+                        break;
+                    }
+                    float deltaX = ev.getX() - mDownPos.x;
+                    float deltaY = ev.getY() - mDownPos.y;
+                    if (Math.abs(deltaX) > mSlop && Math.abs(deltaX) > Math.abs(deltaY)) {
+                        return true;
+                    }
+                    mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
+                    break;
+                default:
+                    break;
+            }
+        }
+        return result;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/allapps/PredictionRowView.java b/src/com/android/launcher3/allapps/PredictionRowView.java
new file mode 100644
index 0000000..5551f07
--- /dev/null
+++ b/src/com/android/launcher3/allapps/PredictionRowView.java
@@ -0,0 +1,139 @@
+/*
+ * 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.allapps;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.AppInfo;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.ComponentKeyMapper;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+public class PredictionRowView extends LinearLayout {
+
+    private static final String TAG = "PredictionRowView";
+
+    private HashMap<ComponentKey, AppInfo> mComponentToAppMap;
+    private int mNumPredictedAppsPerRow;
+    // The set of predicted app component names
+    private final List<ComponentKeyMapper<AppInfo>> mPredictedAppComponents = new ArrayList<>();
+    // The set of predicted apps resolved from the component names and the current set of apps
+    private final List<AppInfo> mPredictedApps = new ArrayList<>();
+
+    public PredictionRowView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public PredictionRowView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+        setOrientation(LinearLayout.HORIZONTAL);
+    }
+
+    public void setComponentToAppMap(HashMap<ComponentKey, AppInfo> componentToAppMap) {
+        this.mComponentToAppMap = componentToAppMap;
+    }
+
+    /**
+     * Sets the number of apps per row.
+     */
+    public void setNumAppsPerRow(int numPredictedAppsPerRow) {
+        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+    }
+
+    public void onAppsUpdated() {
+        // TODO
+    }
+
+    /**
+     * Returns the predicted apps.
+     */
+    public List<AppInfo> getPredictedApps() {
+        return mPredictedApps;
+    }
+
+    /**
+     * Sets the current set of predicted apps.
+     *
+     * This can be called before we get the full set of applications, we should merge the results
+     * only in onAppsUpdated() which is idempotent.
+     *
+     * If the number of predicted apps is the same as the previous list of predicted apps,
+     * we can optimize by swapping them in place.
+     */
+    public void setPredictedApps(List<ComponentKeyMapper<AppInfo>> apps) {
+        mPredictedAppComponents.clear();
+        mPredictedAppComponents.addAll(apps);
+
+        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);
+            } else {
+                // We need to update the appIndex of all the items.
+                onAppsUpdated();
+            }
+        }
+    }
+
+    private List<AppInfo> processPredictedAppComponents(
+            List<ComponentKeyMapper<AppInfo>> components) {
+        if (mComponentToAppMap.isEmpty()) {
+            // Apps have not been bound yet.
+            return Collections.emptyList();
+        }
+
+        List<AppInfo> predictedApps = new ArrayList<>();
+        for (ComponentKeyMapper<AppInfo> mapper : components) {
+            AppInfo info = mapper.getItem(mComponentToAppMap);
+            if (info != null) {
+                predictedApps.add(info);
+            } else {
+                if (FeatureFlags.IS_DOGFOOD_BUILD) {
+                    Log.e(TAG, "Predicted app not found: " + mapper);
+                }
+            }
+            // Stop at the number of predicted apps
+            if (predictedApps.size() == mNumPredictedAppsPerRow) {
+                break;
+            }
+        }
+        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/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
index 34230e0..f562b6a 100644
--- a/src/com/android/launcher3/allapps/SearchUiManager.java
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -27,7 +27,7 @@
     /**
      * Initializes the search manager.
      */
-    void initialize(AlphabeticalAppsList appsList, AllAppsRecyclerView recyclerView);
+    void initialize(AllAppsContainerView containerView);
 
     /**
      * A {@link SpringAnimation} that will be used when the user flings.
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
index ddf6e58..e65a2c4 100644
--- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -31,19 +31,19 @@
 import android.view.KeyEvent;
 import android.view.View;
 import android.widget.FrameLayout;
+
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
-import com.android.launcher3.allapps.AllAppsGridAdapter;
-import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.allapps.SearchUiManager;
-import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.discovery.AppDiscoveryItem;
 import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.util.ComponentKey;
+
 import java.util.ArrayList;
 
 /**
@@ -60,11 +60,9 @@
 
     private ExtendedEditText mSearchInput;
     private AlphabeticalAppsList mApps;
-    private AllAppsRecyclerView mAppsRecyclerView;
-    private AllAppsGridAdapter mAdapter;
     private View mDivider;
     private HeaderElevationController mElevationController;
-
+    private AllAppsContainerView mAppsView;
     private SpringAnimation mSpring;
 
     public AppsSearchContainerLayout(Context context) {
@@ -124,14 +122,12 @@
 
 
     @Override
-    public void initialize(
-            AlphabeticalAppsList appsList, AllAppsRecyclerView recyclerView) {
-        mApps = appsList;
-        mAppsRecyclerView = recyclerView;
-        mAppsRecyclerView.addOnScrollListener(mElevationController);
-        mAdapter = (AllAppsGridAdapter) mAppsRecyclerView.getAdapter();
+    public void initialize(AllAppsContainerView appsView) {
+        mApps = appsView.getApps();
+        mAppsView = appsView;
+        appsView.addElevationController(mElevationController);
         mSearchBarController.initialize(
-                new DefaultAppSearchAlgorithm(appsList.getApps()), mSearchInput, mLauncher, this);
+                new DefaultAppSearchAlgorithm(mApps.getApps()), mSearchInput, mLauncher, this);
     }
 
     @Override
@@ -174,7 +170,7 @@
         if (apps != null) {
             mApps.setOrderedFilter(apps);
             notifyResultChanged();
-            mAdapter.setLastSearchQuery(query);
+            mAppsView.setLastSearchQuery(query);
         }
     }
 
@@ -201,7 +197,7 @@
 
     private void notifyResultChanged() {
         mElevationController.reset();
-        mAppsRecyclerView.onSearchResultsChanged();
+        mAppsView.onSearchResultsChanged();
     }
 
     @Override
diff --git a/src/com/android/launcher3/config/BaseFlags.java b/src/com/android/launcher3/config/BaseFlags.java
index a03dabb..1924710 100644
--- a/src/com/android/launcher3/config/BaseFlags.java
+++ b/src/com/android/launcher3/config/BaseFlags.java
@@ -59,4 +59,6 @@
     // Features to control Launcher3Go behavior
     public static final boolean GO_DISABLE_WIDGETS = false;
 
+    // When enabled shows a work profile tab in all apps
+    public static final boolean ALL_APPS_TABS_ENABLED = false;
 }
diff --git a/src/com/android/launcher3/states/InternalStateHandler.java b/src/com/android/launcher3/states/InternalStateHandler.java
new file mode 100644
index 0000000..c6feefc
--- /dev/null
+++ b/src/com/android/launcher3/states/InternalStateHandler.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.launcher3.states;
+
+import android.content.Intent;
+import android.os.Binder;
+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
+ */
+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);
+
+    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 static void handleNewIntent(Launcher launcher, Intent intent, boolean alreadyOnHome) {
+        if (intent.getExtras() != null) {
+            IBinder stateBinder = intent.getExtras().getBinder(EXTRA_STATE_HANDLER);
+            if (stateBinder instanceof InternalStateHandler) {
+                InternalStateHandler handler = (InternalStateHandler) stateBinder;
+                launcher.setOnResumeCallback(handler);
+                handler.onNewIntent(launcher, alreadyOnHome);
+            }
+            intent.getExtras().remove(EXTRA_STATE_HANDLER);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index 18787b6..daedaef 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -93,6 +93,19 @@
         };
     }
 
+    /**
+     * Returns a new matcher which returns the opposite boolean value of the provided
+     * {@param matcher}.
+     */
+    public static ItemInfoMatcher not(final ItemInfoMatcher matcher) {
+        return new ItemInfoMatcher() {
+            @Override
+            public boolean matches(ItemInfo info, ComponentName cn) {
+                return !matcher.matches(info, cn);
+            }
+        };
+    }
+
     public static ItemInfoMatcher ofUser(final UserHandle user) {
         return new ItemInfoMatcher() {
             @Override
diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
index 8c9a441..8f20a8d 100644
--- a/src/com/android/launcher3/views/RecyclerViewFastScroller.java
+++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java
@@ -98,6 +98,7 @@
     private String mPopupSectionName;
 
     protected BaseRecyclerView mRv;
+    private RecyclerView.OnScrollListener mOnScrollListener;
 
     private int mDownX;
     private int mDownY;
@@ -141,7 +142,10 @@
 
     public void setRecyclerView(BaseRecyclerView rv, TextView popupView) {
         mRv = rv;
-        mRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
+        if (mOnScrollListener != null) {
+            mRv.removeOnScrollListener(mOnScrollListener);
+        }
+        mRv.addOnScrollListener(mOnScrollListener = new RecyclerView.OnScrollListener() {
             @Override
             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                 mDy = dy;