Merge "Don't draw bottom scrim in fully gestural mode" into ub-launcher3-master
diff --git a/quickstep/res/layout/back_gesture_tutorial_fragment.xml b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
index 294e46e..d8c25bd 100644
--- a/quickstep/res/layout/back_gesture_tutorial_fragment.xml
+++ b/quickstep/res/layout/back_gesture_tutorial_fragment.xml
@@ -16,9 +16,7 @@
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:layerType="software"
     android:background="@color/back_gesture_tutorial_background_color">
-    <!--The layout is rendered on the software layer to avoid b/136158117-->
 
     <ImageView
         android:id="@+id/back_gesture_tutorial_fragment_hand_coaching"
diff --git a/quickstep/res/values/colors.xml b/quickstep/res/values/colors.xml
new file mode 100644
index 0000000..3583676
--- /dev/null
+++ b/quickstep/res/values/colors.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources>
+    <color name="back_arrow_color_dark">#99000000</color>
+</resources>
\ No newline at end of file
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
index 3fe91a3..5c2e992 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java
@@ -25,6 +25,7 @@
 import com.android.launcher3.R;
 import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
 import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 
 import java.util.Optional;
 
@@ -79,21 +80,33 @@
         mHandCoachingAnimation.stop();
     }
 
-    void onGestureDetected() {
-        hideHandCoachingAnimation();
-
-        if (mTutorialStep == TutorialStep.CONFIRM) {
+    void onGestureAttempted(BackGestureResult result) {
+        if (mTutorialStep == TutorialStep.CONFIRM
+                && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                    || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) {
             mFragment.closeTutorial();
             return;
         }
 
-        if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
-            mFragment.changeController(TutorialStep.ENGAGED,
-                    TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+        if (!mTutorialTypeInfo.isPresent()) {
             return;
         }
 
-        mFragment.changeController(TutorialStep.CONFIRM);
+        switch (mTutorialTypeInfo.get().getTutorialType()) {
+            case RIGHT_EDGE_BACK_NAVIGATION:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
+                    hideHandCoachingAnimation();
+                    mFragment.changeController(
+                            TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION);
+                }
+                break;
+            case LEFT_EDGE_BACK_NAVIGATION:
+                if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) {
+                    hideHandCoachingAnimation();
+                    mFragment.changeController(TutorialStep.CONFIRM);
+                }
+                break;
+        }
     }
 
     abstract Optional<Integer> getTitleStringId();
diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
index 54408ce..593b695 100644
--- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
+++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java
@@ -17,21 +17,26 @@
 
 import android.content.ActivityNotFoundException;
 import android.content.Intent;
+import android.graphics.Insets;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.WindowInsets;
 
+import androidx.annotation.NonNull;
 import androidx.fragment.app.Fragment;
 
 import com.android.launcher3.R;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
+import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
 
 import java.net.URISyntaxException;
 import java.util.Optional;
 
 /** Shows the Back gesture interactive tutorial. */
-public class BackGestureTutorialFragment extends Fragment {
+public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback {
 
     private static final String LOG_TAG = "TutorialFragment";
     private static final String KEY_TUTORIAL_STEP = "tutorialStep";
@@ -47,6 +52,7 @@
     private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
     private View mRootView;
     private BackGestureTutorialHandAnimation mHandCoachingAnimation;
+    private EdgeBackGestureHandler mEdgeBackGestureHandler;
 
     public static BackGestureTutorialFragment newInstance(
             TutorialStep tutorialStep, TutorialType tutorialType) {
@@ -64,17 +70,25 @@
         Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
         mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
         mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
+        mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
+        mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this);
     }
 
     @Override
     public View onCreateView(
-            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         super.onCreateView(inflater, container, savedInstanceState);
 
         mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
                 container, /* attachToRoot= */ false);
         mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
                 .setOnClickListener(this::onCloseButtonClicked);
+        mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
+            Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
+            mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
+            return insets;
+        });
+        mRootView.setOnTouchListener(mEdgeBackGestureHandler);
         mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
 
         return mRootView;
@@ -92,6 +106,14 @@
         mHandCoachingAnimation.stop();
     }
 
+    void onAttachedToWindow() {
+        mEdgeBackGestureHandler.setIsEnabled(true);
+    }
+
+    void onDetachedFromWindow() {
+        mEdgeBackGestureHandler.setIsEnabled(false);
+    }
+
     @Override
     public void onSaveInstanceState(Bundle savedInstanceState) {
         savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
@@ -125,10 +147,9 @@
         this.mTutorialType = tutorialType;
     }
 
