Long-press on an app to reveal its shortcuts.

- Add ShortcutsContainerListener to icons on workspace, folders, and
  all apps. This handles long-press and forwards following touches to
  the DeepShortcutsContainer that is created.
- Drag over shortcut before lifting finger to launch it.
- Shortcuts are rendered in pill-shaped DeepShortcutViews,
  which are inside DeepShortcutContainer on DragLayer.
- The shortcut container orients above or below the icon, and left or
  right-aligns with it. Biases for above + left-align.
- Long press a DeepShortcutPill to drag and pin it to the workspace.

Bug: 28980830
Change-Id: I08658d13ae51fe53064644e8d8f7b42f150fdd7d
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 1762ca4..ca60d5c 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -67,6 +67,7 @@
     private final Launcher mLauncher;
     private Drawable mIcon;
     private final Drawable mBackground;
+    private OnLongClickListener mOnLongClickListener;
     private final CheckLongPressHelper mLongPressHelper;
     private final HolographicOutlineHelper mOutlineHelper;
     private final StylusEventHelper mStylusEventHelper;
@@ -271,6 +272,16 @@
     }
 
     @Override
+    public void setOnLongClickListener(OnLongClickListener l) {
+        super.setOnLongClickListener(l);
+        mOnLongClickListener = l;
+    }
+
+    public OnLongClickListener getOnLongClickListener() {
+        return mOnLongClickListener;
+    }
+
+    @Override
     public boolean onTouchEvent(MotionEvent event) {
         // Call the superclass onTouchEvent first, because sometimes it changes the state to
         // isPressed() on an ACTION_UP
diff --git a/src/com/android/launcher3/CheckLongPressHelper.java b/src/com/android/launcher3/CheckLongPressHelper.java
index 483c622..dde733c 100644
--- a/src/com/android/launcher3/CheckLongPressHelper.java
+++ b/src/com/android/launcher3/CheckLongPressHelper.java
@@ -22,10 +22,12 @@
 
 public class CheckLongPressHelper {
 
+    public static final int DEFAULT_LONG_PRESS_TIMEOUT = 300;
+
     @Thunk View mView;
     @Thunk View.OnLongClickListener mListener;
     @Thunk boolean mHasPerformedLongPress;
-    private int mLongPressTimeout = 300;
+    private int mLongPressTimeout = DEFAULT_LONG_PRESS_TIMEOUT;
     private CheckForLongPress mPendingCheckForLongPress;
 
     class CheckForLongPress implements Runnable {
diff --git a/src/com/android/launcher3/ItemInfo.java b/src/com/android/launcher3/ItemInfo.java
index f54a2d4..2a94e55 100644
--- a/src/com/android/launcher3/ItemInfo.java
+++ b/src/com/android/launcher3/ItemInfo.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3;
 
+import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
@@ -137,6 +138,10 @@
         return null;
     }
 
+    public ComponentName getTargetComponent() {
+        return getIntent() == null ? null : getIntent().getComponent();
+    }
+
     public void writeToValues(ContentValues values) {
         values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
         values.put(LauncherSettings.Favorites.CONTAINER, container);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 97af37c..84c29dc 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -59,7 +59,6 @@
 import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
-import android.os.Environment;
 import android.os.Handler;
 import android.os.Message;
 import android.os.StrictMode;
@@ -113,11 +112,10 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
 import com.android.launcher3.logging.FileLog;
-import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.model.WidgetsModel;
 import com.android.launcher3.pageindicators.PageIndicator;
-import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
@@ -136,7 +134,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
 
 /**
  * Default launcher application.
@@ -4062,6 +4059,15 @@
         if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
     }
 
+    public List<String> getShortcutIdsForItem(ItemInfo info) {
+        if (!DeepShortcutManager.supportsShortcuts(info)) {
+            return Collections.EMPTY_LIST;
+        }
+        ComponentName component = info.getTargetComponent();
+        List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
+        return ids == null ? Collections.EMPTY_LIST : ids;
+    }
+
     /**
      * A package was updated.
      *
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 8b42deb..cedbe74 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -47,7 +47,6 @@
 import android.os.Build.VERSION;
 import android.os.Bundle;
 import android.os.PowerManager;
-import android.support.v4.os.BuildCompat;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.TextUtils;
@@ -57,7 +56,9 @@
 import android.util.Pair;
 import android.util.SparseArray;
 import android.util.TypedValue;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewParent;
 import android.widget.Toast;
 
 import com.android.launcher3.compat.UserHandleCompat;
@@ -68,8 +69,10 @@
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Locale;
 import java.util.Set;
 import java.util.concurrent.Executor;
@@ -425,6 +428,30 @@
                 localY < (v.getHeight() + slop);
     }
 
+    /** Translates MotionEvents from src's coordinate system to dst's. */
+    public static void translateEventCoordinates(View src, View dst, MotionEvent dstEvent) {
+        toGlobalMotionEvent(src, dstEvent);
+        toLocalMotionEvent(dst, dstEvent);
+    }
+
+    /**
+     * Emulates View.toGlobalMotionEvent(). This implementation does not handle transformations
+     * (scaleX, scaleY, etc).
+     */
+    private static void toGlobalMotionEvent(View view, MotionEvent event) {
+        view.getLocationOnScreen(sLoc0);
+        event.offsetLocation(sLoc0[0], sLoc0[1]);
+    }
+
+    /**
+     * Emulates View.toLocalMotionEvent(). This implementation does not handle transformations
+     * (scaleX, scaleY, etc).
+     */
+    private static void toLocalMotionEvent(View view, MotionEvent event) {
+        view.getLocationOnScreen(sLoc0);
+        event.offsetLocation(-sLoc0[0], -sLoc0[1]);
+    }
+
     public static int[] getCenterDeltaInScreenSpace(View v0, View v1, int[] delta) {
         v0.getLocationInWindow(sLoc0);
         v1.getLocationInWindow(sLoc1);
@@ -819,6 +846,11 @@
         return true;
     }
 
+    /** Returns whether the collection is null or empty. */
+    public static boolean isEmpty(Collection c) {
+        return c == null || c.isEmpty();
+    }
+
     /**
      * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
      * This allows the badging to be done based on the action bitmap size rather than
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 65fc94c..9366c42 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -77,7 +77,8 @@
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.logging.UserEventDispatcher;
-import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutsContainerListener;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.LongArrayMap;
@@ -1094,6 +1095,10 @@
         if (!(child instanceof Folder)) {
             child.setHapticFeedbackEnabled(false);
             child.setOnLongClickListener(mLongClickListener);
+            if (child instanceof BubbleTextView && DeepShortcutManager.supportsShortcuts(info)) {
+                // TODO: only add this listener if the item has shortcuts associated with it.
+                child.setOnTouchListener(new ShortcutsContainerListener((BubbleTextView) child));
+            }
         }
         if (child instanceof DropTarget) {
             mDragController.addDropTarget((DropTarget) child);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index e67c9df..c3da491 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -18,17 +18,13 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
-import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -45,14 +41,14 @@
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
 import com.android.launcher3.ExtendedEditText;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.folder.Folder;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherTransitionable;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.folder.Folder;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.util.ComponentKey;
 
@@ -133,8 +129,7 @@
  * The all apps view container.
  */
 public class AllAppsContainerView extends BaseContainerView implements DragSource,
-        LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener,
-        AllAppsSearchBarController.Callbacks {
+        LauncherTransitionable, View.OnLongClickListener, AllAppsSearchBarController.Callbacks {
 
     private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
     private static final int MAX_NUM_MERGES_PHONE = 2;
@@ -163,8 +158,6 @@
     private int mRecyclerViewTopBottomPadding;
     // This coordinate is relative to this container view
     private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1);
-    // This coordinate is relative to its parent
-    private final Point mIconLastTouchPos = new Point();
 
     public AllAppsContainerView(Context context) {
         this(context, null);
@@ -181,7 +174,7 @@
         mLauncher = Launcher.getLauncher(context);
         mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
         mApps = new AlphabeticalAppsList(context);
-        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this);
+        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this);
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mItemDecoration = mAdapter.getItemDecoration();
@@ -529,18 +522,6 @@
         return handleTouchEvent(ev);
     }
 
-    @SuppressLint("ClickableViewAccessibility")
-    @Override
-    public boolean onTouch(View v, MotionEvent ev) {
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-            case MotionEvent.ACTION_MOVE:
-                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
-                break;
-        }
-        return false;
-    }
-
     @Override
     public boolean onLongClick(View v) {
         // Return early if this is not initiated from a touch
@@ -553,7 +534,7 @@
         if (!mLauncher.isDraggingEnabled()) return false;
 
         // Start the drag
-        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
+        mLauncher.getWorkspace().beginDragShared(v, this, false);
         // Enter spring loaded mode
         mLauncher.enterSpringLoadedDragMode();
 
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index ca2556e..6540a23 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -17,17 +17,13 @@
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.v4.view.accessibility.AccessibilityRecordCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
-import android.net.Uri;
+import android.support.v4.view.accessibility.AccessibilityRecordCompat;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.Gravity;
@@ -38,13 +34,14 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 import android.widget.TextView;
+
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.util.Thunk;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutsContainerListener;
 
 import java.util.HashMap;
 import java.util.List;
@@ -331,7 +328,6 @@
     private final GridLayoutManager mGridLayoutMgr;
     private final GridSpanSizer mGridSizer;
     private final GridItemDecoration mItemDecoration;
-    private final View.OnTouchListener mTouchListener;
     private final View.OnClickListener mIconClickListener;
     private final View.OnLongClickListener mIconLongClickListener;
 
@@ -357,8 +353,7 @@
     private Intent mMarketSearchIntent;
 
     public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps,
-            View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
-            View.OnLongClickListener iconLongClickListener) {
+            View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
         Resources res = launcher.getResources();
         mLauncher = launcher;
         mApps = apps;
@@ -368,7 +363,6 @@
         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
         mItemDecoration = new GridItemDecoration();
         mLayoutInflater = LayoutInflater.from(launcher);
-        mTouchListener = touchListener;
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
         mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
@@ -454,7 +448,6 @@
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
                         viewType == ICON_VIEW_TYPE ? R.layout.all_apps_icon :
                                 R.layout.all_apps_prediction_bar_icon, parent, false);
-                icon.setOnTouchListener(mTouchListener);
                 icon.setOnClickListener(mIconClickListener);
                 icon.setOnLongClickListener(mIconLongClickListener);
                 icon.setLongPressTimeout(ViewConfiguration.get(parent.getContext())
@@ -490,6 +483,10 @@
                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                 BubbleTextView icon = (BubbleTextView) holder.mContent;
                 icon.applyFromApplicationInfo(info);
+                if (DeepShortcutManager.supportsShortcuts(info)) {
+                    // TODO: only add this listener if the item has shortcuts associated with it.
+                    icon.setOnTouchListener(new ShortcutsContainerListener(icon));
+                }
                 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
                 break;
             }
@@ -497,6 +494,10 @@
                 AppInfo info = mApps.getAdapterItems().get(position).appInfo;
                 BubbleTextView icon = (BubbleTextView) holder.mContent;
                 icon.applyFromApplicationInfo(info);
+                if (DeepShortcutManager.supportsShortcuts(info)) {
+                    // TODO: only add this listener if the item has shortcuts associated with it.
+                    icon.setOnTouchListener(new ShortcutsContainerListener(icon));
+                }
                 icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
                 break;
             }
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index af5ff58..dc93bca 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -45,6 +45,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.TouchController;
 