-    void onBackPressed() {
-        if (mTutorialController.isPresent()) {
-            mTutorialController.get().onGestureDetected();
-        }
+    @Override
+    public void onBackGestureAttempted(BackGestureResult result) {
+        mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result));
     }
 
     void closeTutorial() {
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
new file mode 100644
index 0000000..04cd2f4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2020 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.interaction;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemProperties;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+import com.android.launcher3.ResourceUtils;
+
+/**
+ * Utility class to handle edge swipes for back gestures.
+ *
+ * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java.
+ */
+public class EdgeBackGestureHandler implements DisplayListener, OnTouchListener {
+
+    private static final String TAG = "EdgeBackGestureHandler";
+    private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt(
+            "gestures.back_timeout", 250);
+
+    private final Context mContext;
+
+    private final Point mDisplaySize = new Point();
+    private final int mDisplayId;
+
+    // The edge width where touch down is allowed
+    private int mEdgeWidth;
+    // The bottom gesture area height
+    private int mBottomGestureHeight;
+    // The slop to distinguish between horizontal and vertical motion
+    private final float mTouchSlop;
+    // Duration after which we consider the event as longpress.
+    private final int mLongPressTimeout;
+
+    private final PointF mDownPoint = new PointF();
+    private boolean mThresholdCrossed = false;
+    private boolean mAllowGesture = false;
+    private boolean mIsEnabled;
+    private int mLeftInset;
+    private int mRightInset;
+
+    private EdgeBackGesturePanel mEdgeBackPanel;
+    private BackGestureAttemptCallback mGestureCallback;
+
+    private final EdgeBackGesturePanel.BackCallback mBackCallback =
+            new EdgeBackGesturePanel.BackCallback() {
+                @Override
+                public void triggerBack() {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+                                ? BackGestureResult.BACK_COMPLETED_FROM_LEFT
+                                : BackGestureResult.BACK_COMPLETED_FROM_RIGHT);
+                    }
+                }
+
+                @Override
+                public void cancelBack() {
+                    if (mGestureCallback != null) {
+                        mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel()
+                                ? BackGestureResult.BACK_CANCELLED_FROM_LEFT
+                                : BackGestureResult.BACK_CANCELLED_FROM_RIGHT);
+                    }
+                }
+            };
+
+    EdgeBackGestureHandler(Context context) {
+        final Resources res = context.getResources();
+        mContext = context;
+        mDisplayId = context.getDisplay() == null
+                ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId();
+
+        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
+                ViewConfiguration.getLongPressTimeout());
+
+        mBottomGestureHeight =
+            ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res);
+        mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res);
+    }
+
+    void setIsEnabled(boolean isEnabled) {
+        if (isEnabled == mIsEnabled) {
+            return;
+        }
+        mIsEnabled = isEnabled;
+
+        if (mEdgeBackPanel != null) {
+            mEdgeBackPanel.onDestroy();
+            mEdgeBackPanel = null;
+        }
+
+        if (!mIsEnabled) {
+            mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+        } else {
+            updateDisplaySize();
+            mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
+                    new Handler(Looper.getMainLooper()));
+
+            // Add a nav bar panel window.
+            mEdgeBackPanel = new EdgeBackGesturePanel(mContext);
+            mEdgeBackPanel.setBackCallback(mBackCallback);
+            mEdgeBackPanel.setLayoutParams(createLayoutParams());
+            updateDisplaySize();
+        }
+    }
+
+    void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) {
+        mGestureCallback = callback;
+    }
+
+    private WindowManager.LayoutParams createLayoutParams() {
+        Resources resources = mContext.getResources();
+        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
+                ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources),
+                ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources),
+                LayoutParams.TYPE_APPLICATION_PANEL,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+                PixelFormat.TRANSLUCENT);
+        layoutParams.setTitle(TAG + mDisplayId);
+        layoutParams.windowAnimations = 0;
+        layoutParams.setFitInsetsTypes(0 /* types */);
+        return layoutParams;
+    }
+
+    @Override
+    public boolean onTouch(View view, MotionEvent motionEvent) {
+        if (mIsEnabled) {
+            onMotionEvent(motionEvent);
+            return true;
+        }
+        return false;
+    }
+
+    private boolean isWithinTouchRegion(int x, int y) {
+        // Disallow if too far from the edge
+        if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
+            return false;
+        }
+
+        // Disallow if we are in the bottom gesture area
+        if (y >= (mDisplaySize.y - mBottomGestureHeight)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private void cancelGesture(MotionEvent ev) {
+        // Send action cancel to reset all the touch events
+        mAllowGesture = false;
+        MotionEvent cancelEv = MotionEvent.obtain(ev);
+        cancelEv.setAction(MotionEvent.ACTION_CANCEL);
+        mEdgeBackPanel.onMotionEvent(cancelEv);
+        cancelEv.recycle();
+    }
+
+    private void onMotionEvent(MotionEvent ev) {
+        int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
+            mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
+            if (mAllowGesture) {
+                mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge);
+                mEdgeBackPanel.onMotionEvent(ev);
+
+                mDownPoint.set(ev.getX(), ev.getY());
+                mThresholdCrossed = false;
+            }
+        } else if (mAllowGesture) {
+            if (!mThresholdCrossed) {
+                if (action == MotionEvent.ACTION_POINTER_DOWN) {
+                    // We do not support multi touch for back gesture
+                    cancelGesture(ev);
+                    return;
+                } else if (action == MotionEvent.ACTION_MOVE) {
+                    if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
+                        cancelGesture(ev);
+                        return;
+                    }
+                    float dx = Math.abs(ev.getX() - mDownPoint.x);
+                    float dy = Math.abs(ev.getY() - mDownPoint.y);
+                    if (dy > dx && dy > mTouchSlop) {
+                        cancelGesture(ev);
+                        return;
+
+                    } else if (dx > dy && dx > mTouchSlop) {
+                        mThresholdCrossed = true;
+                    }
+                }
+
+            }
+
+            // forward touch
+            mEdgeBackPanel.onMotionEvent(ev);
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            if (!mAllowGesture && mGestureCallback != null) {
+                mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED);
+            }
+        }
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) { }
+
+    @Override
+    public void onDisplayRemoved(int displayId) { }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        if (displayId == mDisplayId) {
+            updateDisplaySize();
+        }
+    }
+
+    private void updateDisplaySize() {
+        mContext.getDisplay().getRealSize(mDisplaySize);
+        if (mEdgeBackPanel != null) {
+            mEdgeBackPanel.setDisplaySize(mDisplaySize);
+        }
+    }
+
+    void setInsets(int leftInset, int rightInset) {
+        mLeftInset = leftInset;
+        mRightInset = rightInset;
+    }
+
+    enum BackGestureResult {
+        UNKNOWN,
+        BACK_COMPLETED_FROM_LEFT,
+        BACK_COMPLETED_FROM_RIGHT,
+        BACK_CANCELLED_FROM_LEFT,
+        BACK_CANCELLED_FROM_RIGHT,
+        BACK_NOT_STARTED,
+    }
+
+    /** Callback to let the UI react to attempted back gestures. */
+    interface BackGestureAttemptCallback {
+        /** Called whenever any touch is completed. */
+        void onBackGestureAttempted(BackGestureResult result);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
new file mode 100644
index 0000000..34eeafc
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/interaction/EdgeBackGesturePanel.java
@@ -0,0 +1,701 @@
+/*
+ * Copyright (C) 2019 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.interaction;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+import androidx.core.math.MathUtils;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatPropertyCompat;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+import com.android.launcher3.R;
+import com.android.launcher3.ResourceUtils;
+import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.util.VibratorWrapper;
+
+/** Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarEdgePanel.java. */
+public class EdgeBackGesturePanel extends View {
+
+    private static final String LOG_TAG = "EdgeBackGesturePanel";
+
+    private static final long DISAPPEAR_FADE_ANIMATION_DURATION_MS = 80;
+    private static final long DISAPPEAR_ARROW_ANIMATION_DURATION_MS = 100;
+
+    /**
+     * The time required since the first vibration effect to automatically trigger a click
+     */
+    private static final int GESTURE_DURATION_FOR_CLICK_MS = 400;
+
+    /**
+     * The basic translation in dp where the arrow resides
+     */
+    private static final int BASE_TRANSLATION_DP = 32;
+
+    /**
+     * The length of the arrow leg measured from the center to the end
+     */
+    private static final int ARROW_LENGTH_DP = 18;
+
+    /**
+     * The angle measured from the xAxis, where the leg is when the arrow rests
+     */
+    private static final int ARROW_ANGLE_WHEN_EXTENDED_DEGREES = 56;
+
+    /**
+     * The angle that is added per 1000 px speed to the angle of the leg
+     */
+    private static final int ARROW_ANGLE_ADDED_PER_1000_SPEED = 4;
+
+    /**
+     * The maximum angle offset allowed due to speed
+     */
+    private static final int ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES = 4;
+
+    /**
+     * The thickness of the arrow. Adjusted to match the home handle (approximately)
+     */
+    private static final float ARROW_THICKNESS_DP = 2.5f;
+
+    /**
+     * The amount of rubber banding we do for the vertical translation
+     */
+    private static final int RUBBER_BAND_AMOUNT = 15;
+
+    /**
+     * The interpolator used to rubberband
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR =
+            new PathInterpolator(1.0f / 5.0f, 1.0f, 1.0f, 1.0f);
+
+    /**
+     * The amount of rubber banding we do for the translation before base translation
+     */
+    private static final int RUBBER_BAND_AMOUNT_APPEAR = 4;
+
+    /**
+     * The interpolator used to rubberband the appearing of the arrow.
+     */
+    private static final Interpolator RUBBER_BAND_INTERPOLATOR_APPEAR =
+            new PathInterpolator(1.0f / RUBBER_BAND_AMOUNT_APPEAR, 1.0f, 1.0f, 1.0f);
+
+    private final WindowManager mWindowManager;
+    private BackCallback mBackCallback;
+
+    /**
+     * The paint the arrow is drawn with
+     */
+    private final Paint mPaint = new Paint();
+
+    private final float mDensity;
+    private final float mBaseTranslation;
+    private final float mArrowLength;
+    private final float mArrowThickness;
+
+    /**
+     * The minimum delta needed in movement for the arrow to change direction / stop triggering back
+     */
+    private final float mMinDeltaForSwitch;
+    // The closest to y = 0 that the arrow will be displayed.
+    private int mMinArrowPosition;
+    // The amount the arrow is shifted to avoid the finger.
+    private int mFingerOffset;
+
+    private final float mSwipeThreshold;
+    private final Path mArrowPath = new Path();
+    private final Point mDisplaySize = new Point();
+
+    private final SpringAnimation mAngleAnimation;
+    private final SpringAnimation mTranslationAnimation;
+    private final SpringAnimation mVerticalTranslationAnimation;
+    private final SpringForce mAngleAppearForce;
+    private final SpringForce mAngleDisappearForce;
+    private final ValueAnimator mArrowDisappearAnimation;
+    private final SpringForce mRegularTranslationSpring;
+    private final SpringForce mTriggerBackSpring;
+
+    private VelocityTracker mVelocityTracker;
+    private int mArrowPaddingEnd;
+    private WindowManager.LayoutParams mLayoutParams;
+
+    /**
+     * True if the panel is currently on the left of the screen
+     */
+    private boolean mIsLeftPanel;
+
+    private float mStartX;
+    private float mStartY;
+    private float mCurrentAngle;
+    /**
+     * The current translation of the arrow
+     */
+    private float mCurrentTranslation;
+    /**
+     * Where the arrow will be in the resting position.
+     */
+    private float mDesiredTranslation;
+
+    private boolean mDragSlopPassed;
+    private boolean mArrowsPointLeft;
+    private float mMaxTranslation;
+    private boolean mTriggerBack;
+    private float mPreviousTouchTranslation;
+    private float mTotalTouchDelta;
+    private float mVerticalTranslation;
+    private float mDesiredVerticalTranslation;
+    private float mDesiredAngle;
+    private float mAngleOffset;
+    private float mDisappearAmount;
+    private long mVibrationTime;
+    private int mScreenSize;
+
+    private final DynamicAnimation.OnAnimationEndListener mSetGoneEndListener =
+            new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(
+                        DynamicAnimation animation, boolean canceled, float value, float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        setVisibility(GONE);
+                    }
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_ANGLE =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("currentAngle") {
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setCurrentAngle(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getCurrentAngle();
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_TRANSLATION =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("currentTranslation") {
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setCurrentTranslation(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getCurrentTranslation();
+                }
+            };
+
+    private static final FloatPropertyCompat<EdgeBackGesturePanel> CURRENT_VERTICAL_TRANSLATION =
+            new FloatPropertyCompat<EdgeBackGesturePanel>("verticalTranslation") {
+
+                @Override
+                public void setValue(EdgeBackGesturePanel object, float value) {
+                    object.setVerticalTranslation(value);
+                }
+
+                @Override
+                public float getValue(EdgeBackGesturePanel object) {
+                    return object.getVerticalTranslation();
+                }
+            };
+
+    public EdgeBackGesturePanel(Context context) {
+        super(context);
+
+        mWindowManager = context.getSystemService(WindowManager.class);
+
+        mDensity = context.getResources().getDisplayMetrics().density;
+
+        mBaseTranslation = dp(BASE_TRANSLATION_DP);
+        mArrowLength = dp(ARROW_LENGTH_DP);
+        mArrowThickness = dp(ARROW_THICKNESS_DP);
+        mMinDeltaForSwitch = dp(32);
+
+        mPaint.setStrokeWidth(mArrowThickness);
+        mPaint.setStrokeCap(Paint.Cap.ROUND);
+        mPaint.setAntiAlias(true);
+        mPaint.setStyle(Paint.Style.STROKE);
+        mPaint.setStrokeJoin(Paint.Join.ROUND);
+
+        mArrowDisappearAnimation = ValueAnimator.ofFloat(0.0f, 1.0f);
+        mArrowDisappearAnimation.setDuration(DISAPPEAR_ARROW_ANIMATION_DURATION_MS);
+        mArrowDisappearAnimation.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+        mArrowDisappearAnimation.addUpdateListener(animation -> {
+            mDisappearAmount = (float) animation.getAnimatedValue();
+            invalidate();
+        });
+
+        mAngleAnimation =
+                new SpringAnimation(this, CURRENT_ANGLE);
+        mAngleAppearForce = new SpringForce()
+                .setStiffness(500)
+                .setDampingRatio(0.5f);
+        mAngleDisappearForce = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+                .setFinalPosition(90);
+        mAngleAnimation.setSpring(mAngleAppearForce).setMaxValue(90);
+
+        mTranslationAnimation =
+                new SpringAnimation(this, CURRENT_TRANSLATION);
+        mRegularTranslationSpring = new SpringForce()
+                .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTriggerBackSpring = new SpringForce()
+                .setStiffness(450)
+                .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        mVerticalTranslationAnimation =
+                new SpringAnimation(this, CURRENT_VERTICAL_TRANSLATION);
+        mVerticalTranslationAnimation.setSpring(
+                new SpringForce()
+                        .setStiffness(SpringForce.STIFFNESS_MEDIUM)
+                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
+        mPaint.setColor(context.getColor(R.color.back_arrow_color_dark));
+        loadDimens();
+        updateArrowDirection();
+
+        mSwipeThreshold = ResourceUtils.getDimenByName(
+            "navigation_edge_action_drag_threshold", context.getResources(), 16 /* defaultValue */);
+        setVisibility(GONE);
+    }
+
+    void onDestroy() {
+        mWindowManager.removeView(this);
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return false;
+    }
+
+    @SuppressLint("RtlHardcoded")
+    void setIsLeftPanel(boolean isLeftPanel) {
+        mIsLeftPanel = isLeftPanel;
+        mLayoutParams.gravity = mIsLeftPanel
+                ? (Gravity.LEFT | Gravity.TOP)
+                : (Gravity.RIGHT | Gravity.TOP);
+    }
+
+    boolean getIsLeftPanel() {
+        return mIsLeftPanel;
+    }
+
+    void setDisplaySize(Point displaySize) {
+        mDisplaySize.set(displaySize.x, displaySize.y);
+        mScreenSize = Math.min(mDisplaySize.x, mDisplaySize.y);
+    }
+
+    void setBackCallback(BackCallback callback) {
+        mBackCallback = callback;
+    }
+
+    void setLayoutParams(WindowManager.LayoutParams layoutParams) {
+        mLayoutParams = layoutParams;
+        mWindowManager.addView(this, mLayoutParams);
+    }
+
+    private float getCurrentAngle() {
+        return mCurrentAngle;
+    }
+
+    private float getCurrentTranslation() {
+        return mCurrentTranslation;
+    }
+
+    void onMotionEvent(MotionEvent event) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                mDragSlopPassed = false;
+                resetOnDown();
+                mStartX = event.getX();
+                mStartY = event.getY();
+                setVisibility(VISIBLE);
+                updatePosition(event.getY());
+                mWindowManager.updateViewLayout(this, mLayoutParams);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                handleMoveEvent(event);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mTriggerBack) {
+                    triggerBack();
+                } else {
+                    cancelBack();
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                cancelBack();
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+                break;
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateArrowDirection();
+        loadDimens();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        float pointerPosition = mCurrentTranslation - mArrowThickness / 2.0f;
+        canvas.save();
+        canvas.translate(
+                mIsLeftPanel ? pointerPosition : getWidth() - pointerPosition,
+                (getHeight() * 0.5f) + mVerticalTranslation);
+
+        // Let's calculate the position of the end based on the angle
+        float x = (polarToCartX(mCurrentAngle) * mArrowLength);
+        float y = (polarToCartY(mCurrentAngle) * mArrowLength);
+        Path arrowPath = calculatePath(x, y);
+
+        canvas.drawPath(arrowPath, mPaint);
+        canvas.restore();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mMaxTranslation = getWidth() - mArrowPaddingEnd;
+    }
+
+    private void loadDimens() {
+        Resources res = getResources();
+        mArrowPaddingEnd = ResourceUtils.getDimenByName("navigation_edge_panel_padding", res, 8);
+        mMinArrowPosition = ResourceUtils.getDimenByName("navigation_edge_arrow_min_y", res, 64);
+        mFingerOffset = ResourceUtils.getDimenByName("navigation_edge_finger_offset", res, 48);
+    }
+
+    private void updateArrowDirection() {
+        // Both panels arrow point the same way
+        mArrowsPointLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR;
+        invalidate();
+    }
+
+    private float getStaticArrowWidth() {
+        return polarToCartX(ARROW_ANGLE_WHEN_EXTENDED_DEGREES) * mArrowLength;
+    }
+
+    private float polarToCartX(float angleInDegrees) {
+        return (float) Math.cos(Math.toRadians(angleInDegrees));
+    }
+
+    private float polarToCartY(float angleInDegrees) {
+        return (float) Math.sin(Math.toRadians(angleInDegrees));
+    }
+
+    private Path calculatePath(float x, float y) {
+        if (!mArrowsPointLeft) {
+            x = -x;
+        }
+        float extent = lerp(1.0f, 0.75f, mDisappearAmount);
+        x = x * extent;
+        y = y * extent;
+        mArrowPath.reset();
+        mArrowPath.moveTo(x, y);
+        mArrowPath.lineTo(0, 0);
+        mArrowPath.lineTo(x, -y);
+        return mArrowPath;
+    }
+
+    private static float lerp(float start, float stop, float amount) {
+        return start + (stop - start) * amount;
+    }
+
+    private void triggerBack() {
+        if (mBackCallback != null) {
+            mBackCallback.triggerBack();
+        }
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.computeCurrentVelocity(1000);
+        // Only do the extra translation if we're not already flinging
+        boolean isSlow = Math.abs(mVelocityTracker.getXVelocity()) < 500;
+        if (isSlow
+                || SystemClock.uptimeMillis() - mVibrationTime >= GESTURE_DURATION_FOR_CLICK_MS) {
+            VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+        }
+
+        // Let's also snap the angle a bit
+        if (mAngleOffset > -4) {
+            mAngleOffset = Math.max(-8, mAngleOffset - 8);
+            updateAngle(true /* animated */);
+        }
+
+        // Finally, after the translation, animate back and disappear the arrow
+        Runnable translationEnd = () -> {
+            // let's snap it back
+            mAngleOffset = Math.max(0, mAngleOffset + 8);
+            updateAngle(true /* animated */);
+
+            mTranslationAnimation.setSpring(mTriggerBackSpring);
+            // Translate the arrow back a bit to make for a nice transition
+            setDesiredTranslation(mDesiredTranslation - dp(32), true /* animated */);
+            animate().alpha(0f).setDuration(DISAPPEAR_FADE_ANIMATION_DURATION_MS)
+                    .withEndAction(() -> setVisibility(GONE));
+            mArrowDisappearAnimation.start();
+        };
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(new DynamicAnimation.OnAnimationEndListener() {
+                @Override
+                public void onAnimationEnd(DynamicAnimation animation, boolean canceled,
+                        float value,
+                        float velocity) {
+                    animation.removeEndListener(this);
+                    if (!canceled) {
+                        translationEnd.run();
+                    }
+                }
+            });
+        } else {
+            translationEnd.run();
+        }
+    }
+
+    private void cancelBack() {
+        if (mBackCallback != null) {
+            mBackCallback.cancelBack();
+        }
+
+        if (mTranslationAnimation.isRunning()) {
+            mTranslationAnimation.addEndListener(mSetGoneEndListener);
+        } else {
+            setVisibility(GONE);
+        }
+    }
+
+    private void resetOnDown() {
+        animate().cancel();
+        mAngleAnimation.cancel();
+        mTranslationAnimation.cancel();
+        mVerticalTranslationAnimation.cancel();
+        mArrowDisappearAnimation.cancel();
+        mAngleOffset = 0;
+        mTranslationAnimation.setSpring(mRegularTranslationSpring);
+        // Reset the arrow to the side
+        setTriggerBack(false /* triggerBack */, false /* animated */);
+        setDesiredTranslation(0, false /* animated */);
+        setCurrentTranslation(0);
+        updateAngle(false /* animate */);
+        mPreviousTouchTranslation = 0;
+        mTotalTouchDelta = 0;
+        mVibrationTime = 0;
+        setDesiredVerticalTransition(0, false /* animated */);
+    }
+
+    private void handleMoveEvent(MotionEvent event) {
+        float x = event.getX();
+        float y = event.getY();
+        float touchTranslation = Math.abs(x - mStartX);
+        float yOffset = y - mStartY;
+        float delta = touchTranslation - mPreviousTouchTranslation;
+        if (Math.abs(delta) > 0) {
+            if (Math.signum(delta) == Math.signum(mTotalTouchDelta)) {
+                mTotalTouchDelta += delta;
+            } else {
+                mTotalTouchDelta = delta;
+            }
+        }
+        mPreviousTouchTranslation = touchTranslation;
+
+        // Apply a haptic on drag slop passed
+        if (!mDragSlopPassed && touchTranslation > mSwipeThreshold) {
+            mDragSlopPassed = true;
+            VibratorWrapper.INSTANCE.get(getContext()).vibrate(VibratorWrapper.EFFECT_CLICK);
+            mVibrationTime = SystemClock.uptimeMillis();
+
+            // Let's show the arrow and animate it in!
+            mDisappearAmount = 0.0f;
+            setAlpha(1f);
+            // And animate it go to back by default!
+            setTriggerBack(true /* triggerBack */, true /* animated */);
+        }
+
+        // Let's make sure we only go to the baseextend and apply rubberbanding afterwards
+        if (touchTranslation > mBaseTranslation) {
+            float diff = touchTranslation - mBaseTranslation;
+            float progress = MathUtils.clamp(diff / (mScreenSize - mBaseTranslation), 0, 1);
+            progress = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                    * (mMaxTranslation - mBaseTranslation);
+            touchTranslation = mBaseTranslation + progress;
+        } else {
+            float diff = mBaseTranslation - touchTranslation;
+            float progress = MathUtils.clamp(diff / mBaseTranslation, 0, 1);
+            progress = RUBBER_BAND_INTERPOLATOR_APPEAR.getInterpolation(progress)
+                    * (mBaseTranslation / RUBBER_BAND_AMOUNT_APPEAR);
+            touchTranslation = mBaseTranslation - progress;
+        }
+        // By default we just assume the current direction is kept
+        boolean triggerBack = mTriggerBack;
+
+        //  First lets see if we had continuous motion in one direction for a while
+        if (Math.abs(mTotalTouchDelta) > mMinDeltaForSwitch) {
+            triggerBack = mTotalTouchDelta > 0;
+        }
+
+        // Then, let's see if our velocity tells us to change direction
+        mVelocityTracker.computeCurrentVelocity(1000);
+        float xVelocity = mVelocityTracker.getXVelocity();
+        float yVelocity = mVelocityTracker.getYVelocity();
+        float velocity = (float) Math.hypot(xVelocity, yVelocity);
+        mAngleOffset = Math.min(velocity / 1000 * ARROW_ANGLE_ADDED_PER_1000_SPEED,
+                ARROW_MAX_ANGLE_SPEED_OFFSET_DEGREES) * Math.signum(xVelocity);
+        if (mIsLeftPanel && mArrowsPointLeft || !mIsLeftPanel && !mArrowsPointLeft) {
+            mAngleOffset *= -1;
+        }
+
+        // Last if the direction in Y is bigger than X * 2 we also abort
+        if (Math.abs(yOffset) > Math.abs(x - mStartX) * 2) {
+            triggerBack = false;
+        }
+        setTriggerBack(triggerBack, true /* animated */);
+
+        if (!mTriggerBack) {
+            touchTranslation = 0;
+        } else if (mIsLeftPanel && mArrowsPointLeft
+                || (!mIsLeftPanel && !mArrowsPointLeft)) {
+            // If we're on the left we should move less, because the arrow is facing the other
+            // direction
+            touchTranslation -= getStaticArrowWidth();
+        }
+        setDesiredTranslation(touchTranslation, true /* animated */);
+        updateAngle(true /* animated */);
+
+        float maxYOffset = getHeight() / 2.0f - mArrowLength;
+        float progress =
+                MathUtils.clamp(Math.abs(yOffset) / (maxYOffset * RUBBER_BAND_AMOUNT), 0, 1);
+        float verticalTranslation = RUBBER_BAND_INTERPOLATOR.getInterpolation(progress)
+                * maxYOffset * Math.signum(yOffset);
+        setDesiredVerticalTransition(verticalTranslation, true /* animated */);
+    }
+
+    private void updatePosition(float touchY) {
+        float position = touchY - mFingerOffset;
+        position = Math.max(position, mMinArrowPosition);
+        position -= mLayoutParams.height / 2.0f;
+        mLayoutParams.y = MathUtils.clamp((int) position, 0, mDisplaySize.y);
+    }
+
+    private void setDesiredVerticalTransition(float verticalTranslation, boolean animated) {
+        if (mDesiredVerticalTranslation != verticalTranslation) {
+            mDesiredVerticalTranslation = verticalTranslation;
+            if (!animated) {
+                setVerticalTranslation(verticalTranslation);
+            } else {
+                mVerticalTranslationAnimation.animateToFinalPosition(verticalTranslation);
+            }
+            invalidate();
+        }
+    }
+
+    private void setVerticalTranslation(float verticalTranslation) {
+        mVerticalTranslation = verticalTranslation;
+        invalidate();
+    }
+
+    private float getVerticalTranslation() {
+        return mVerticalTranslation;
+    }
+
+    private void setDesiredTranslation(float desiredTranslation, boolean animated) {
+        if (mDesiredTranslation != desiredTranslation) {
+            mDesiredTranslation = desiredTranslation;
+            if (!animated) {
+                setCurrentTranslation(desiredTranslation);
+            } else {
+                mTranslationAnimation.animateToFinalPosition(desiredTranslation);
+            }
+        }
+    }
+
+    private void setCurrentTranslation(float currentTranslation) {
+        mCurrentTranslation = currentTranslation;
+        invalidate();
+    }
+
+    private void setTriggerBack(boolean triggerBack, boolean animated) {
+        if (mTriggerBack != triggerBack) {
+            mTriggerBack = triggerBack;
+            mAngleAnimation.cancel();
+            updateAngle(animated);
+            // Whenever the trigger back state changes the existing translation animation should be
+            // cancelled
+            mTranslationAnimation.cancel();
+        }
+    }
+
+    private void updateAngle(boolean animated) {
+        float newAngle = mTriggerBack ? ARROW_ANGLE_WHEN_EXTENDED_DEGREES + mAngleOffset : 90;
+        if (newAngle != mDesiredAngle) {
+            if (!animated) {
+                setCurrentAngle(newAngle);
+            } else {
+                mAngleAnimation.setSpring(mTriggerBack ? mAngleAppearForce : mAngleDisappearForce);
+                mAngleAnimation.animateToFinalPosition(newAngle);
+            }
+            mDesiredAngle = newAngle;
+        }
+    }
+
+    private void setCurrentAngle(float currentAngle) {
+        mCurrentAngle = currentAngle;
+        invalidate();
+    }
+
+    private float dp(float dp) {
+        return mDensity * dp;
+    }
+
+    /** Callback to let the gesture handler react to the detected back gestures. */
+    interface BackCallback {
+        /** Indicates that a Back gesture was recognized. */
+        void triggerBack();
+
+        /** Indicates that the gesture was cancelled. */
+        void cancelBack();
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 8081ad7..4815366 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -30,12 +30,11 @@
 import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
 
 import java.util.List;
-import java.util.Optional;
 
 /** Shows the gesture interactive sandbox in full screen mode. */
 public class GestureSandboxActivity extends FragmentActivity {
 
-    Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
+    private BackGestureTutorialFragment mFragment;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -43,10 +42,10 @@
         requestWindowFeature(Window.FEATURE_NO_TITLE);
         setContentView(R.layout.back_gesture_tutorial_activity);
 
-        mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
-                TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
+        mFragment = BackGestureTutorialFragment.newInstance(
+            TutorialStep.ENGAGED, TutorialType.RIGHT_EDGE_BACK_NAVIGATION);
         getSupportFragmentManager().beginTransaction()
-                .add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
+                .add(R.id.back_gesture_tutorial_fragment_container, mFragment)
                 .commit();
     }
 
@@ -54,6 +53,13 @@
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
         disableSystemGestures();
+        mFragment.onAttachedToWindow();
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mFragment.onDetachedFromWindow();
     }
 
     @Override
@@ -64,13 +70,6 @@
         }
     }
 
-    @Override
-    public void onBackPressed() {
-        if (mFragment.isPresent()) {
-            mFragment.get().onBackPressed();
-        }
-    }
-
     private void hideSystemUI() {
         getWindow().getDecorView().setSystemUiVisibility(
                 View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
diff --git a/res/layout/work_mode_switch.xml b/res/layout/work_mode_switch.xml
new file mode 100644
index 0000000..9cb7ce8
--- /dev/null
+++ b/res/layout/work_mode_switch.xml
@@ -0,0 +1,38 @@
+<?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.WorkModeSwitch
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    style="@style/PrimaryMediumText"
+    android:id="@+id/work_mode_toggle"
+    android:drawableStart="@drawable/ic_corp"
+    android:drawablePadding="16dp"
+    android:drawableTint="?attr/workProfileOverlayTextColor"
+    android:textColor="?attr/workProfileOverlayTextColor"
+    android:layout_alignParentBottom="true"
+    android:ellipsize="end"
+    android:gravity="start"
+    android:lines="1"
+    android:showText="false"
+    android:textSize="16sp"
+    android:background="?attr/allAppsScrimColor"
+    android:text="@string/work_profile_toggle_label"
+    android:paddingBottom="@dimen/all_apps_work_profile_tab_footer_padding"
+    android:paddingLeft="@dimen/all_apps_work_profile_tab_footer_padding"
+    android:paddingRight="@dimen/all_apps_work_profile_tab_footer_padding"
+    android:paddingTop="@dimen/all_apps_work_profile_tab_footer_padding"
+/>
diff --git a/res/layout/work_tab_footer.xml b/res/layout/work_tab_footer.xml
deleted file mode 100644
index 264e273..0000000
--- a/res/layout/work_tab_footer.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2017 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<com.android.launcher3.views.WorkFooterContainer
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:id="@+id/work_toggle_container"
-    android:focusable="true"
-    android:orientation="horizontal"
-    android:background="?attr/allAppsScrimColor"
-    android:paddingBottom="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingLeft="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingRight="@dimen/all_apps_work_profile_tab_footer_padding"
-    android:paddingTop="@dimen/all_apps_work_profile_tab_footer_padding">
-
-    <TextView
-        style="@style/PrimaryMediumText"
-        android:id="@+id/work_mode_label"
-        android:layout_width="0dp"
-        android:layout_weight="1"
-        android:drawableStart="@drawable/ic_corp"
-        android:drawablePadding="16dp"
-        android:drawableTint="?attr/workProfileOverlayTextColor"
-        android:textColor="?attr/workProfileOverlayTextColor"
-        android:layout_height="wrap_content"
-        android:ellipsize="end"
-        android:gravity="center_vertical"
-        android:lines="1"
-        android:minHeight="24dp"
-        android:paddingEnd="12dp"
-        android:text="@string/work_profile_toggle_label"
-        android:textSize="16sp"/>
-    <com.android.launcher3.allapps.WorkModeSwitch
-        android:id="@+id/work_mode_toggle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"/>
-
-</com.android.launcher3.views.WorkFooterContainer>
\ No newline at end of file
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index e6f8a85..21a8fd4 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -46,6 +46,7 @@
 import com.android.launcher3.Launcher.OnResumeCallback;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.dot.DotInfo;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.IconShape;
@@ -65,7 +66,7 @@
  * too aggressive.
  */
 public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, OnResumeCallback,
-        IconLabelDotView {
+        IconLabelDotView, DraggableView {
 
     private static final int DISPLAY_WORKSPACE = 0;
     private static final int DISPLAY_ALL_APPS = 1;
@@ -103,7 +104,7 @@
 
     private final ActivityContext mActivity;
     private Drawable mIcon;
-    private final boolean mCenterVertically;
+    private boolean mCenterVertically;
 
     private final int mDisplay;
 
@@ -701,4 +702,24 @@
     public int getIconSize() {
         return mIconSize;
     }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_ICON;
+    }
+
+    @Override
+    public void getVisualDragBounds(Rect bounds) {
+        DeviceProfile grid = mActivity.getDeviceProfile();
+        BubbleTextView.getIconBounds(this, bounds, grid.iconSizePx);
+    }
+
+    @Override
+    public void prepareDrawDragView() {
+        if (getIcon() instanceof FastBitmapDrawable) {
+            FastBitmapDrawable icon = (FastBitmapDrawable) getIcon();
+            icon.setScale(1f);
+        }
+        setForceHideDot(true);
+    }
 }
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index f2d07f2..9682d09 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -56,6 +56,7 @@
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.graphics.RotationMode;
@@ -100,6 +101,11 @@
     @Thunk final int[] mTmpPoint = new int[2];
     @Thunk final int[] mTempLocation = new int[2];
 
+    // Used to visualize / debug the Grid of the CellLayout
+    private static final boolean VISUALIZE_GRID = false;
+    private Rect mVisualizeGridRect = new Rect();
+    private Paint mVisualizeGridPaint = new Paint();
+
     private GridOccupancy mOccupied;
     private GridOccupancy mTmpOccupied;
 
@@ -462,6 +468,37 @@
             mFolderLeaveBehind.drawLeaveBehind(canvas);
             canvas.restore();
         }
+
+        if (VISUALIZE_GRID) {
+            visualizeGrid(canvas);
+        }
+    }
+
+    protected void visualizeGrid(Canvas canvas) {
+        mVisualizeGridRect.set(0, 0, mCellWidth, mCellHeight);
+        mVisualizeGridPaint.setStrokeWidth(4);
+
+        for (int i = 0; i < mCountX; i++) {
+            for (int j = 0; j < mCountY; j++) {
+                canvas.save();
+
+                int transX = i * mCellWidth;
+                int transY = j * mCellHeight;
+
+                canvas.translate(getPaddingLeft() + transX, getPaddingTop() + transY);
+
+                mVisualizeGridPaint.setStyle(Paint.Style.FILL);
+                mVisualizeGridPaint.setColor(Color.argb(80, 255, 100, 100));
+
+                canvas.drawRect(mVisualizeGridRect, mVisualizeGridPaint);
+
+                mVisualizeGridPaint.setStyle(Paint.Style.STROKE);
+                mVisualizeGridPaint.setColor(Color.argb(255, 255, 100, 100));
+
+                canvas.drawRect(mVisualizeGridRect, mVisualizeGridPaint);
+                canvas.restore();
+            }
+        }
     }
 
     @Override
@@ -928,8 +965,8 @@
         return false;
     }
 
-    void visualizeDropLocation(View v, DragPreviewProvider outlineProvider, int cellX, int cellY,
-            int spanX, int spanY, boolean resize, DropTarget.DragObject dragObject) {
+    void visualizeDropLocation(DraggableView v, DragPreviewProvider outlineProvider, int cellX, int
+            cellY, int spanX, int spanY, boolean resize, DropTarget.DragObject dragObject) {
         final int oldDragCellX = mDragCell[0];
         final int oldDragCellY = mDragCell[1];
 
@@ -939,9 +976,6 @@
 
         Bitmap dragOutline = outlineProvider.generatedDragOutline;
         if (cellX != oldDragCellX || cellY != oldDragCellY) {
-            Point dragOffset = dragObject.dragView.getDragVisualizeOffset();
-            Rect dragRegion = dragObject.dragView.getDragRegion();
-
             mDragCell[0] = cellX;
             mDragCell[1] = cellY;
 
@@ -950,50 +984,27 @@
             mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length;
             Rect r = mDragOutlines[mDragOutlineCurrent];
 
+            cellToRect(cellX, cellY, spanX, spanY, r);
+            int left = r.left;
+            int top = r.top;
+
+            int width = dragOutline.getWidth();
+            int height = dragOutline.getHeight();
+
             if (resize) {
-                cellToRect(cellX, cellY, spanX, spanY, r);
-                if (v instanceof LauncherAppWidgetHostView) {
-                    DeviceProfile profile = mActivity.getWallpaperDeviceProfile();
-                    Utilities.shrinkRect(r, profile.appWidgetScale.x, profile.appWidgetScale.y);
-                }
-            } else {
-                // Find the top left corner of the rect the object will occupy
-                final int[] topLeft = mTmpPoint;
-                cellToPoint(cellX, cellY, topLeft);
-
-                int left = topLeft[0];
-                int top = topLeft[1];
-
-                if (v != null && dragOffset == null) {
-                    // When drawing the drag outline, it did not account for margin offsets
-                    // added by the view's parent.
-                    MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
-                    left += lp.leftMargin;
-                    top += lp.topMargin;
-
-                    // Offsets due to the size difference between the View and the dragOutline.
-                    // There is a size difference to account for the outer blur, which may lie
-                    // outside the bounds of the view.
-                    top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
-                    // We center about the x axis
-                    left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
-                } else {
-                    if (dragOffset != null && dragRegion != null) {
-                        // Center the drag region *horizontally* in the cell and apply a drag
-                        // outline offset
-                        left += dragOffset.x + ((mCellWidth * spanX) - dragRegion.width()) / 2;
-                        int cHeight = getShortcutsAndWidgets().getCellContentHeight();
-                        int cellPaddingY = (int) Math.max(0, ((mCellHeight - cHeight) / 2f));
-                        top += dragOffset.y + cellPaddingY;
-                    } else {
-                        // Center the drag outline in the cell
-                        left += ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
-                        top += ((mCellHeight * spanY) - dragOutline.getHeight()) / 2;
-                    }
-                }
-                r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight());
+                width = r.width();
+                height = r.height();
             }
 
+            if (v != null && v.getViewType() == DraggableView.DRAGGABLE_ICON) {
+                left +=  ((mCellWidth * spanX) - dragOutline.getWidth()) / 2;
+                int cHeight = getShortcutsAndWidgets().getCellContentHeight();
+                int cellPaddingY = (int) Math.max(0, ((mCellHeight - cHeight) / 2f));
+                top += cellPaddingY;
+            }
+
+            r.set(left, top, left + width, top + height);
+
             Utilities.scaleRectAboutCenter(r, mChildScale);
             mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline);
             mDragOutlineAnims[mDragOutlineCurrent].animateIn();
@@ -1879,7 +1890,7 @@
 
     // This method starts or changes the reorder preview animations
     private void beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution,
-            View dragView, int delay, int mode) {
+            View dragView, int mode) {
         int childCount = mShortcutsAndWidgets.getChildCount();
         for (int i = 0; i < childCount; i++) {
             View child = mShortcutsAndWidgets.getChildAt(i);
@@ -1946,6 +1957,8 @@
 
             this.child = child;
             this.mode = mode;
+
+            // TODO issue!
             setInitialAnimationValues(false);
             finalScale = (mChildScale - (CHILD_DIVIDEND / child.getWidth())) * initScale;
             finalDeltaX = initDeltaX;
@@ -2141,6 +2154,8 @@
     */
     private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX,
             int spanY, View dragView, int[] resultDirection) {
+
+        //TODO(adamcohen) b/151776141 use the items visual center for the direction vector
         int[] targetDestination = new int[2];
 
         findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination);
@@ -2251,7 +2266,7 @@
                 setItemPlacementDirty(false);
             } else {
                 beginOrAdjustReorderPreviewAnimations(swapSolution, dragView,
-                        REORDER_ANIMATION_DURATION, ReorderPreviewAnimation.MODE_PREVIEW);
+                        ReorderPreviewAnimation.MODE_PREVIEW);
             }
             mShortcutsAndWidgets.requestLayout();
         }