@@ -247,9 +248,12 @@
 
         mDragObject = new DropTarget.DragObject();
 
+        final Resources res = mLauncher.getResources();
+        final float scaleDps = FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND ?
+                res.getDimensionPixelSize(R.dimen.dragViewScale) : 0f;
         final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
                 registrationY, 0, 0, b.getWidth(), b.getHeight(),
-                initialDragViewScale);
+                initialDragViewScale, scaleDps);
 
         mDragObject.dragComplete = false;
         if (mIsAccessibleDrag) {
@@ -284,6 +288,10 @@
         return dragView;
     }
 
+    public Point getMotionDown() {
+        return new Point(mMotionDownX, mMotionDownY);
+    }
+
     /**
      * Call this from a drag source view like this:
      *
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index 7ad45f9..2164708 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -16,12 +16,6 @@
 
 package com.android.launcher3.dragndrop;
 
-import com.android.launcher3.AnotherWindowDropTarget;
-import com.android.launcher3.DropTarget;
-import com.android.launcher3.ItemInfo;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-
 import android.content.ClipData;
 import android.content.Intent;
 import android.graphics.Canvas;
@@ -30,6 +24,12 @@
 import android.view.MotionEvent;
 import android.view.View;
 
+import com.android.launcher3.AnotherWindowDropTarget;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+
 /**
  * Base class for driving a drag/drop operation.
  */
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 8aed6d8..aebb1fd 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -43,23 +43,22 @@
 
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.CellLayout;
+import com.android.launcher3.DropTargetBar;
 import com.android.launcher3.InsettableFrameLayout;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherAppWidgetHostView;
 import com.android.launcher3.PinchToOverviewListener;
 import com.android.launcher3.R;