@@ -2305,7 +2320,7 @@
 
         if (mode == MODE_SHOW_REORDER_HINT) {
             if (finalSolution != null) {
-                beginOrAdjustReorderPreviewAnimations(finalSolution, dragView, 0,
+                beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
                         ReorderPreviewAnimation.MODE_HINT);
                 result[0] = finalSolution.cellX;
                 result[1] = finalSolution.cellY;
@@ -2345,7 +2360,7 @@
                     setItemPlacementDirty(false);
                 } else {
                     beginOrAdjustReorderPreviewAnimations(finalSolution, dragView,
-                            REORDER_ANIMATION_DURATION,  ReorderPreviewAnimation.MODE_PREVIEW);
+                            ReorderPreviewAnimation.MODE_PREVIEW);
                 }
             }
         } else {
diff --git a/src/com/android/launcher3/DropTarget.java b/src/com/android/launcher3/DropTarget.java
index c03011b..ef02e87 100644
--- a/src/com/android/launcher3/DropTarget.java
+++ b/src/com/android/launcher3/DropTarget.java
@@ -23,6 +23,7 @@
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.folder.FolderNameProvider;
 
 /**
@@ -69,6 +70,10 @@
 
         public FolderNameProvider folderNameProvider;
 
+        /** The source view (ie. icon, widget etc.) that is being dragged and which the
+         * DragView represents. May be an actual View class or a virtual stand-in */
+        public DraggableView originalView = null;
+
         public DragObject(Context context) {
             if (FeatureFlags.FOLDER_NAME_SUGGEST.get()) {
                 folderNameProvider = FolderNameProvider.newInstance(context);
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 1bd8263..c07dd9d 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -145,38 +145,46 @@
             final View child = getChildAt(i);
             if (child.getVisibility() != GONE) {
                 CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
-
-                if (child instanceof LauncherAppWidgetHostView) {
-                    LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
-
-                    // Scale and center the widget to fit within its cells.
-                    DeviceProfile profile = mActivity.getDeviceProfile();
-                    float scaleX = profile.appWidgetScale.x;
-                    float scaleY = profile.appWidgetScale.y;
-
-                    lahv.setScaleToFit(Math.min(scaleX, scaleY));
-                    lahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
-                            -(lp.height - (lp.height * scaleY)) / 2.0f);
-                }
-
-                int childLeft = lp.x;
-                int childTop = lp.y;
-                child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
-
-                if (lp.dropped) {
-                    lp.dropped = false;
-
-                    final int[] cellXY = mTmpCellXY;
-                    getLocationOnScreen(cellXY);
-                    mWallpaperManager.sendWallpaperCommand(getWindowToken(),
-                            WallpaperManager.COMMAND_DROP,
-                            cellXY[0] + childLeft + lp.width / 2,
-                            cellXY[1] + childTop + lp.height / 2, 0, null);
-                }
+                layoutChild(child);
             }
         }
     }
 
+    /**
+     * Core logic to layout a child for this ViewGroup.
+     */
+    public void layoutChild(View child) {
+        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
+        if (child instanceof LauncherAppWidgetHostView) {
+            LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
+
+            // Scale and center the widget to fit within its cells.
+            DeviceProfile profile = mActivity.getDeviceProfile();
+            float scaleX = profile.appWidgetScale.x;
+            float scaleY = profile.appWidgetScale.y;
+
+            lahv.setScaleToFit(Math.min(scaleX, scaleY));
+            lahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
+                    -(lp.height - (lp.height * scaleY)) / 2.0f);
+        }
+
+        int childLeft = lp.x;
+        int childTop = lp.y;
+        child.layout(childLeft, childTop, childLeft + lp.width, childTop + lp.height);
+
+        if (lp.dropped) {
+            lp.dropped = false;
+
+            final int[] cellXY = mTmpCellXY;
+            getLocationOnScreen(cellXY);
+            mWallpaperManager.sendWallpaperCommand(getWindowToken(),
+                    WallpaperManager.COMMAND_DROP,
+                    cellXY[0] + childLeft + lp.width / 2,
+                    cellXY[1] + childTop + lp.height / 2, 0, null);
+        }
+    }
+
+
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (ev.getAction() == ACTION_DOWN && getAlpha() == 0) {
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 46493b7..c42e480 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -70,6 +70,7 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.dragndrop.SpringLoadedDragController;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
@@ -81,7 +82,6 @@
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.pageindicators.WorkspacePageIndicator;
 import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.states.StateAnimationConfig;
 import com.android.launcher3.touch.WorkspaceTouchListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
@@ -1452,12 +1452,17 @@
                     + "View: " + child + "  tag: " + child.getTag();
             throw new IllegalStateException(msg);
         }
-        beginDragShared(child, source, (ItemInfo) dragObject,
+        beginDragShared(child, null, source, (ItemInfo) dragObject,
                 new DragPreviewProvider(child), options);
     }
 
-    public DragView beginDragShared(View child, DragSource source, ItemInfo dragObject,
-            DragPreviewProvider previewProvider, DragOptions dragOptions) {
+    /**
+     * Core functionality for beginning a drag operation for an item that will be dropped within
+     * the workspace
+     */
+    public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
+            ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
+
         float iconScale = 1f;
         if (child instanceof BubbleTextView) {
             Drawable icon = ((BubbleTextView) child).getIcon();
@@ -1466,41 +1471,36 @@
             }
         }
 
+        // Clear the pressed state if necessary
         child.clearFocus();
         child.setPressed(false);
+        if (child instanceof BubbleTextView) {
+            BubbleTextView icon = (BubbleTextView) child;
+            icon.clearPressedBackground();
+        }
+
         mOutlineProvider = previewProvider;
 
         // The drag bitmap follows the touch point around on the screen
         final Bitmap b = previewProvider.createDragBitmap();
         int halfPadding = previewProvider.previewPadding / 2;
-
         float scale = previewProvider.getScaleAndPosition(b, mTempXY);
         int dragLayerX = mTempXY[0];
         int dragLayerY = mTempXY[1];
 
-        DeviceProfile grid = mLauncher.getDeviceProfile();
         Point dragVisualizeOffset = null;
-        Rect dragRect = null;
-        if (child instanceof BubbleTextView) {
-            dragRect = new Rect();
-            BubbleTextView.getIconBounds(child, dragRect, grid.iconSizePx);
+        Rect dragRect = new Rect();
+
+        if (draggableView == null && child instanceof DraggableView) {
+            draggableView = (DraggableView) child;
+        }
+
+        if (draggableView != null) {
+            draggableView.getVisualDragBounds(dragRect);
             dragLayerY += dragRect.top;
-            // Note: The dragRect is used to calculate drag layer offsets, but the
-            // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
-            dragVisualizeOffset = new Point(- halfPadding, halfPadding);
-        } else if (child instanceof FolderIcon) {
-            int previewSize = grid.folderIconSizePx;
-            dragVisualizeOffset = new Point(- halfPadding, halfPadding - child.getPaddingTop());
-            dragRect = new Rect(0, child.getPaddingTop(), child.getWidth(), previewSize);
-        } else if (previewProvider instanceof ShortcutDragPreviewProvider) {
             dragVisualizeOffset = new Point(- halfPadding, halfPadding);
         }
 
-        // Clear the pressed state if necessary
-        if (child instanceof BubbleTextView) {
-            BubbleTextView icon = (BubbleTextView) child;
-            icon.clearPressedBackground();
-        }
 
         if (child.getParent() instanceof ShortcutAndWidgetContainer) {
             mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
@@ -1511,13 +1511,13 @@
                     .showForIcon((BubbleTextView) child);
             if (popupContainer != null) {
                 dragOptions.preDragCondition = popupContainer.createPreDragCondition();
-
                 mLauncher.getUserEventDispatcher().resetElapsedContainerMillis("dragging started");
             }
         }
 
-        DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
-                dragObject, dragVisualizeOffset, dragRect, scale * iconScale, scale, dragOptions);
+        DragView dv = mDragController.startDrag(b, draggableView, dragLayerX, dragLayerY, source,
+                dragObject, dragVisualizeOffset, dragRect, scale * iconScale,
+                scale, dragOptions);
         dv.setIntrinsicIconScaleFactor(dragOptions.intrinsicIconScaleFactor);
         return dv;
     }
@@ -2186,7 +2186,7 @@
                     item.spanY, child, mTargetCell);
 
             if (!nearestDropOccupied) {
-                mDragTargetLayout.visualizeDropLocation(child, mOutlineProvider,
+                mDragTargetLayout.visualizeDropLocation(d.originalView, mOutlineProvider,
                         mTargetCell[0], mTargetCell[1], item.spanX, item.spanY, false, d);
             } else if ((mDragMode == DRAG_MODE_NONE || mDragMode == DRAG_MODE_REORDER)
                     && !mReorderAlarm.alarmPending() && (mLastReorderX != reorderX ||
@@ -2279,7 +2279,8 @@
 
     private void manageFolderFeedback(float distance, DragObject dragObject) {
         if (distance > mMaxDistanceForFolderCreation) {
-            if (mDragMode != DRAG_MODE_NONE) {
+            if ((mDragMode == DRAG_MODE_ADD_TO_FOLDER
+                    || mDragMode == DRAG_MODE_CREATE_FOLDER)) {
                 setDragMode(DRAG_MODE_NONE);
             }
             return;
@@ -2368,7 +2369,7 @@
             }
 
             boolean resize = resultSpan[0] != spanX || resultSpan[1] != spanY;
-            mDragTargetLayout.visualizeDropLocation(child, mOutlineProvider,
+            mDragTargetLayout.visualizeDropLocation(dragObject.originalView, mOutlineProvider,
                 mTargetCell[0], mTargetCell[1], resultSpan[0], resultSpan[1], resize, dragObject);
         }
     }
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index e085ff0..10a3060 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -62,7 +62,6 @@
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.RecyclerViewFastScroller;
 import com.android.launcher3.views.SpringRelativeLayout;
-import com.android.launcher3.views.WorkFooterContainer;
 
 import java.util.ArrayList;
 
@@ -91,7 +90,7 @@
     private AllAppsPagedView mViewPager;
 
     private FloatingHeaderView mHeader;
-    private WorkFooterContainer mWorkFooterContainer;
+    private WorkModeSwitch mWorkModeSwitch;
 
 
     private SpannableStringBuilder mSearchQueryBuilder = null;
@@ -156,8 +155,8 @@
         return mMultiValueAlpha.getProperty(index);
     }
 
-    public WorkFooterContainer getWorkFooterContainer() {
-        return mWorkFooterContainer;
+    public WorkModeSwitch getWorkModeSwitch() {
+        return mWorkModeSwitch;
     }
 
 
@@ -195,7 +194,7 @@
     }
 
     private void resetWorkProfile() {
-        mWorkFooterContainer.refresh();
+        mWorkModeSwitch.refresh();
         mAH[AdapterHolder.WORK].setupOverlay();
         mAH[AdapterHolder.WORK].applyPadding();
     }
@@ -410,9 +409,9 @@
         } else {
             mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null);
             mAH[AdapterHolder.WORK].recyclerView = null;
-            if (mWorkFooterContainer != null) {
-                ((ViewGroup) mWorkFooterContainer.getParent()).removeView(mWorkFooterContainer);
-                mWorkFooterContainer = null;
+            if (mWorkModeSwitch != null) {
+                ((ViewGroup) mWorkModeSwitch.getParent()).removeView(mWorkModeSwitch);
+                mWorkModeSwitch = null;
             }
         }
         setupHeader();
@@ -422,14 +421,11 @@
     }
 
     private void setupWorkToggle() {
-        mWorkFooterContainer = (WorkFooterContainer) mLauncher.getLayoutInflater().inflate(
-                R.layout.work_tab_footer, findViewById(R.id.work_toggle_container));
-        mWorkFooterContainer.setLayoutParams(
-                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                        ViewGroup.LayoutParams.WRAP_CONTENT));
-        this.addView(mWorkFooterContainer);
-        mWorkFooterContainer.setInsets(mInsets);
-        mWorkFooterContainer.post(() -> mAH[AdapterHolder.WORK].applyPadding());
+        mWorkModeSwitch = (WorkModeSwitch) mLauncher.getLayoutInflater().inflate(
+                R.layout.work_mode_switch, this, false);
+        this.addView(mWorkModeSwitch);
+        mWorkModeSwitch.setInsets(mInsets);
+        mWorkModeSwitch.post(() -> mAH[AdapterHolder.WORK].applyPadding());
     }
 
     private void replaceRVContainer(boolean showTabs) {
@@ -469,8 +465,8 @@
             findViewById(R.id.tab_work)
                     .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK));
         }