-import com.android.launcher3.DropTargetBar;
 import com.android.launcher3.ShortcutAndWidgetContainer;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
+import com.android.launcher3.shortcuts.DeepShortcutsContainer;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.TouchController;
 
@@ -193,22 +192,23 @@
     }
 
     public boolean isEventOverHotseat(MotionEvent ev) {
-        getDescendantRectRelativeToSelf(mLauncher.getHotseat(), mHitRect);
-        return mHitRect.contains((int) ev.getX(), (int) ev.getY());
+        return isEventOverView(mLauncher.getHotseat(), ev);
     }
 
     private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
-        getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
-        return mHitRect.contains((int) ev.getX(), (int) ev.getY());
+        return isEventOverView(folder.getEditTextRegion(), ev);
     }
 
     private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
-        getDescendantRectRelativeToSelf(folder, mHitRect);
-        return mHitRect.contains((int) ev.getX(), (int) ev.getY());
+        return isEventOverView(folder, ev);
     }
 
     private boolean isEventOverDropTargetBar(MotionEvent ev) {
-        getDescendantRectRelativeToSelf(mLauncher.getDropTargetBar(), mHitRect);
+        return isEventOverView(mLauncher.getDropTargetBar(), ev);
+    }
+
+    private boolean isEventOverView(View view, MotionEvent ev) {
+        getDescendantRectRelativeToSelf(view, mHitRect);
         return mHitRect.contains((int) ev.getX(), (int) ev.getY());
     }
 
@@ -251,6 +251,25 @@
                 }
             }
         }
+
+        // Remove the shortcuts container when touching outside of it.
+        DeepShortcutsContainer deepShortcutsContainer = (DeepShortcutsContainer)
+                findViewById(R.id.deep_shortcuts_container);
+        if (deepShortcutsContainer != null) {
+            if (!isEventOverView(deepShortcutsContainer, ev)) {
+                if (isInAccessibleDrag()) {
+                    // Do not close the container if in drag and drop.
+                    if (!isEventOverDropTargetBar(ev)) {
+                        return true;
+                    }
+                } else {
+                    removeView(deepShortcutsContainer);
+                    // We let touches on the original icon go through so that users can launch
+                    // the app with one tap if they don't find a shortcut they want.
+                    return !isEventOverView(deepShortcutsContainer.getDeferredDragIcon(), ev);
+                }
+            }
+        }
         return false;
     }
 
@@ -258,7 +277,6 @@
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         int action = ev.getAction();
 