-        if (mWorkFooterContainer != null) {
-            mWorkFooterContainer.setWorkTabVisible(pos == AdapterHolder.WORK);
+        if (mWorkModeSwitch != null) {
+            mWorkModeSwitch.setWorkTabVisible(pos == AdapterHolder.WORK);
         }
     }
 
@@ -648,6 +644,8 @@
             if (!mIsWork || recyclerView == null) return;
             boolean workDisabled = UserCache.INSTANCE.get(mLauncher).isAnyProfileQuietModeEnabled();
             if (mWorkDisabled == workDisabled) return;
+            recyclerView.setContentDescription(
+                    workDisabled ? mLauncher.getString(R.string.work_apps_paused_title) : null);
             if (workDisabled) {
                 appsList.updateItemFilter((info, cn) -> false);
                 recyclerView.addAutoSizedOverlay(
@@ -662,8 +660,7 @@
         void applyPadding() {
             if (recyclerView != null) {
                 int bottomOffset =
-                        mWorkFooterContainer != null && mIsWork ? mWorkFooterContainer.getHeight()
-                                : 0;
+                        mWorkModeSwitch != null && mIsWork ? mWorkModeSwitch.getHeight() : 0;
                 recyclerView.setPadding(padding.left, padding.top, padding.right,
                         padding.bottom + bottomOffset);
             }
diff --git a/src/com/android/launcher3/allapps/WorkModeSwitch.java b/src/com/android/launcher3/allapps/WorkModeSwitch.java
index aadb297..f935e4d 100644
--- a/src/com/android/launcher3/allapps/WorkModeSwitch.java
+++ b/src/com/android/launcher3/allapps/WorkModeSwitch.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2020 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.
@@ -15,7 +15,13 @@
  */
 package com.android.launcher3.allapps;
 
+import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
 import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Rect;
 import android.os.AsyncTask;
 import android.os.Process;
 import android.os.UserHandle;
@@ -24,28 +30,45 @@
 import android.view.MotionEvent;
 import android.widget.Switch;
 
+import com.android.launcher3.Insettable;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.pm.UserCache;
 
 import java.lang.ref.WeakReference;
 
-public class WorkModeSwitch extends Switch {
+/**
+ * Work profile toggle switch shown at the bottom of AllApps work tab
+ */
+public class WorkModeSwitch extends Switch implements Insettable {
+
+    private Rect mInsets = new Rect();
+    protected ObjectAnimator mOpenCloseAnimator;
+
 
     public WorkModeSwitch(Context context) {
         super(context);
+        init();
     }
 
     public WorkModeSwitch(Context context, AttributeSet attrs) {
         super(context, attrs);
+        init();
     }
 
     public WorkModeSwitch(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
     }
 
     @Override
     public void setChecked(boolean checked) {
-        // No-op, do not change the checked state until broadcast is received.
+
     }
 
     @Override
@@ -55,15 +78,24 @@
 
     private void setCheckedInternal(boolean checked) {
         super.setChecked(checked);
+        setCompoundDrawablesWithIntrinsicBounds(
+                checked ? R.drawable.ic_corp : R.drawable.ic_corp_off, 0, 0, 0);
     }
 
     public void refresh() {
+        if (!shouldShowWorkSwitch()) return;
         UserCache userManager = UserCache.INSTANCE.get(getContext());
         setCheckedInternal(!userManager.isAnyProfileQuietModeEnabled());
         setEnabled(true);
     }
 
     @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        this.setVisibility(shouldShowWorkSwitch() ? VISIBLE : GONE);
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent ev) {
         return ev.getActionMasked() == MotionEvent.ACTION_MOVE || super.onTouchEvent(ev);
     }
@@ -72,6 +104,24 @@
         new SetQuietModeEnabledAsyncTask(enabled, new WeakReference<>(this)).execute();
     }
 
+    @Override
+    public void setInsets(Rect insets) {
+        int bottomInset = insets.bottom - mInsets.bottom;
+        mInsets.set(insets);
+        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
+                getPaddingBottom() + bottomInset);
+    }
+
+    /**
+     * Animates in/out work profile toggle panel based on the tab user is on
+     */
+    public void setWorkTabVisible(boolean workTabVisible) {
+        if (!shouldShowWorkSwitch()) return;
+
+        mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(ALPHA, workTabVisible ? 1 : 0));
+        mOpenCloseAnimator.start();
+    }
+
     private static final class SetQuietModeEnabledAsyncTask
             extends AsyncTask<Void, Void, Boolean> {
 
@@ -122,4 +172,11 @@
             }
         }
     }
+
+    private boolean shouldShowWorkSwitch() {
+        Launcher launcher = Launcher.getLauncher(getContext());
+        return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher)
+                || launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
+                == PackageManager.PERMISSION_GRANTED);
+    }
 }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 1539747..d0d9aaf 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -137,6 +137,8 @@
      *
      * @param b The bitmap to display as the drag image.  It will be re-scaled to the
      *          enlarged size.
+     * @param originalView The source view (ie. icon, widget etc.) that is being dragged
+     *          and which the DragView represents
      * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
      * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
      * @param source An object representing where the drag originated
@@ -144,7 +146,7 @@
      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
      *          Makes dragging feel more precise, e.g. you can clip out a transparent border
      */
-    public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
+    public DragView startDrag(Bitmap b, DraggableView originalView, int dragLayerX, int dragLayerY,
             DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion,
             float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) {
         if (PROFILE_DRAWING_DURING_DRAG) {
@@ -170,6 +172,7 @@
         mLastDropTarget = null;
 
         mDragObject = new DropTarget.DragObject(mLauncher.getApplicationContext());
+        mDragObject.originalView = originalView;
 
         mIsInPreDrag = mOptions.preDragCondition != null
                 && !mOptions.preDragCondition.shouldStartDrag(0);
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 9d07595..e18ca54 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -41,7 +41,6 @@
 import android.view.accessibility.AccessibilityManager;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
-import android.widget.TextView;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.CellLayout;
@@ -52,7 +51,6 @@
 import com.android.launcher3.Workspace;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.OverviewScrim;
 import com.android.launcher3.graphics.RotationMode;
 import com.android.launcher3.graphics.WorkspaceAndHotseatScrim;
@@ -90,6 +88,8 @@
     private int mTopViewIndex;
     private int mChildCountOnLastUpdate = -1;
 
+    private Rect mTmpRect = new Rect();
+
     // Related to adjacent page hints
     private final ViewGroupFocusHelper mFocusIndicatorHelper;
     private final WorkspaceAndHotseatScrim mWorkspaceScrim;
@@ -254,59 +254,46 @@
 
     public void animateViewIntoPosition(DragView dragView, final View child, int duration,
             View anchorView) {
+
         ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
         CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
         parentChildren.measureChild(child);
+        parentChildren.layoutChild(child);
 
-        Rect r = new Rect();
-        getViewRectRelativeToSelf(dragView, r);
+        getViewRectRelativeToSelf(dragView, mTmpRect);
+        final int fromX = mTmpRect.left;
+        final int fromY = mTmpRect.top;
 
         float coord[] = new float[2];
         float childScale = child.getScaleX();
+
         coord[0] = lp.x + (child.getMeasuredWidth() * (1 - childScale) / 2);
         coord[1] = lp.y + (child.getMeasuredHeight() * (1 - childScale) / 2);
 
         // Since the child hasn't necessarily been laid out, we force the lp to be updated with
         // the correct coordinates (above) and use these to determine the final location
         float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
+
         // We need to account for the scale of the child itself, as the above only accounts for
         // for the scale in parents.
         scale *= childScale;
         int toX = Math.round(coord[0]);
         int toY = Math.round(coord[1]);
         float toScale = scale;
-        if (child instanceof TextView) {
-            TextView tv = (TextView) child;
-            // Account for the source scale of the icon (ie. from AllApps to Workspace, in which
-            // the workspace may have smaller icon bounds).
-            toScale = scale / dragView.getIntrinsicIconScaleFactor();
 
-            // The child may be scaled (always about the center of the view) so to account for it,
-            // we have to offset the position by the scaled size.  Once we do that, we can center
-            // the drag view about the scaled child view.
-            // padding will remain constant (does not scale with size)
-            toY += tv.getPaddingTop();
-            toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2;
-            if (dragView.getDragVisualizeOffset() != null) {
-                toY -=  Math.round(toScale * dragView.getDragVisualizeOffset().y);
-            }
+        if (child instanceof DraggableView) {
+            DraggableView d = (DraggableView) child;
+            d.getVisualDragBounds(mTmpRect);
 
-            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
-        } else if (child instanceof FolderIcon) {
-            // Account for holographic blur padding on the drag view
-            toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop()));
-            toY -= scale * dragView.getBlurSizeOutline() / 2;
-            toY -= (1 - scale) * dragView.getMeasuredHeight() / 2;
-            // Center in the x coordinate about the target's drawable
-            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
-        } else {
-            toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
-            toX -= (Math.round(scale * (dragView.getMeasuredWidth()
-                    - child.getMeasuredWidth()))) / 2;
+            // This accounts for the offset of the DragView created by scaling it about its
+            // center as it animates into place.
+            float scaleShiftX = dragView.getMeasuredWidth() * (1 - scale) / 2;
+            float scaleShiftY = dragView.getMeasuredHeight() * (1 - scale) / 2;
+
+            toX += scale * (mTmpRect.left - dragView.getBlurSizeOutline() / 2) - scaleShiftX;
+            toY += scale * (mTmpRect.top - dragView.getBlurSizeOutline() / 2) - scaleShiftY;
         }
 
-        final int fromX = r.left;
-        final int fromY = r.top;
         child.setVisibility(INVISIBLE);
         Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE);
         animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
diff --git a/src/com/android/launcher3/dragndrop/DraggableView.java b/src/com/android/launcher3/dragndrop/DraggableView.java
new file mode 100644
index 0000000..df99902
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/DraggableView.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2020 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.dragndrop;
+
+import android.graphics.Rect;
+
+/**
+ * Interface defining methods required for drawing and previewing DragViews, drag previews, and
+ * related animations
+ */
+public interface DraggableView {
+    int DRAGGABLE_ICON = 0;
+    int DRAGGABLE_WIDGET = 1;
+
+    /**
+     * Static ctr for a simple instance which just returns the view type.
+     */
+    static DraggableView ofType(int type) {
+        return () -> type;
+    }
+
+    /**
+     * Certain handling of DragViews depend only on whether this is an Icon Type item or a Widget
+     * Type item.
+     *
+     * @return DRAGGABLE_ICON or DRAGGABLE_WIDGET as appropriate
+     */
+    int getViewType();
+
+    /**
+     * Before rendering as a DragView bitmap, some views need a preparation step.
+     */
+    default void prepareDrawDragView() { }
+
+    /**
+     * If an actual View subclass, this method returns the rectangle (within the View's coordinates)
+     * of the visual region that should get dragged. This is used to extract exactly that element
+     * as well as to offset that element as appropriate for various animations
+     *
+     * @param bounds Visual bounds in the views coordinates will be written here.
+     */
+    default void getVisualDragBounds(Rect bounds) { }
+}
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
index 0bb3fba..ea1fbdb 100644
--- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -20,10 +20,10 @@
 
 import android.annotation.TargetApi;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
 import android.graphics.Matrix;
 import android.graphics.Path;
 import android.graphics.Point;
+import android.graphics.Rect;
 import android.graphics.drawable.AdaptiveIconDrawable;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
@@ -33,7 +33,6 @@
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.folder.PreviewBackground;
 import com.android.launcher3.graphics.ShiftedBitmapDrawable;
@@ -50,6 +49,7 @@
     private final Drawable mBadge;
     private final Path mMask;
     private final ConstantState mConstantState;
+    private static final Rect sTmpRect = new Rect();
 
     private FolderAdaptiveIcon(Drawable bg, Drawable fg, Drawable badge, Path mask) {
         super(bg, fg);
@@ -72,23 +72,14 @@
     public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon(
             Launcher launcher, int folderId, Point dragViewSize) {
         Preconditions.assertNonUiThread();
-        int margin = launcher.getResources()
-                .getDimensionPixelSize(R.dimen.blur_size_medium_outline);
-
-        // Allocate various bitmaps on the background thread, because why not!
-        int width = dragViewSize.x - margin;
-        int height = dragViewSize.y - margin;
-        if (width <= 0 || height <= 0) {
-            return null;
-        }
-        final Bitmap badge = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
 
         // Create the actual drawable on the UI thread to avoid race conditions with
         // FolderIcon draw pass
         try {
             return MAIN_EXECUTOR.submit(() -> {
                 FolderIcon icon = launcher.findFolderIcon(folderId);
-                return icon == null ? null : createDrawableOnUiThread(icon, badge, dragViewSize);
+                return icon == null ? null : createDrawableOnUiThread(icon, dragViewSize);
+
             }).get();
         } catch (Exception e) {
             Log.e(TAG, "Unable to create folder icon", e);
@@ -96,49 +87,50 @@
         }
     }
 
-    /**
-     * Initializes various bitmaps on the UI thread and returns the final drawable.
-     */
     private static FolderAdaptiveIcon createDrawableOnUiThread(FolderIcon icon,
-            Bitmap badgeBitmap, Point dragViewSize) {
+                                                               Point dragViewSize) {
         Preconditions.assertUIThread();
-        float margin = icon.getResources().getDimension(R.dimen.blur_size_medium_outline) / 2;
 
-        Canvas c = new Canvas();
+        icon.getPreviewBounds(sTmpRect);
+
         PreviewBackground bg = icon.getFolderBackground();
 
-        // Initialize badge
-        c.setBitmap(badgeBitmap);
-        bg.drawShadow(c);
-        bg.drawBackgroundStroke(c);
-        icon.drawDot(c);
+        // assume square
+        assert (dragViewSize.x == dragViewSize.y);
+        final int previewSize = sTmpRect.width();
 
-        // Initialize preview
-        final float sizeScaleFactor = 1 + 2 * AdaptiveIconDrawable.getExtraInsetFraction();
-        final int previewWidth = (int) (dragViewSize.x * sizeScaleFactor);
-        final int previewHeight = (int) (dragViewSize.y * sizeScaleFactor);
+        final int margin = (dragViewSize.x - previewSize) / 2;
+        final float previewShiftX = -sTmpRect.left + margin;
+        final float previewShiftY = -sTmpRect.top + margin;
 
-        final float shiftFactor = AdaptiveIconDrawable.getExtraInsetFraction() / sizeScaleFactor;
-        final float previewShiftX = shiftFactor * previewWidth;
-        final float previewShiftY = shiftFactor * previewHeight;
-
-        Bitmap previewBitmap = BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight,
+        // Initialize badge, which consists of the outline stroke, shadow and dot; these
+        // must be rendered above the foreground
+        Bitmap badgeBmp = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y,
                 (canvas) -> {
-                    int count = canvas.save();
+                    canvas.save();
                     canvas.translate(previewShiftX, previewShiftY);
-                    icon.getPreviewItemManager().draw(canvas);
-                    canvas.restoreToCount(count);
+                    bg.drawShadow(canvas);
+                    bg.drawBackgroundStroke(canvas);
+                    icon.drawDot(canvas);
+                    canvas.restore();
                 });
 
         // Initialize mask
         Path mask = new Path();
         Matrix m = new Matrix();
-        m.setTranslate(margin, margin);
+        m.setTranslate(previewShiftX, previewShiftY);
         bg.getClipPath().transform(m, mask);
 
-        ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBitmap, margin, margin);
-        ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap,
-                margin - previewShiftX, margin - previewShiftY);
+        Bitmap previewBitmap = BitmapRenderer.createHardwareBitmap(dragViewSize.x, dragViewSize.y,
+                (canvas) -> {
+                    canvas.save();
+                    canvas.translate(previewShiftX, previewShiftY);
+                    icon.getPreviewItemManager().draw(canvas);
+                    canvas.restore();
+                });
+
+        ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBmp, 0, 0);
+        ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap, 0, 0);
 
         return new FolderAdaptiveIcon(new ColorDrawable(bg.getBgColor()), foreground, badge, mask);
     }
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index ab1ff10..f0d18ae 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -63,6 +63,7 @@
 import com.android.launcher3.dragndrop.BaseItemDragListener;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.Executors;
@@ -78,7 +79,8 @@
 /**
  * An icon that can appear on in the workspace representing an {@link Folder}.
  */
-public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView {
+public class FolderIcon extends FrameLayout implements FolderListener, IconLabelDotView,
+        DraggableView {
 
     @Thunk ActivityContext mActivity;
     @Thunk Folder mFolder;
@@ -230,6 +232,16 @@
         mBackground.getBounds(outBounds);
     }
 
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_ICON;
+    }
+
+    @Override
+    public void getVisualDragBounds(Rect bounds) {
+        getPreviewBounds(bounds);
+    }
+
     public float getBackgroundStrokeWidth() {
         return mBackground.getStrokeWidth();
     }
@@ -525,6 +537,10 @@
         invalidate();
     }
 
+    public boolean getIconVisible() {
+        return mBackgroundIsVisible;
+    }
+
     public PreviewBackground getFolderBackground() {
         return mBackground;
     }
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index 94d30f6..ed9dfbb 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -30,14 +30,12 @@
 import android.view.View;
 
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.FastBitmapDrawable;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.icons.BitmapRenderer;
 import com.android.launcher3.widget.LauncherAppWidgetHostView;
-import com.android.launcher3.widget.PendingAppWidgetHostView;
 
 import java.nio.ByteBuffer;
 
@@ -53,7 +51,7 @@
     // The padding added to the drag view during the preview generation.
     public final int previewPadding;
 
-    protected final int blurSizeOutline;
+    public final int blurSizeOutline;
 
     private OutlineGeneratorCallback mOutlineGeneratorCallback;
     public Bitmap generatedDragOutline;
@@ -66,56 +64,25 @@
         mView = view;
         blurSizeOutline =
                 context.getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
-
-        if (mView instanceof BubbleTextView) {
-            Drawable d = ((BubbleTextView) mView).getIcon();
-            Rect bounds = getDrawableBounds(d);
-            previewPadding = blurSizeOutline - bounds.left - bounds.top;
-        } else {
-            previewPadding = blurSizeOutline;
-        }
+        previewPadding = blurSizeOutline;
     }
 
     /**
      * Draws the {@link #mView} into the given {@param destCanvas}.
      */
     protected void drawDragView(Canvas destCanvas, float scale) {
-        destCanvas.save();
+        int saveCount = destCanvas.save();
         destCanvas.scale(scale, scale);
 
-        if (mView instanceof BubbleTextView) {
-            Drawable d = ((BubbleTextView) mView).getIcon();
-            Rect bounds = getDrawableBounds(d);
-            destCanvas.translate(blurSizeOutline / 2 - bounds.left,
-                    blurSizeOutline / 2 - bounds.top);
-            if (d instanceof FastBitmapDrawable) {
-                ((FastBitmapDrawable) d).setScale(1);
-            }
-            d.draw(destCanvas);
-        } else {
-            final Rect clipRect = mTempRect;
-            mView.getDrawingRect(clipRect);
-
-            boolean textVisible = false;
-            if (mView instanceof FolderIcon) {
-                // For FolderIcons the text can bleed into the icon area, and so we need to
-                // hide the text completely (which can't be achieved by clipping).
-                if (((FolderIcon) mView).getTextVisible()) {
-                    ((FolderIcon) mView).setTextVisible(false);
-                    textVisible = true;
-                }
-            }
-            destCanvas.translate(-mView.getScrollX() + blurSizeOutline / 2,
-                    -mView.getScrollY() + blurSizeOutline / 2);
-            destCanvas.clipRect(clipRect);
+        if (mView instanceof DraggableView) {
+            DraggableView dv = (DraggableView) mView;
+            dv.prepareDrawDragView();
+            dv.getVisualDragBounds(mTempRect);
+            destCanvas.translate(blurSizeOutline / 2 - mTempRect.left,
+                    blurSizeOutline / 2 - mTempRect.top);
             mView.draw(destCanvas);
-
-            // Restore text visibility of FolderIcon if necessary
-            if (textVisible) {
-                ((FolderIcon) mView).setTextVisible(true);
-            }
         }
-        destCanvas.restore();
+        destCanvas.restoreToCount(saveCount);
     }
 
     /**
@@ -123,28 +90,15 @@
      * Responsibility for the bitmap is transferred to the caller.
      */
     public Bitmap createDragBitmap() {
-        int width = mView.getWidth();
-        int height = mView.getHeight();
-
-        if (mView instanceof BubbleTextView) {
-            Drawable d = ((BubbleTextView) mView).getIcon();
-            Rect bounds = getDrawableBounds(d);
-            width = bounds.width();
-            height = bounds.height();
-        } else if (mView instanceof LauncherAppWidgetHostView) {
-            float scale = ((LauncherAppWidgetHostView) mView).getScaleToFit();
-            width = (int) (mView.getWidth() * scale);
-            height = (int) (mView.getHeight() * scale);
-
-            if (mView instanceof PendingAppWidgetHostView) {
-                // Use hardware renderer as the icon for the pending app widget may be a hw bitmap
-                return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
-                        height + blurSizeOutline, (c) -> drawDragView(c, scale));
-            } else {
-                // Use software renderer for widgets as we know that they already work
-                return BitmapRenderer.createSoftwareBitmap(width + blurSizeOutline,
-                        height + blurSizeOutline, (c) -> drawDragView(c, scale));
-            }
+        int width = 0;
+        int height = 0;
+        if (mView instanceof DraggableView) {
+            ((DraggableView) mView).getVisualDragBounds(mTempRect);
+            width = mTempRect.width();
+            height = mTempRect.height();
+        } else {
+            width = mView.getWidth();
+            height = mView.getHeight();
         }
 
         return BitmapRenderer.createHardwareBitmap(width + blurSizeOutline,
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 5af5ebb..9bac259 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -59,6 +59,7 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.notification.NotificationKeyData;
@@ -662,7 +663,8 @@
             iconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
             iconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
 
-            DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(),
+            DraggableView draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON);
+            DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getIconView(), draggableView,
                     mContainer, sv.getFinalInfo(),
                     new ShortcutDragPreviewProvider(sv.getIconView(), iconShift),
                     new DragOptions());
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index 04741a1..b0defd4 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -39,7 +39,7 @@
     public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
             new MainThreadInitializedObject<>(VibratorWrapper::new);
 
-    private static final VibrationEffect EFFECT_CLICK =
+    public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
 
     /**
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index fa625ed..d41bb86 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -67,7 +67,6 @@
 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.IconShape;
-import com.android.launcher3.graphics.ShiftedBitmapDrawable;
 import com.android.launcher3.icons.LauncherIcons;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -412,9 +411,8 @@
                 drawable = originalView.getBackground();
             }
         } else {
-            boolean isFolderIcon = originalView instanceof FolderIcon;
-            int width = isFolderIcon ? originalView.getWidth() : (int) pos.width();
-            int height = isFolderIcon ? originalView.getHeight() : (int) pos.height();
+            int width = (int) pos.width();
+            int height = (int) pos.height();
             if (supportsAdaptiveIcons) {
                 drawable = getFullDrawable(l, info, width, height, sTmpObjArray);
                 if (drawable instanceof AdaptiveIconDrawable) {
@@ -499,20 +497,7 @@
                 }
             }
 
-            if (isFolderIcon) {
-                ((FolderIcon) originalView).getPreviewBounds(sTmpRect);
-                float bgStroke = ((FolderIcon) originalView).getBackgroundStrokeWidth();
-                if (mForeground instanceof ShiftedBitmapDrawable) {
-                    ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mForeground;
-                    sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke);
-                    sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke);
-                }
-                if (mBadge instanceof ShiftedBitmapDrawable) {
-                    ShiftedBitmapDrawable sbd = (ShiftedBitmapDrawable) mBadge;
-                    sbd.setShiftX(sbd.getShiftX() - sTmpRect.left - bgStroke);
-                    sbd.setShiftY(sbd.getShiftY() - sTmpRect.top - bgStroke);
-                }
-            } else {
+            if (!isFolderIcon) {
                 Utilities.scaleRectAboutCenter(mStartRevealRect,
                         IconShape.getNormalizationScale());
             }
@@ -615,8 +600,9 @@
     @WorkerThread
     @SuppressWarnings("WrongThread")
     private static int getOffsetForIconBounds(Launcher l, Drawable drawable, RectF position) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
-                !(drawable instanceof AdaptiveIconDrawable)) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+                || !(drawable instanceof AdaptiveIconDrawable)
+                || (drawable instanceof FolderAdaptiveIcon)) {
             return 0;
         }
         int blurSizeOutline =
diff --git a/src/com/android/launcher3/views/WorkFooterContainer.java b/src/com/android/launcher3/views/WorkFooterContainer.java
deleted file mode 100644
index d86d0ff..0000000
--- a/src/com/android/launcher3/views/WorkFooterContainer.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.launcher3.views;
-
-import static com.android.launcher3.util.PackageManagerHelper.hasShortcutsPermission;
-
-import android.animation.ObjectAnimator;
-import android.animation.PropertyValuesHolder;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.graphics.Rect;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.launcher3.Insettable;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.allapps.WorkModeSwitch;
-import com.android.launcher3.pm.UserCache;
-
-/**
- * Container to show work footer in all-apps.
- */
-public class WorkFooterContainer extends LinearLayout implements Insettable {
-    private Rect mInsets = new Rect();
-
-    private WorkModeSwitch mWorkModeSwitch;
-    private TextView mWorkModeLabel;
-
-    protected final ObjectAnimator mOpenCloseAnimator;
-
-    public WorkFooterContainer(Context context) {
-        this(context, null, 0);
-    }
-
-    public WorkFooterContainer(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public WorkFooterContainer(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-        mOpenCloseAnimator = ObjectAnimator.ofPropertyValuesHolder(this);
-    }
-
-    @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        updateTranslation();
-        this.setVisibility(shouldShowWorkFooter() ? VISIBLE : GONE);
-    }
-
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-        mWorkModeSwitch = findViewById(R.id.work_mode_toggle);
-        mWorkModeLabel = findViewById(R.id.work_mode_label);
-    }
-
-    @Override
-    public void offsetTopAndBottom(int offset) {
-        super.offsetTopAndBottom(offset);
-        updateTranslation();
-    }
-
-    private void updateTranslation() {
-        if (getParent() instanceof View) {
-            View parent = (View) getParent();
-            int availableBot = parent.getHeight() - parent.getPaddingBottom();
-            setTranslationY(Math.max(0, availableBot - getBottom()));
-        }
-    }
-
-    @Override
-    public void setInsets(Rect insets) {
-        int bottomInset = insets.bottom - mInsets.bottom;
-        mInsets.set(insets);
-        setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
-                getPaddingBottom() + bottomInset);
-    }
-
-    /**
-     * Animates in/out work profile toggle panel based on the tab user is on
-     */
-    public void setWorkTabVisible(boolean workTabVisible) {
-        if (!shouldShowWorkFooter()) return;
-
-        mOpenCloseAnimator.setValues(PropertyValuesHolder.ofFloat(ALPHA, workTabVisible ? 1 : 0));
-        mOpenCloseAnimator.start();
-    }
-
-    /**
-     * Refreshes views based on current work profile enabled status
-     */
-    public void refresh() {
-        if (!shouldShowWorkFooter()) return;
-        boolean anyProfileQuietModeEnabled = UserCache.INSTANCE.get(
-                getContext()).isAnyProfileQuietModeEnabled();
-
-        mWorkModeLabel.setCompoundDrawablesWithIntrinsicBounds(
-                anyProfileQuietModeEnabled ? R.drawable.ic_corp_off : R.drawable.ic_corp, 0, 0, 0);
-        mWorkModeSwitch.refresh();
-    }
-
-    /**
-     * Returns work mode switch
-     */
-    public WorkModeSwitch getWorkModeSwitch() {
-        return mWorkModeSwitch;
-    }
-
-    private boolean shouldShowWorkFooter() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        return Utilities.ATLEAST_P && (hasShortcutsPermission(launcher)
-                || launcher.checkSelfPermission("android.permission.MODIFY_QUIET_MODE")
-                == PackageManager.PERMISSION_GRANTED);
-    }
-}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index f3fd7ca..c1310e3 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.res.Configuration;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.SparseBooleanArray;
@@ -44,6 +45,7 @@
 import com.android.launcher3.StylusEventHelper;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.util.Executors;
 import com.android.launcher3.util.Themes;
 import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