-
         if (action == MotionEvent.ACTION_DOWN) {
             if (handleTouchDown(ev, true)) {
                 return true;
@@ -275,6 +293,7 @@
             mActiveController = mDragController;
             return true;
         }
+
         if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && mAllAppsController.onInterceptTouchEvent(ev)) {
             mActiveController = mAllAppsController;
             return true;
@@ -395,7 +414,6 @@
         int x = (int) ev.getX();
         int y = (int) ev.getY();
 
-
         if (action == MotionEvent.ACTION_DOWN) {
             if (handleTouchDown(ev, false)) {
                 return true;
@@ -526,6 +544,10 @@
         return new LayoutParams(p);
     }
 
+    public void setController(TouchController controller) {
+        mActiveController = controller;
+    }
+
     public static class LayoutParams extends InsettableFrameLayout.LayoutParams {
         public int x, y;
         public boolean customPosition = false;
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index b1df41b..a5644ad 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -84,16 +84,13 @@
      * @param registrationY The y coordinate of the registration point.
      */
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
-    public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
-            int left, int top, int width, int height, final float initialScale) {
+    public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, int left,
+            int top, int width, int height, final float initialScale, final float finalScaleDps) {
         super(launcher);
         mDragLayer = launcher.getDragLayer();
         mDragController = launcher.getDragController();
 
-        final Resources res = getResources();
-        final float scaleDps = !FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND ? 0f
-                : res.getDimensionPixelSize(R.dimen.dragViewScale);
-        final float scale = (width + scaleDps) / width;
+        final float scale = (width + finalScaleDps) / width;
 
         // Set the initial scale to avoid any jumps
         setScaleX(initialScale);
@@ -349,12 +346,12 @@
      * @param touchX the x coordinate the user touched in DragLayer coordinates
      * @param touchY the y coordinate the user touched in DragLayer coordinates
      */
-    void move(int touchX, int touchY) {
+    public void move(int touchX, int touchY) {
         setTranslationX(touchX - mRegistrationX);
         setTranslationY(touchY - mRegistrationY);
     }
 
-    void remove() {
+    public void remove() {
         if (getParent() != null) {
             mDragLayer.removeView(DragView.this);
         }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 2035f99..e94e02f 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -25,7 +25,6 @@
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Build;
@@ -295,7 +294,7 @@
                 return false;
             }
 
-            mLauncher.getWorkspace().beginDragShared(v, new Point(), this, accessible);
+            mLauncher.getWorkspace().beginDragShared(v, this, accessible);
 
             mCurrentDragInfo = item;
             mEmptyCellRank = item.rank;
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index c6fc4cb..82c79a9 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -46,6 +46,8 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.pageindicators.PageIndicator;
 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.ShortcutsContainerListener;
 import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
@@ -234,6 +236,10 @@
         textView.applyFromShortcutInfo(item, mIconCache);
         textView.setOnClickListener(mFolder);
         textView.setOnLongClickListener(mFolder);
+        if (DeepShortcutManager.supportsShortcuts(item)) {
+            // TODO: only add this listener if the item has shortcuts associated with it.
+            textView.setOnTouchListener(new ShortcutsContainerListener(textView));
+        }
         textView.setOnFocusChangeListener(mFocusIndicatorHelper);
         textView.setOnKeyListener(mKeyListener);
 
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
index 97c384d..0d5102f 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutManager.java
@@ -27,6 +27,8 @@
 import android.os.Bundle;
 import android.util.Log;
 
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.LauncherSettings;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.UserHandleCompat;
 
@@ -54,6 +56,11 @@
         mLauncherApps = (LauncherApps) context.getSystemService(Context.LAUNCHER_APPS_SERVICE);
     }
 
+    public static boolean supportsShortcuts(ItemInfo info) {
+        return info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
+                || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
+    }
+
     public void onShortcutsChanged(List<ShortcutInfoCompat> shortcuts) {
         // mShortcutCache.removeShortcuts(shortcuts);
     }
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
new file mode 100644
index 0000000..7997d1e
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.shortcuts;
+
+import android.content.Context;
+import android.support.annotation.IntDef;
+import android.util.AttributeSet;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.R;
+
+/**
+ * A {@link BubbleTextView} that represents a deep shortcut within an app.
+ */
+public class DeepShortcutView extends BubbleTextView {
+
+    private static final float HOVER_SCALE = 1.1f;
+    // The direction this view should translate when animating the hover state.
+    // This allows hovered shortcuts to "push" other shortcuts away.
+    @IntDef({DIRECTION_UP, DIRECTION_NONE, DIRECTION_DOWN})
+    public @interface TranslationDirection {}
+
+    public static final int DIRECTION_UP = -1;
+    public static final int DIRECTION_NONE = 0;
+    public static final int DIRECTION_DOWN = 1;
+    @TranslationDirection
+    private int mTranslationDirection = DIRECTION_NONE;
+
+    private int mSpacing;
+    private int mTop;
+    private boolean mIsHoveringOver = false;
+
+    public DeepShortcutView(Context context) {
+        this(context, null, 0);
+    }
+
+    public DeepShortcutView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DeepShortcutView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mSpacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+    }
+
+    public int getSpacing() {
+        return mSpacing;
+    }
+
+    /**
+     * Updates the state of this view based on touches over the container before user lifts finger.
+     *
+     * @param containerContainsTouch whether the {@link DeepShortcutsContainer} this shortcut
+     *                               is inside contains the current touch
+     * @param isBelowHoveredShortcut whether a sibling shortcut before this one in the
+     *                               view hierarchy is being hovered over
+     * @param touchY the y coordinate of the touch, relative to the {@link DeepShortcutsContainer}
+     *               this shortcut is inside
+     * @return whether this shortcut is being hovered over
+     */
+    public boolean updateHoverState(boolean containerContainsTouch, boolean isBelowHoveredShortcut,
+            float touchY) {
+        if (!containerContainsTouch) {
+            mIsHoveringOver = false;
+            mTranslationDirection = DIRECTION_NONE;
+        } else if (isBelowHoveredShortcut) {
+            mIsHoveringOver = false;
+            mTranslationDirection = DIRECTION_DOWN;
+        } else {
+            // Include space around the view when determining hover state to avoid gaps.
+            mTop = (int) (getY() - getTranslationY());
+            mIsHoveringOver = (touchY >= mTop - mSpacing / 2)
+                    && (touchY < mTop + getHeight() + mSpacing / 2);
+            mTranslationDirection = mIsHoveringOver ? DIRECTION_NONE : DIRECTION_UP;
+        }
+        animateHoverState();
+        return mIsHoveringOver;
+    }
+
+    /**
+     * If this shortcut is being hovered over, we scale it up. If another shortcut is being hovered
+     * over, we translate this one away from it to account for its increased size.
+     *
+     * TODO: apply motion spec here
+     */
+    private void animateHoverState() {
+        float scale = mIsHoveringOver ? HOVER_SCALE : 1f;
+        setScaleX(scale);
+        setScaleY(scale);
+
+        float translation = (HOVER_SCALE - 1f) * getHeight();
+        setTranslationY(translation * mTranslationDirection);
+    }
+
+    public boolean isHoveringOver() {
+        return mIsHoveringOver;
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
new file mode 100644
index 0000000..008b265
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
@@ -0,0 +1,409 @@
+package com.android.launcher3.shortcuts;
+
+import android.animation.Animator;
+import android.annotation.TargetApi;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.LogDecelerateInterpolator;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.compat.UserHandleCompat;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.logging.UserEventDispatcher;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.util.TouchController;
+import com.android.launcher3.util.UiThreadCircularReveal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A container for shortcuts to deep links within apps.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class DeepShortcutsContainer extends LinearLayout implements  View.OnClickListener,
+        View.OnLongClickListener, View.OnTouchListener, DragSource,
+        UserEventDispatcher.LaunchSourceProvider, TouchController {
+    private static final String TAG = "ShortcutsContainer";
+
+    private Launcher mLauncher;
+    private DeepShortcutManager mDeepShortcutsManager;
+    private final int mDragDeadzone;
+    private final int mStartDragThreshold;
+    private BubbleTextView mDeferredDragIcon;
+    private int mActivePointerId;
+    private Point mTouchDown = null;
+    private DragView mDragView;
+    private float mLastX, mLastY;
+    private float mDistanceDragged = 0;
+    private final Rect mTempRect = new Rect();
+    private final int[] mTempXY = new int[2];
+    private Point mIconLastTouchPos = new Point();
+    private boolean mIsLeftAligned;
+    private boolean mIsAboveIcon;
+
+    public DeepShortcutsContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = (Launcher) context;
+        mDeepShortcutsManager = LauncherAppState.getInstance().getShortcutManager();
+
+        mDragDeadzone = ViewConfiguration.get(context).getScaledTouchSlop();
+        mStartDragThreshold = getResources().getDimensionPixelSize(
+                R.dimen.deep_shortcuts_start_drag_threshold);
+    }
+
+    public DeepShortcutsContainer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DeepShortcutsContainer(Context context) {
+        this(context, null, 0);
+    }
+
+    public void populateAndShow(final BubbleTextView originalIcon, final List<String> ids) {
+        // Add dummy views first, and populate with real shortcut info when ready.
+        for (int i = 0; i < ids.size(); i++) {
+            final DeepShortcutView shortcut = (DeepShortcutView)
+                    mLauncher.getLayoutInflater().inflate(R.layout.deep_shortcut, this, false);
+            if (i < ids.size() - 1) {
+                ((LayoutParams) shortcut.getLayoutParams()).bottomMargin = shortcut.getSpacing();
+            }
+            addView(shortcut);
+        }
+
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        animateOpen(originalIcon);
+
+        deferDrag(originalIcon);
+
+        // Load the shortcuts on a background thread and update the container as it animates.
+        final ItemInfo originalInfo = (ItemInfo) originalIcon.getTag();
+        final UserHandleCompat user = originalInfo.user;
+        final ComponentName activity = originalInfo.getTargetComponent();
+        new AsyncTask<Void, Void, List<ShortcutInfo>>() {
+            public List<ShortcutInfo> doInBackground(Void ... args) {
+                List<ShortcutInfoCompat> shortcuts = mDeepShortcutsManager
+                        .queryForAllAppShortcuts(activity, ids, user);
+                List<ShortcutInfo> shortcutInfos = new ArrayList<>(shortcuts.size());
+                for (ShortcutInfoCompat shortcut : shortcuts) {
+                    shortcutInfos.add(ShortcutInfo.fromDeepShortcutInfo(shortcut, mLauncher));
+                }
+                return shortcutInfos;
+            }
+
+            // TODO: implement onProgressUpdate() to load shortcuts one at a time.
+
+            @Override
+            protected void onPostExecute(List<ShortcutInfo> shortcuts) {
+                for (int i = 0; i < shortcuts.size(); i++) {
+                    DeepShortcutView iconAndText = (DeepShortcutView) getChildAt(i);
+                    ShortcutInfo launcherShortcutInfo = shortcuts.get(i);
+                    iconAndText.applyFromShortcutInfo(launcherShortcutInfo,
+                            LauncherAppState.getInstance().getIconCache());
+                    iconAndText.setOnClickListener(DeepShortcutsContainer.this);
+                    iconAndText.setOnLongClickListener(DeepShortcutsContainer.this);
+                    iconAndText.setOnTouchListener(DeepShortcutsContainer.this);
+                    int viewId = mLauncher.getViewIdForItem(originalInfo);
+                    iconAndText.setId(viewId);
+                }
+            }
+        }.execute();
+    }
+
+    // TODO: update this animation
+    private void animateOpen(BubbleTextView originalIcon) {
+        orientAboutIcon(originalIcon);
+
+        setVisibility(View.VISIBLE);
+        int rx = (int) Math.max(Math.max(getMeasuredWidth() - getPivotX(), 0), getPivotX());
+        int ry = (int) Math.max(Math.max(getMeasuredHeight() - getPivotY(), 0), getPivotY());
+        float radius = (float) Math.hypot(rx, ry);
+        Animator reveal = UiThreadCircularReveal.createCircularReveal(this, (int) getPivotX(),
+                (int) getPivotY(), 0, radius);
+        reveal.setDuration(getResources().getInteger(R.integer.config_materialFolderExpandDuration));
+        reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
+        reveal.start();
+    }
+
+    /**
+     * Orients this container above or below the given icon, aligning with the left or right.
+     *
+     * These are the preferred orientations, in order:
+     * - Above and left-aligned
+     * - Above and right-aligned
+     * - Below and left-aligned
+     * - Below and right-aligned
+     *
+     * So we always align left if there is enough horizontal space
+     * and align above if there is enough vertical space.
+     *
+     * TODO: draw pointer based on orientation.
+     */
+    private void orientAboutIcon(BubbleTextView icon) {
+        int width = getMeasuredWidth();
+        int height = getMeasuredHeight();
+
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
+        // Align left and above by default.
+        int x = mTempRect.left + icon.getPaddingLeft();
+        int y = mTempRect.top - height;
+        Rect insets = dragLayer.getInsets();
+
+        mIsLeftAligned = x + width < dragLayer.getRight() - insets.right;
+        if (!mIsLeftAligned) {
+            x = mTempRect.right - width - icon.getPaddingRight();
+        }
+
+        mIsAboveIcon = mTempRect.top - height > dragLayer.getTop() + insets.top;
+        if (!mIsAboveIcon) {
+            y = mTempRect.bottom;
+        }
+
+        setPivotX(width / 2);
+        setPivotY(height / 2);
+
+        // Insets are added later, so subtract them now.
+        y -= insets.top;
+
+        setX(x);
+        setY(y);
+    }
+
+    private void deferDrag(BubbleTextView originalIcon) {
+        mDeferredDragIcon = originalIcon;
+        showDragView(originalIcon);
+    }
+
+    public BubbleTextView getDeferredDragIcon() {
+        return mDeferredDragIcon;
+    }
+
+    private void showDragView(BubbleTextView originalIcon) {
+        // TODO: implement support for Drawable DragViews so we don't have to create a bitmap here.
+        Bitmap b = Utilities.createIconBitmap(originalIcon.getIcon(), mLauncher);
+        float scale = mLauncher.getDragLayer().getLocationInDragLayer(originalIcon, mTempXY);
+        int dragLayerX = Math.round(mTempXY[0] - (b.getWidth() - scale * originalIcon.getWidth()) / 2);
+        int dragLayerY = Math.round(mTempXY[1] - (b.getHeight() - scale * b.getHeight()) / 2
+                - Workspace.DRAG_BITMAP_PADDING / 2) + originalIcon.getPaddingTop();
+        int motionDownX = mLauncher.getDragController().getMotionDown().x;
+        int motionDownY = mLauncher.getDragController().getMotionDown().y;
+        final int registrationX = motionDownX - dragLayerX;
+        final int registrationY = motionDownY - dragLayerY;
+
+        float scaleDps = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_drag_view_scale);
+        mDragView = new DragView(mLauncher, b, registrationX, registrationY,
+                0, 0, b.getWidth(), b.getHeight(), 1f, scaleDps);
+        mLastX = mLastY = mDistanceDragged = 0;
+        mDragView.show(motionDownX, motionDownY);
+    }
+
+    public boolean onForwardedEvent(MotionEvent ev, int activePointerId, MotionEvent touchDownEvent) {
+        mTouchDown = new Point((int) touchDownEvent.getX(), (int) touchDownEvent.getY());
+        mActivePointerId = activePointerId;
+        return dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mDeferredDragIcon == null) {
+            return false;
+        }
+
+
+        final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+        if (activePointerIndex < 0) {
+            return false;
+        }
+        final float x = ev.getX(activePointerIndex);
+        final float y = ev.getY(activePointerIndex);
+
+
+        int action = ev.getAction();
+        // The event was in this container's coordinate system before this,
+        // but will be in DragLayer's coordinate system from now on.
+        Utilities.translateEventCoordinates(this, mLauncher.getDragLayer(), ev);
+        final int dragLayerX = (int) ev.getX();
+        final int dragLayerY = (int) ev.getY();
+        int childCount = getChildCount();
+        if (action == MotionEvent.ACTION_MOVE) {
+            if (mLastX != 0 || mLastY != 0) {
+                mDistanceDragged += Math.hypot(mLastX - x, mLastY - y);
+            }
+            mLastX = x;
+            mLastY = y;
+
+            boolean containerContainsTouch = x >= 0 && y >= 0 && x < getWidth() && y < getHeight();
+            if (shouldStartDeferredDrag((int) x, (int) y, containerContainsTouch)) {
+                mDeferredDragIcon.getParent().requestDisallowInterceptTouchEvent(false);
+                mDeferredDragIcon.setVisibility(VISIBLE);
+                mDeferredDragIcon.getOnLongClickListener().onLongClick(mDeferredDragIcon);
+                mLauncher.getDragLayer().removeView(this);
+                mLauncher.getDragController().onTouchEvent(ev);
+                cleanupDeferredDrag();
+                return true;
+            } else {
+                // Determine whether touch is over a shortcut.
+                boolean hoveringOverShortcut = false;
+                for (int i = 0; i < childCount; i++) {
+                    DeepShortcutView shortcut = (DeepShortcutView) getChildAt(i);
+                    if (shortcut.updateHoverState(containerContainsTouch, hoveringOverShortcut, y)) {
+                        hoveringOverShortcut = true;
+                    }
+                }
+
+                if (!hoveringOverShortcut && mDistanceDragged > mDragDeadzone) {
+                    // After dragging further than a small deadzone,
+                    // have the drag view follow the user's finger.
+                    mDragView.setVisibility(VISIBLE);
+                    mDragView.move(dragLayerX, dragLayerY);
+                    mDeferredDragIcon.setVisibility(INVISIBLE);
+                } else if (hoveringOverShortcut) {
+                    // Jump drag view back to original place on grid,
+                    // so user doesn't think they are still dragging.
+                    // TODO: can we improve this interaction? maybe with a ghost icon or similar?
+                    mDragView.setVisibility(INVISIBLE);
+                    mDeferredDragIcon.setVisibility(VISIBLE);
+                }
+            }
+        } else if (action == MotionEvent.ACTION_UP) {
+            mDeferredDragIcon.setVisibility(VISIBLE);
+            cleanupDeferredDrag();
+            // Launch a shortcut if user was hovering over it.
+            for (int i = 0; i < childCount; i++) {
+                DeepShortcutView shortcut = (DeepShortcutView) getChildAt(i);
+                if (shortcut.isHoveringOver()) {
+                    shortcut.performClick();
+                    break;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Determines whether the deferred drag should be started based on touch coordinates
+     * relative to the original icon and the shortcuts container.
+     *
+     * Current behavior:
+     * - Compute distance from original touch down to closest container edge.
+     * - Compute distance from latest touch (given x and y) and compare to original distance;
+     *   if the new distance is larger than a threshold, the deferred drag should start.
+     * - Never defer the drag if this container contains the touch.
+     *
+     * @param x the x touch coordinate relative to this container
+     * @param y the y touch coordinate relative to this container
+     */
+    private boolean shouldStartDeferredDrag(int x, int y, boolean containerContainsTouch) {
+        Point closestEdge = new Point(mTouchDown.x, mIsAboveIcon ? getMeasuredHeight() : 0);
+        double distToEdge = Math.hypot(mTouchDown.x - closestEdge.x, mTouchDown.y - closestEdge.y);
+        double newDistToEdge = Math.hypot(x - closestEdge.x, y - closestEdge.y);
+        return  !containerContainsTouch && (newDistToEdge - distToEdge > mStartDragThreshold);
+    }
+
+    public void cleanupDeferredDrag() {
+        if (mDragView != null) {
+            mDragView.remove();
+        }
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        // Touched a shortcut, update where it was touched so we can drag from there on long click.
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public void onClick(View view) {
+        // Clicked on a shortcut.
+        mLauncher.onClick(view);
+        ((DragLayer) getParent()).removeView(this);
+    }
+
+    public boolean onLongClick(View v) {
+        // Return early if this is not initiated from a touch
+        if (!v.isInTouchMode()) return false;
+        // Return if global dragging is not enabled
+        if (!mLauncher.isDraggingEnabled()) return false;
+
+        // Long clicked on a shortcut.
+        // TODO remove this hack; it required because DragLayer isn't intercepting touch, so
+        // the controller is not updated from what it was previously.
+        mLauncher.getDragLayer().setController(mLauncher.getDragController());
+        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
+        ((DragLayer) getParent()).removeView(this);
+        return false;
+    }
+
+    @Override
+    public boolean supportsFlingToDelete() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsAppInfoDropTarget() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsDeleteDropTarget() {
+        return true;
+    }
+
+    @Override
+    public float getIntrinsicIconScaleFactor() {
+        return (float) getResources().getDimensionPixelSize(R.dimen.deep_shortcut_icon_size)
+                / mLauncher.getDeviceProfile().iconSizePx;
+    }
+
+    @Override
+    public void onFlingToDeleteCompleted() {
+        // Don't care; ignore.
+    }
+
+    @Override
+    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
+            boolean success) {
+        if (!success) {
+            d.dragView.remove();
+            mLauncher.showWorkspace(true);
+            mLauncher.getDropTargetBar().onDragEnd();
+        }
+    }
+
+    @Override
+    public void fillInLaunchSourceData(View v, ItemInfo info, Target target, Target targetParent) {
+        target.itemType = LauncherLogProto.SHORTCUT; // TODO: change to DYNAMIC_SHORTCUT
+        target.gridX = info.cellX;
+        target.gridY = info.cellY;
+        target.pageIndex = 0;
+        targetParent.containerType = LauncherLogProto.FOLDER; // TODO: change to DYNAMIC_SHORTCUTS
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/ShortcutCache.java b/src/com/android/launcher3/shortcuts/ShortcutCache.java
index fc118a8..d4db96d 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutCache.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutCache.java
@@ -56,6 +56,7 @@
         for (ShortcutInfoCompat shortcut : shortcuts) {
             ShortcutKey key = ShortcutKey.fromInfo(shortcut);
             mCachedShortcuts.remove(key);
+            mPinnedShortcuts.remove(key);
         }
     }
 
diff --git a/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
new file mode 100644
index 0000000..956623e
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
@@ -0,0 +1,289 @@
+package com.android.launcher3.shortcuts;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CheckLongPressHelper;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.dragndrop.DragLayer;
+
+import java.util.List;
+
+/**
+ * A {@link android.view.View.OnTouchListener} that creates a {@link DeepShortcutsContainer} and
+ * forwards touch events to it. This listener should be put on any icon that supports shortcuts.
+ */
+public class ShortcutsContainerListener implements View.OnTouchListener,
+        View.OnAttachStateChangeListener {
+
+    /** Scaled touch slop, used for detecting movement outside bounds. */
+    private final float mScaledTouchSlop;
+
+    /** Timeout before disallowing intercept on the source's parent. */
+    private final int mTapTimeout;
+
+    /** Timeout before accepting a long-press to start forwarding. */
+    private final int mLongPressTimeout;
+
+    /** Source view from which events are forwarded. */
+    private final BubbleTextView mSrcIcon;
+
+    /** Runnable used to prevent conflicts with scrolling parents. */
+    private Runnable mDisallowIntercept;
+
+    /** Runnable used to trigger forwarding on long-press. */
+    private Runnable mTriggerLongPress;
+
+    /** Whether this listener is currently forwarding touch events. */
+    private boolean mForwarding;
+
+    /** The id of the first pointer down in the current event stream. */
+    private int mActivePointerId;
+
+    private Launcher mLauncher;
+    private DragLayer mDragLayer;
+    private MotionEvent mTouchDownEvent;
+
+    public ShortcutsContainerListener(BubbleTextView icon) {
+        mSrcIcon = icon;
+        mScaledTouchSlop = ViewConfiguration.get(icon.getContext()).getScaledTouchSlop();
+        mTapTimeout = ViewConfiguration.getTapTimeout();
+
+        mLongPressTimeout = CheckLongPressHelper.DEFAULT_LONG_PRESS_TIMEOUT;
+
+        icon.addOnAttachStateChangeListener(this);
+
+        mLauncher = Launcher.getLauncher(mSrcIcon.getContext());
+
+        mDragLayer = mLauncher.getDragLayer();
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        if (mLauncher.getShortcutIdsForItem((ItemInfo) v.getTag()).isEmpty()) {
+            // There are no shortcuts associated with this item, so return to normal touch handling.
+            return false;
+        }
+
+        if (event.getAction() == MotionEvent.ACTION_DOWN) {
+            mTouchDownEvent = MotionEvent.obtainNoHistory(event);
+        }
+
+        final boolean wasForwarding = mForwarding;
+        final boolean forwarding;
+        if (wasForwarding) {
+            forwarding = onTouchForwarded(event) || !onForwardingStopped();
+        } else {
+            forwarding = onTouchObserved(event) && onForwardingStarted();
+
+            if (forwarding) {
+                // Make sure we cancel any ongoing source event stream.
+                final long now = SystemClock.uptimeMillis();
+                final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
+                        0.0f, 0.0f, 0);
+                mSrcIcon.onTouchEvent(e);
+                e.recycle();
+            }
+        }
+
+        mForwarding = forwarding;
+        return forwarding || wasForwarding;
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View v) {
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View v) {
+        mForwarding = false;
+        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+        if (mDisallowIntercept != null) {
+            mSrcIcon.removeCallbacks(mDisallowIntercept);
+        }
+    }
+
+    /**
+     * Called when forwarding would like to start.
+     * <p>
+     * This is when we populate the shortcuts container and add it to the DragLayer.
+     *
+     * @return true to start forwarding, false otherwise
+     */
+    protected boolean onForwardingStarted() {
+        List<String> ids = mLauncher.getShortcutIdsForItem((ItemInfo) mSrcIcon.getTag());
+        if (!ids.isEmpty()) {
+            // There are shortcuts associated with the app, so defer its drag.
+            LayoutInflater layoutInflater = (LayoutInflater) mLauncher.getSystemService
+                    (Context.LAYOUT_INFLATER_SERVICE);
+            final DeepShortcutsContainer deepShortcutsContainer = (DeepShortcutsContainer)
+                    layoutInflater.inflate(R.layout.deep_shortcuts_container, mDragLayer, false);
+            deepShortcutsContainer.setVisibility(View.INVISIBLE);
+            mDragLayer.addView(deepShortcutsContainer);
+            deepShortcutsContainer.populateAndShow(mSrcIcon, ids);
+            mSrcIcon.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
+                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Called when forwarding would like to stop.
+     *
+     * @return true to stop forwarding, false otherwise
+     */
+    protected boolean onForwardingStopped() {
+        return true;
+    }
+
+    /**
+     * Observes motion events and determines when to start forwarding.
+     *
+     * @param srcEvent motion event in source view coordinates
+     * @return true to start forwarding motion events, false otherwise
+     */
+    private boolean onTouchObserved(MotionEvent srcEvent) {
+        final View src = mSrcIcon;
+        if (!src.isEnabled()) {
+            return false;
+        }
+
+        final int actionMasked = srcEvent.getActionMasked();
+        switch (actionMasked) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = srcEvent.getPointerId(0);
+
+                if (mDisallowIntercept == null) {
+                    mDisallowIntercept = new DisallowIntercept();
+                }
+                src.postDelayed(mDisallowIntercept, mTapTimeout);
+
+                if (mTriggerLongPress == null) {
+                    mTriggerLongPress = new TriggerLongPress();
+                }
+                src.postDelayed(mTriggerLongPress, mLongPressTimeout);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
+                if (activePointerIndex >= 0) {
+                    final float x = srcEvent.getX(activePointerIndex);
+                    final float y = srcEvent.getY(activePointerIndex);
+
+                    // Has the pointer moved outside of the view?
+                    if (!Utilities.pointInView(src, x, y, mScaledTouchSlop)) {
+                        clearCallbacks();
+
+                        return false;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                clearCallbacks();
+                break;
+        }
+
+        return false;
+    }
+
+    private void clearCallbacks() {
+        if (mTriggerLongPress != null) {
+            mSrcIcon.removeCallbacks(mTriggerLongPress);
+        }
+
+        if (mDisallowIntercept != null) {
+            mSrcIcon.removeCallbacks(mDisallowIntercept);
+        }
+    }
+
+    private void onLongPress() {
+        clearCallbacks();
+
+        final View src = mSrcIcon;
+        if (!src.isEnabled() || mLauncher.getShortcutIdsForItem((ItemInfo) src.getTag()).isEmpty()) {
+            // Ignore long-press if the view is disabled or doesn't have shortcuts.
+            return;
+        }
+
+        if (!onForwardingStarted()) {
+            return;
+        }
+
+        // Don't let the parent intercept our events.
+        src.getParent().requestDisallowInterceptTouchEvent(true);
+
+        // Make sure we cancel any ongoing source event stream.
+        final long now = SystemClock.uptimeMillis();
+        final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        src.onTouchEvent(e);
+        e.recycle();
+
+        mForwarding = true;
+    }
+
+    /**
+     * Handles forwarded motion events and determines when to stop
+     * forwarding.
+     *
+     * @param srcEvent motion event in source view coordinates
+     * @return true to continue forwarding motion events, false to cancel
+     */
+    private boolean onTouchForwarded(MotionEvent srcEvent) {
+        final View src = mSrcIcon;
+
+        final DeepShortcutsContainer dst = (DeepShortcutsContainer)
+                mDragLayer.findViewById(R.id.deep_shortcuts_container);
+        if (dst == null) {
+            return false;
+        }
+
+        // Convert event to destination-local coordinates.
+        final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
+        Utilities.translateEventCoordinates(src, dst, dstEvent);
+
+        // Convert touch down event to destination-local coordinates.
+        // TODO: only create this once, or just store the x and y.
+        final MotionEvent touchDownEvent = MotionEvent.obtainNoHistory(mTouchDownEvent);
+        Utilities.translateEventCoordinates(src, dst, touchDownEvent);
+
+        // Forward converted event to destination view, then recycle it.
+        // TODO: don't create objects in onForwardedEvent.
+        final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId, touchDownEvent);
+        dstEvent.recycle();
+        touchDownEvent.recycle();
+
+        // Always cancel forwarding when the touch stream ends.
+        final int action = srcEvent.getActionMasked();
+        final boolean keepForwarding = action != MotionEvent.ACTION_UP
+                && action != MotionEvent.ACTION_CANCEL;
+
+        return handled && keepForwarding;
+    }
+
+    private class DisallowIntercept implements Runnable {
+        @Override
+        public void run() {
+            final ViewParent parent = mSrcIcon.getParent();
+            parent.requestDisallowInterceptTouchEvent(true);
+        }
+    }
+
+    private class TriggerLongPress implements Runnable {
+        @Override
+        public void run() {
+            onLongPress();
+        }
+    }
+}