@@ -52,7 +54,7 @@
  * {@inheritDoc}
  */
 public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
-        implements TouchCompleteListener, View.OnLongClickListener {
+        implements TouchCompleteListener, View.OnLongClickListener, DraggableView {
 
     // Related to the auto-advancing of widgets
     private static final long ADVANCE_INTERVAL = 20000;
@@ -412,4 +414,18 @@
         }
         return false;
     }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_WIDGET;
+    }
+
+    @Override
+    public void getVisualDragBounds(Rect bounds) {
+        int x = (int) (1 - getScaleToFit()) * getMeasuredWidth() / 2;
+        int y = (int) (1 - getScaleToFit()) * getMeasuredWidth() / 2;
+        int width = (int) getScaleToFit() * getMeasuredWidth();
+        int height = (int) getScaleToFit() * getMeasuredHeight();
+        bounds.set(x, y , x + width, y + height);
+    }
 }
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 68b1595..104ad77 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -24,12 +24,15 @@
 import android.view.ViewDebug;
 import android.view.ViewGroup;
 
+import com.android.launcher3.dragndrop.DraggableView;
+
 import java.util.ArrayList;
 
 /**
  * Extension of AppWidgetHostView with support for controlled keyboard navigation.
  */
-public abstract class NavigableAppWidgetHostView extends AppWidgetHostView {
+public abstract class NavigableAppWidgetHostView extends AppWidgetHostView
+        implements DraggableView {
 
     @ViewDebug.ExportedProperty(category = "launcher")
     private boolean mChildrenFocused;
@@ -133,4 +136,14 @@
         // The host view's background changes when selected, to indicate the focus is inside.
         setSelected(childIsFocused);
     }
+
+    @Override
+    public int getViewType() {
+        return DRAGGABLE_WIDGET;
+    }
+
+    @Override
+    public void getVisualDragBounds(Rect bounds) {
+        bounds.set(0, 0 , getMeasuredWidth(), getMeasuredHeight());
+    }
 }
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 662e627..3c11274 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.PendingAddItemInfo;
 import com.android.launcher3.R;
 import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.dragndrop.DraggableView;
 import com.android.launcher3.dragndrop.LivePreviewWidgetCell;
 import com.android.launcher3.graphics.DragPreviewProvider;
 import com.android.launcher3.icons.LauncherIcons;
@@ -79,6 +80,8 @@
 
         mEstimatedCellSize = launcher.getWorkspace().estimateItemSize(mAddInfo);
 
+        DraggableView draggableView;
+
         if (mAddInfo instanceof PendingAddWidgetInfo) {
             PendingAddWidgetInfo createWidgetInfo = (PendingAddWidgetInfo) mAddInfo;
 
@@ -110,6 +113,7 @@
 
             dragOffset = null;
             dragRegion = null;
+            draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_WIDGET);
         } else {
             PendingAddShortcutInfo createShortcutInfo = (PendingAddShortcutInfo) mAddInfo;
             Drawable icon = createShortcutInfo.activityInfo.getFullResIcon(app.getIconCache());
@@ -136,6 +140,7 @@
             dragRegion.top = (mEstimatedCellSize[1]
                     - iconSize - dp.iconTextSizePx - dp.iconDrawablePaddingPx) / 2;
             dragRegion.bottom = dragRegion.top + iconSize;
+            draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON);
         }
 
         // Since we are not going through the workspace for starting the drag, set drag related
@@ -148,8 +153,8 @@
                 + (int) ((scale * preview.getHeight() - preview.getHeight()) / 2);
 
         // Start the drag
-        launcher.getDragController().startDrag(preview, dragLayerX, dragLayerY, source, mAddInfo,
-                dragOffset, dragRegion, scale, scale, options);
+        launcher.getDragController().startDrag(preview, draggableView, dragLayerX, dragLayerY,
+                source, mAddInfo, dragOffset, dragRegion, scale, scale, options);
     }
 
     @Override
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index f5dd995..551b533 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -220,6 +220,7 @@
 
     @Before
     public void setUp() throws Exception {
+        mDevice.executeShellCommand("setprop vendor.battery.defender.disable 1");
         mDevice.executeShellCommand("settings put global stay_on_while_plugged_in 3");
         if (hasSystemUiObject("keyguard_status_view")) {
             Log.d(TAG, "Before unlocking the phone");
diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java
index db2d974..8d571ff 100644
--- a/tests/src/com/android/launcher3/ui/WorkTabTest.java
+++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java
@@ -16,6 +16,7 @@
 package com.android.launcher3.ui;
 
 import static com.android.launcher3.LauncherState.ALL_APPS;
+import static com.android.launcher3.LauncherState.NORMAL;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -31,9 +32,9 @@
 import com.android.launcher3.R;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsPagedView;
+import com.android.launcher3.allapps.WorkModeSwitch;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.views.WorkEduView;
-import com.android.launcher3.views.WorkFooterContainer;
 
 import org.junit.After;
 import org.junit.Before;
@@ -87,7 +88,7 @@
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
         waitForState("Launcher internal state didn't switch to All Apps", () -> ALL_APPS);
         getOnceNotNull("Apps view did not bind",
-                launcher -> launcher.getAppsView().getWorkFooterContainer(), 60000);
+                launcher -> launcher.getAppsView().getWorkModeSwitch(), 60000);
 
         UserManager userManager = getFromLauncher(l -> l.getSystemService(UserManager.class));
         assertEquals(2, userManager.getUserProfiles().size());
@@ -102,10 +103,10 @@
 
         assertTrue(userManager.isQuietModeEnabled(workProfile));
         executeOnLauncher(launcher -> {
-            WorkFooterContainer wf = launcher.getAppsView().getWorkFooterContainer();
+            WorkModeSwitch wf = launcher.getAppsView().getWorkModeSwitch();
             ((AllAppsPagedView) launcher.getAppsView().getContentView()).snapToPageImmediately(
                     AllAppsContainerView.AdapterHolder.WORK);
-            wf.getWorkModeSwitch().toggle();
+            wf.toggle();
         });
         waitForLauncherCondition("Work toggle did not work",
                 l -> l.getSystemService(UserManager.class).isQuietModeEnabled(workProfile));
@@ -158,9 +159,11 @@
 
         // dismiss personal edu
         mDevice.pressHome();
+        waitForState("Launcher did not go home", () -> NORMAL);
 
         // open work tab
         executeOnLauncher(launcher -> launcher.getStateManager().goToState(ALL_APPS));
+        waitForState("Launcher did not switch to all apps", () -> ALL_APPS);
         executeOnLauncher(launcher -> {
             AllAppsPagedView pagedView = (AllAppsPagedView) launcher.getAppsView().getContentView();
             pagedView.setCurrentPage(WORK_PAGE);