Refactor DeepShortcutsContainer to PopupContainerWithArrow

- Also added PopupItemView, which takes animation logic from
  DeepShortcutView, and which DeepShortcutView now extends.
- Renamed ShortcutFilter to PopupPopulator, which has support
  for new item types (not yet used). Also moved populating
  logic (e.g. UpdateShortcutChild Runnable) to PopupPopulator.

Bug: 32410600
Change-Id: Ib6e444ac7ca99c80ba438801c26e62d9542e0ad9
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 65da002..c834c6b 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -32,11 +32,11 @@
  */
 public abstract class AbstractFloatingView extends LinearLayout {
 
-    @IntDef(flag = true, value = {TYPE_FOLDER, TYPE_DEEPSHORTCUT_CONTAINER})
+    @IntDef(flag = true, value = {TYPE_FOLDER, TYPE_POPUP_CONTAINER_WITH_ARROW})
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
     public static final int TYPE_FOLDER = 1 << 0;
-    public static final int TYPE_DEEPSHORTCUT_CONTAINER = 1 << 1;
+    public static final int TYPE_POPUP_CONTAINER_WITH_ARROW = 1 << 1;
 
     protected boolean mIsOpen;
 
@@ -119,6 +119,6 @@
     }
 
     public static AbstractFloatingView getTopOpenView(Launcher launcher) {
-        return getOpenView(launcher, TYPE_FOLDER | TYPE_DEEPSHORTCUT_CONTAINER);
+        return getOpenView(launcher, TYPE_FOLDER | TYPE_POPUP_CONTAINER_WITH_ARROW);
     }
 }
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 47a5b4f..ed8b531 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -41,14 +41,12 @@
 import com.android.launcher3.IconCache.ItemInfoUpdateReceiver;
 import com.android.launcher3.badge.BadgeInfo;
 import com.android.launcher3.badge.BadgeRenderer;
-import com.android.launcher3.badging.NotificationInfo;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.HolographicOutlineHelper;
 import com.android.launcher3.model.PackageItemInfo;
 
 import java.text.NumberFormat;
-import java.util.List;
 
 /**
  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 26e388d..e2108a7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -85,7 +85,7 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DefaultAppSearchController;
 import com.android.launcher3.anim.AnimationLayerSet;
-import com.android.launcher3.badging.NotificationListener;
+import com.android.launcher3.badge.NotificationListener;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
@@ -109,8 +109,8 @@
 import com.android.launcher3.model.PackageItemInfo;
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
-import com.android.launcher3.shortcuts.DeepShortcutsContainer;
 import com.android.launcher3.shortcuts.ShortcutInfoCompat;
 import com.android.launcher3.shortcuts.ShortcutKey;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
@@ -1738,7 +1738,7 @@
             mWorkspace.exitWidgetResizeMode();
 
             AbstractFloatingView topOpenView = AbstractFloatingView.getTopOpenView(this);
-            if (topOpenView instanceof DeepShortcutsContainer) {
+            if (topOpenView instanceof PopupContainerWithArrow) {
                 ued.logActionCommand(Action.Command.HOME_INTENT,
                         topOpenView.getExtendedTouchView(), ContainerType.DEEPSHORTCUTS);
             } else if (topOpenView instanceof Folder) {
@@ -2256,7 +2256,7 @@
             if (topView.getActiveTextView() != null) {
                 topView.getActiveTextView().dispatchBackKey();
             } else {
-                if (topView instanceof DeepShortcutsContainer) {
+                if (topView instanceof PopupContainerWithArrow) {
                     ued.logActionCommand(Action.Command.BACK,
                             topView.getExtendedTouchView(), ContainerType.DEEPSHORTCUTS);
                 } else if (topView instanceof Folder) {
@@ -4077,7 +4077,7 @@
                             && mAccessibilityDelegate.performAction(focusedView,
                                     (ItemInfo) focusedView.getTag(),
                                     LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
-                        DeepShortcutsContainer.getOpen(this).requestFocus();
+                        PopupContainerWithArrow.getOpen(this).requestFocus();
                         return true;
                     }
                     break;
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index e646dd9..cd1d8d9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -72,7 +72,7 @@
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.DragPreviewProvider;
-import com.android.launcher3.shortcuts.DeepShortcutsContainer;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ItemInfoMatcher;
@@ -2267,9 +2267,10 @@
         }
 
         if (child instanceof BubbleTextView) {
-            DeepShortcutsContainer dsc = DeepShortcutsContainer.showForIcon((BubbleTextView) child);
-            if (dsc != null) {
-                dragOptions.preDragCondition = dsc.createPreDragCondition();
+            PopupContainerWithArrow popupContainer = PopupContainerWithArrow
+                    .showForIcon((BubbleTextView) child);
+            if (popupContainer != null) {
+                dragOptions.preDragCondition = popupContainer.createPreDragCondition();
 
                 mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
             }
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 9347852..b77493b 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -18,6 +18,7 @@
 import com.android.launcher3.AppWidgetResizeFrame;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.DeleteDropTarget;
 import com.android.launcher3.DropTarget.DragObject;
 import com.android.launcher3.FolderInfo;
@@ -36,7 +37,6 @@
 import com.android.launcher3.dragndrop.DragController.DragListener;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.shortcuts.DeepShortcutsContainer;
 import com.android.launcher3.util.Thunk;
 
 import java.util.ArrayList;
@@ -231,7 +231,7 @@
                 .show();
             return true;
         } else if (action == DEEP_SHORTCUTS) {
-            return DeepShortcutsContainer.showForIcon((BubbleTextView) host) != null;
+            return PopupContainerWithArrow.showForIcon((BubbleTextView) host) != null;
         }
         return false;
     }
diff --git a/src/com/android/launcher3/badge/BadgeInfo.java b/src/com/android/launcher3/badge/BadgeInfo.java
index 98d2277..4255c51 100644
--- a/src/com/android/launcher3/badge/BadgeInfo.java
+++ b/src/com/android/launcher3/badge/BadgeInfo.java
@@ -30,7 +30,7 @@
     private PackageUserKey mPackageUserKey;
     /**
      * The keys of the notifications that this badge represents. These keys can later be
-     * used to retrieve {@link com.android.launcher3.badging.NotificationInfo}'s.
+     * used to retrieve {@link NotificationInfo}'s.
      */
     private Set<String> mNotificationKeys;
 
diff --git a/src/com/android/launcher3/badging/NotificationInfo.java b/src/com/android/launcher3/badge/NotificationInfo.java
similarity index 94%
rename from src/com/android/launcher3/badging/NotificationInfo.java
rename to src/com/android/launcher3/badge/NotificationInfo.java
index 2590add..51f6a4f 100644
--- a/src/com/android/launcher3/badging/NotificationInfo.java
+++ b/src/com/android/launcher3/badge/NotificationInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.badging;
+package com.android.launcher3.badge;
 
 import android.app.Notification;
 import android.app.PendingIntent;
@@ -25,7 +25,7 @@
 import android.view.View;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.shortcuts.DeepShortcutsContainer;
+import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.util.PackageUserKey;
 
 /**
@@ -77,6 +77,6 @@
         if (autoCancel) {
             launcher.getPopupDataProvider().cancelNotification(notificationKey);
         }
-        DeepShortcutsContainer.getOpen(launcher).close(true);
+        PopupContainerWithArrow.getOpen(launcher).close(true);
     }
 }
diff --git a/src/com/android/launcher3/badging/NotificationListener.java b/src/com/android/launcher3/badge/NotificationListener.java
similarity index 99%
rename from src/com/android/launcher3/badging/NotificationListener.java
rename to src/com/android/launcher3/badge/NotificationListener.java
index 0a85d56..1668a62 100644
--- a/src/com/android/launcher3/badging/NotificationListener.java
+++ b/src/com/android/launcher3/badge/NotificationListener.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.badging;
+package com.android.launcher3.badge;
 
 import android.app.Notification;
 import android.os.Handler;
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
index 6603e93..bb0b58a 100644
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
@@ -24,10 +24,10 @@
 import android.widget.PopupMenu;
 import android.widget.PopupMenu.OnMenuItemClickListener;
 
+import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.shortcuts.DeepShortcutsContainer;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -46,7 +46,7 @@
     public CustomActionsPopup(Launcher launcher, View icon) {
         mLauncher = launcher;
         mIcon = icon;
-        DeepShortcutsContainer container = DeepShortcutsContainer.getOpen(launcher);
+        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
         if (container != null) {
             mDelegate = container.getAccessibilityDelegate();
         } else {
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
similarity index 78%
rename from src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
rename to src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 5e12a57..95d51dc 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.shortcuts;
+package com.android.launcher3.popup;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -22,8 +22,8 @@
 import android.animation.TimeInterpolator;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
-import android.content.ComponentName;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Point;
@@ -33,7 +33,6 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.UserHandle;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -43,7 +42,6 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.FrameLayout;
-import android.widget.LinearLayout;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
@@ -57,69 +55,101 @@
 import com.android.launcher3.LauncherViewPropertyAnimator;
 import com.android.launcher3.LogAccelerateInterpolator;
 import com.android.launcher3.R;
-import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
+import com.android.launcher3.badge.BadgeInfo;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.graphics.LauncherIcons;
+import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.TriangleShape;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
-import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.util.PackageUserKey;
 
-import java.util.Collections;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 
+import static com.android.launcher3.userevent.nano.LauncherLogProto.*;
+
 /**
  * A container for shortcuts to deep links within apps.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class DeepShortcutsContainer extends AbstractFloatingView
-        implements View.OnLongClickListener,
-        View.OnTouchListener, DragSource, DragController.DragListener {
+public class PopupContainerWithArrow extends AbstractFloatingView
+        implements View.OnLongClickListener, View.OnTouchListener, DragSource,
+        DragController.DragListener {
 
     private final Point mIconShift = new Point();
+    private final Point mIconLastTouchPos = new Point();
 
-    private final Launcher mLauncher;
-    private final DeepShortcutManager mDeepShortcutsManager;
+    protected final Launcher mLauncher;
     private final int mStartDragThreshold;
-    private final ShortcutMenuAccessibilityDelegate mAccessibilityDelegate;
+    private LauncherAccessibilityDelegate mAccessibilityDelegate;
     private final boolean mIsRtl;
 
-    private BubbleTextView mOriginalIcon;
+    protected BubbleTextView mOriginalIcon;
     private final Rect mTempRect = new Rect();
     private PointF mInterceptTouchDown = new PointF();
-    private Point mIconLastTouchPos = new Point();
     private boolean mIsLeftAligned;
-    private boolean mIsAboveIcon;
+    protected boolean mIsAboveIcon;
     private View mArrow;
 
-    private Animator mOpenCloseAnimator;
+    protected Animator mOpenCloseAnimator;
     private boolean mDeferContainerRemoval;
 
-    public DeepShortcutsContainer(Context context, AttributeSet attrs, int defStyleAttr) {
+    public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         mLauncher = Launcher.getLauncher(context);
-        mDeepShortcutsManager = DeepShortcutManager.getInstance(context);
 
         mStartDragThreshold = getResources().getDimensionPixelSize(
                 R.dimen.deep_shortcuts_start_drag_threshold);
+        // TODO: make sure the delegate works for all items, not just shortcuts.
         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
         mIsRtl = Utilities.isRtl(getResources());
     }
 
-    public DeepShortcutsContainer(Context context, AttributeSet attrs) {
+    public PopupContainerWithArrow(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
-    public DeepShortcutsContainer(Context context) {
+    public PopupContainerWithArrow(Context context) {
         this(context, null, 0);
     }
 
-    public void populateAndShow(final BubbleTextView originalIcon, final List<String> ids) {
+    public LauncherAccessibilityDelegate getAccessibilityDelegate() {
+        return mAccessibilityDelegate;
+    }
+
+    /**
+     * Shows the notifications and deep shortcuts associated with {@param icon}.
+     * @return the container if shown or null.
+     */
+    public static PopupContainerWithArrow showForIcon(BubbleTextView icon) {
+        Launcher launcher = Launcher.getLauncher(icon.getContext());
+        if (getOpen(launcher) != null) {
+            // There is already an items container open, so don't open this one.
+            icon.clearFocus();
+            return null;
+        }
+        ItemInfo itemInfo = (ItemInfo) icon.getTag();
+        List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
+        if (shortcutIds.size() > 0) {
+            final PopupContainerWithArrow container =
+                    (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
+                            R.layout.popup_container, launcher.getDragLayer(), false);
+            container.setVisibility(View.INVISIBLE);
+            launcher.getDragLayer().addView(container);
+            container.populateAndShow(icon, shortcutIds);
+            return container;
+        }
+        return null;
+    }
+
+    public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds) {
         final Resources resources = getResources();
         final int arrowWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_width);
         final int arrowHeight = resources.getDimensionPixelSize(R.dimen.deep_shortcuts_arrow_height);
@@ -128,25 +158,37 @@
         final int arrowVerticalOffset = resources.getDimensionPixelSize(
                 R.dimen.deep_shortcuts_arrow_vertical_offset);
 
-        // Add dummy views first, and populate with real shortcut info when ready.
-        final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
-        final LayoutInflater inflater = mLauncher.getLayoutInflater();
-        int numShortcuts = Math.min(ids.size(), ShortcutFilter.MAX_SHORTCUTS);
-        for (int i = 0; i < numShortcuts; i++) {
-            final DeepShortcutView shortcut =
-                    (DeepShortcutView) inflater.inflate(R.layout.deep_shortcut, this, false);
-            if (i < numShortcuts - 1) {
-                ((LayoutParams) shortcut.getLayoutParams()).bottomMargin = spacing;
-            }
-            shortcut.getBubbleText().setAccessibilityDelegate(mAccessibilityDelegate);
-            addView(shortcut);
-        }
-        setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
-                numShortcuts, originalIcon.getContentDescription().toString()));
+        // Add dummy views first, and populate with real info when ready.
+        PopupPopulator.Item[] itemsToPopulate = PopupPopulator.getItemsToPopulate(shortcutIds);
+        addDummyViews(originalIcon, itemsToPopulate);
 
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
         orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
 
+        boolean reverseOrder = mIsAboveIcon;
+        if (reverseOrder) {
+            removeAllViews();
+            itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
+            addDummyViews(originalIcon, itemsToPopulate);
+
+            measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+            orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
+        }
+
+        List<DeepShortcutView> shortcutViews = new ArrayList<>();
+        for (int i = 0; i < getChildCount(); i++) {
+            View item = getChildAt(i);
+            switch (itemsToPopulate[i]) {
+                case SHORTCUT:
+                    if (reverseOrder) {
+                        shortcutViews.add(0, (DeepShortcutView) item);
+                    } else {
+                        shortcutViews.add((DeepShortcutView) item);
+                    }
+                    break;
+            }
+        }
+
         // Add the arrow.
         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
         mArrow.setPivotX(arrowWidth / 2);
@@ -155,66 +197,44 @@
         animateOpen();
 
         mOriginalIcon = originalIcon;
+
         mLauncher.getDragController().addDragListener(this);
 
         // Load the shortcuts on a background thread and update the container as it animates.
         final Looper workerLooper = LauncherModel.getWorkerLooper();
-        final Handler uiHandler = new Handler(Looper.getMainLooper());
-        final ItemInfo originalInfo = (ItemInfo) originalIcon.getTag();
-        final UserHandle user = originalInfo.user;
-        final ComponentName activity = originalInfo.getTargetComponent();
-        new Handler(workerLooper).postAtFrontOfQueue(new Runnable() {
-            @Override
-            public void run() {
-                final List<ShortcutInfoCompat> shortcuts = ShortcutFilter.sortAndFilterShortcuts(
-                        mDeepShortcutsManager.queryForShortcutsContainer(activity, ids, user));
-                // We want the lowest rank to be closest to the user's finger.
-                if (mIsAboveIcon) {
-                    Collections.reverse(shortcuts);
-                }
-                for (int i = 0; i < shortcuts.size(); i++) {
-                    final ShortcutInfoCompat shortcut = shortcuts.get(i);
-                    ShortcutInfo si = new ShortcutInfo(shortcut, mLauncher);
-                    // Use unbadged icon for the menu.
-                    si.iconBitmap = LauncherIcons.createShortcutIcon(
-                            shortcut, mLauncher, false /* badged */);
-                    uiHandler.post(new UpdateShortcutChild(i, si, shortcut));
-                }
+        new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
+                mLauncher, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()),
+                this, shortcutIds, shortcutViews));
+    }
+
+    private void addDummyViews(BubbleTextView originalIcon, PopupPopulator.Item[] itemsToPopulate) {
+        final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+        final LayoutInflater inflater = mLauncher.getLayoutInflater();
+        int numItems = itemsToPopulate.length;
+        for (int i = 0; i < numItems; i++) {
+            final PopupItemView item = (PopupItemView) inflater.inflate(
+                    itemsToPopulate[i].layoutId, this, false);
+            if (i < numItems - 1) {
+                ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
             }
-        });
+            item.setAccessibilityDelegate(mAccessibilityDelegate);
+            addView(item);
+        }
+        // TODO: update this, since not all items are shortcuts
+        setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
+                numItems, originalIcon.getContentDescription().toString()));
     }
 
-    /** Updates the child of this container at the given index based on the given shortcut info. */
-    private class UpdateShortcutChild implements Runnable {
-        private final int mShortcutChildIndex;
-        private final ShortcutInfo mShortcutChildInfo;
-        private final ShortcutInfoCompat mDetail;
-
-
-        public UpdateShortcutChild(int shortcutChildIndex, ShortcutInfo shortcutChildInfo,
-                ShortcutInfoCompat detail) {
-            mShortcutChildIndex = shortcutChildIndex;
-            mShortcutChildInfo = shortcutChildInfo;
-            mDetail = detail;
-        }
-
-        @Override
-        public void run() {
-            getShortcutAt(mShortcutChildIndex)
-                    .applyShortcutInfo(mShortcutChildInfo, mDetail, DeepShortcutsContainer.this);
-        }
-    }
-
-    private DeepShortcutView getShortcutAt(int index) {
+    protected PopupItemView getItemViewAt(int index) {
         if (!mIsAboveIcon) {
             // Opening down, so arrow is the first view.
             index++;
         }
-        return (DeepShortcutView) getChildAt(index);
+        return (PopupItemView) getChildAt(index);
     }
 
-    private int getShortcutCount() {
-        // All children except the arrow are shortcuts.
+    protected int getItemCount() {
+        // All children except the arrow are items.
         return getChildCount() - 1;
     }
 
@@ -223,7 +243,7 @@
         mIsOpen = true;
 
         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
-        final int shortcutCount = getShortcutCount();
+        final int itemCount = getItemCount();
 
         final long duration = getResources().getInteger(
                 R.integer.config_deepShortcutOpenDuration);
@@ -236,25 +256,25 @@
 
         // Animate shortcuts
         DecelerateInterpolator interpolator = new DecelerateInterpolator();
-        for (int i = 0; i < shortcutCount; i++) {
-            final DeepShortcutView deepShortcutView = getShortcutAt(i);
-            deepShortcutView.setVisibility(INVISIBLE);
-            deepShortcutView.setAlpha(0);
+        for (int i = 0; i < itemCount; i++) {
+            final PopupItemView popupItemView = getItemViewAt(i);
+            popupItemView.setVisibility(INVISIBLE);
+            popupItemView.setAlpha(0);
 
-            Animator anim = deepShortcutView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
+            Animator anim = popupItemView.createOpenAnimation(mIsAboveIcon, mIsLeftAligned);
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationStart(Animator animation) {
-                    deepShortcutView.setVisibility(VISIBLE);
+                    popupItemView.setVisibility(VISIBLE);
                 }
             });
             anim.setDuration(duration);
-            int animationIndex = mIsAboveIcon ? shortcutCount - i - 1 : i;
+            int animationIndex = mIsAboveIcon ? itemCount - i - 1 : i;
             anim.setStartDelay(stagger * animationIndex);
             anim.setInterpolator(interpolator);
             shortcutAnims.play(anim);
 
-            Animator fadeAnim = new LauncherViewPropertyAnimator(deepShortcutView).alpha(1);
+            Animator fadeAnim = new LauncherViewPropertyAnimator(popupItemView).alpha(1);
             fadeAnim.setInterpolator(fadeInterpolator);
             // We want the shortcut to be fully opaque before the arrow starts animating.
             fadeAnim.setDuration(arrowScaleDelay);
@@ -265,7 +285,7 @@
             public void onAnimationEnd(Animator animation) {
                 mOpenCloseAnimator = null;
                 Utilities.sendCustomAccessibilityEvent(
-                        DeepShortcutsContainer.this,
+                        PopupContainerWithArrow.this,
                         AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
                         getContext().getString(R.string.action_deep_shortcut));
             }
@@ -405,7 +425,7 @@
      *                              points at the center of the original icon
      */
     private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
-        LinearLayout.LayoutParams layoutParams = new LayoutParams(width, height);
+        LayoutParams layoutParams = new LayoutParams(width, height);
         if (mIsLeftAligned) {
             layoutParams.gravity = Gravity.LEFT;
             layoutParams.leftMargin = horizontalOffset;
@@ -593,7 +613,7 @@
         }
     }
 
-    private void animateClose() {
+    protected void animateClose() {
         if (!mIsOpen) {
             return;
         }
@@ -603,10 +623,10 @@
         mIsOpen = false;
 
         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
-        final int shortcutCount = getShortcutCount();
+        final int itemCount = getItemCount();
         int numOpenShortcuts = 0;
-        for (int i = 0; i < shortcutCount; i++) {
-            if (getShortcutAt(i).isOpenOrOpening()) {
+        for (int i = 0; i < itemCount; i++) {
+            if (getItemViewAt(i).isOpenOrOpening()) {
                 numOpenShortcuts++;
             }
         }
@@ -618,13 +638,13 @@
                 R.integer.config_deepShortcutCloseStagger);
         final TimeInterpolator fadeInterpolator = new LogAccelerateInterpolator(100, 0);
 
-        int firstOpenShortcutIndex = mIsAboveIcon ? shortcutCount - numOpenShortcuts : 0;
-        for (int i = firstOpenShortcutIndex; i < firstOpenShortcutIndex + numOpenShortcuts; i++) {
-            final DeepShortcutView view = getShortcutAt(i);
+        int firstOpenItemIndex = mIsAboveIcon ? itemCount - numOpenShortcuts : 0;
+        for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
+            final PopupItemView view = getItemViewAt(i);
             Animator anim;
             if (view.willDrawIcon()) {
                 anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
-                int animationIndex = mIsAboveIcon ? i - firstOpenShortcutIndex
+                int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
                         : numOpenShortcuts - i - 1;
                 anim.setStartDelay(stagger * animationIndex);
 
@@ -681,14 +701,10 @@
         shortcutAnims.start();
     }
 
-    public ShortcutMenuAccessibilityDelegate getAccessibilityDelegate() {
-        return mAccessibilityDelegate;
-    }
-
     /**
      * Closes the folder without animation.
      */
-    private void closeComplete() {
+    protected void closeComplete() {
         if (mOpenCloseAnimator != null) {
             mOpenCloseAnimator.cancel();
             mOpenCloseAnimator = null;
@@ -704,38 +720,13 @@
 
     @Override
     protected boolean isOfType(int type) {
-        return (type & TYPE_DEEPSHORTCUT_CONTAINER) != 0;
-    }
-
-    /**
-     * Shows the shortcuts container for {@param icon}
-     * @return the container if shown or null.
-     */
-    public static DeepShortcutsContainer showForIcon(BubbleTextView icon) {
-        Launcher launcher = Launcher.getLauncher(icon.getContext());
-        if (getOpen(launcher) != null) {
-            // There is already a shortcuts container open, so don't open this one.
-            icon.clearFocus();
-            return null;
-        }
-        List<String> ids = launcher.getPopupDataProvider().getShortcutIdsForItem(
-                (ItemInfo) icon.getTag());
-        if (!ids.isEmpty()) {
-            final DeepShortcutsContainer container =
-                    (DeepShortcutsContainer) launcher.getLayoutInflater().inflate(
-                            R.layout.deep_shortcuts_container, launcher.getDragLayer(), false);
-            container.setVisibility(View.INVISIBLE);
-            launcher.getDragLayer().addView(container);
-            container.populateAndShow(icon, ids);
-            return container;
-        }
-        return null;
+        return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
     }
 
     /**
      * Returns a DeepShortcutsContainer which is already open or null
      */
-    public static DeepShortcutsContainer getOpen(Launcher launcher) {
-        return getOpenView(launcher, TYPE_DEEPSHORTCUT_CONTAINER);
+    public static PopupContainerWithArrow getOpen(Launcher launcher) {
+        return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
     }
 }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index 4ed32b5..b671c36 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -23,7 +23,7 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.badge.BadgeInfo;
-import com.android.launcher3.badging.NotificationListener;
+import com.android.launcher3.badge.NotificationListener;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.MultiHashMap;
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
new file mode 100644
index 0000000..25d496a
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupItemView.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.popup;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.LogAccelerateInterpolator;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.PillRevealOutlineProvider;
+import com.android.launcher3.util.PillWidthRevealOutlineProvider;
+
+/**
+ * An abstract {@link FrameLayout} that supports animating an item's content
+ * (e.g. icon and text) separate from the item's background.
+ */
+public abstract class PopupItemView extends FrameLayout
+        implements ValueAnimator.AnimatorUpdateListener {
+
+    protected static final Point sTempPoint = new Point();
+
+    private final Rect mPillRect;
+    private float mOpenAnimationProgress;
+
+    protected View mIconView;
+
+    public PopupItemView(Context context) {
+        this(context, null, 0);
+    }
+
+    public PopupItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PopupItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mPillRect = new Rect();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mIconView = findViewById(R.id.popup_item_icon);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
+    }
+
+    public boolean willDrawIcon() {
+        return true;
+    }
+
+    /**
+     * Creates an animator to play when the shortcut container is being opened.
+     */
+    public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) {
+        Point center = getIconCenter();
+        ValueAnimator openAnimator =  new ZoomRevealOutlineProvider(center.x, center.y,
+                mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft)
+                        .createRevealAnimator(this, false);
+        mOpenAnimationProgress = 0f;
+        openAnimator.addUpdateListener(this);
+        return openAnimator;
+    }
+
+    @Override
+    public void onAnimationUpdate(ValueAnimator valueAnimator) {
+        mOpenAnimationProgress = valueAnimator.getAnimatedFraction();
+    }
+
+    public boolean isOpenOrOpening() {
+        return mOpenAnimationProgress > 0;
+    }
+
+    /**
+     * Creates an animator to play when the shortcut container is being closed.
+     */
+    public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft,
+            long duration) {
+        Point center = getIconCenter();
+        ValueAnimator closeAnimator = new ZoomRevealOutlineProvider(center.x, center.y,
+                mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft)
+                        .createRevealAnimator(this, true);
+        // Scale down the duration and interpolator according to the progress
+        // that the open animation was at when the close started.
+        closeAnimator.setDuration((long) (duration * mOpenAnimationProgress));
+        closeAnimator.setInterpolator(new CloseInterpolator(mOpenAnimationProgress));
+        closeAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenAnimationProgress = 0;
+            }
+        });
+        return closeAnimator;
+    }
+
+    /**
+     * Creates an animator which clips the container to form a circle around the icon.
+     */
+    public Animator collapseToIcon() {
+        int halfHeight = getMeasuredHeight() / 2;
+        int iconCenterX = getIconCenter().x;
+        return new PillWidthRevealOutlineProvider(mPillRect,
+                iconCenterX - halfHeight, iconCenterX + halfHeight)
+                        .createRevealAnimator(this, true);
+    }
+
+    /**
+     * Returns the position of the center of the icon relative to the container.
+     */
+    public Point getIconCenter() {
+        sTempPoint.y = sTempPoint.x = getMeasuredHeight() / 2;
+        if (Utilities.isRtl(getResources())) {
+            sTempPoint.x = getMeasuredWidth() - sTempPoint.x;
+        }
+        return sTempPoint;
+    }
+
+    /**
+     * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height.
+     */
+    private static class ZoomRevealOutlineProvider extends PillRevealOutlineProvider {
+
+        private final View mTranslateView;
+        private final View mZoomView;
+
+        private final float mFullHeight;
+        private final float mTranslateYMultiplier;
+
+        private final boolean mPivotLeft;
+        private final float mTranslateX;
+
+        public ZoomRevealOutlineProvider(int x, int y, Rect pillRect,
+                View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) {
+            super(x, y, pillRect);
+            mTranslateView = translateView;
+            mZoomView = zoomView;
+            mFullHeight = pillRect.height();
+
+            mTranslateYMultiplier = isContainerAboveIcon ? 0.5f : -0.5f;
+
+            mPivotLeft = pivotLeft;
+            mTranslateX = pivotLeft ? pillRect.height() / 2 : pillRect.right - pillRect.height() / 2;
+        }
+
+        @Override
+        public void setProgress(float progress) {
+            super.setProgress(progress);
+
+            mZoomView.setScaleX(progress);
+            mZoomView.setScaleY(progress);
+
+            float height = mOutline.height();
+            mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height));
+
+            float pivotX = mPivotLeft ? (mOutline.left + height / 2) : (mOutline.right - height / 2);
+            mTranslateView.setTranslationX(mTranslateX - pivotX);
+        }
+    }
+
+    /**
+     * An interpolator that reverses the current open animation progress.
+     */
+    private static class CloseInterpolator extends LogAccelerateInterpolator {
+        private float mStartProgress;
+        private float mRemainingProgress;
+
+        /**
+         * @param openAnimationProgress The progress that the open interpolator ended at.
+         */
+        public CloseInterpolator(float openAnimationProgress) {
+            super(100, 0);
+            mStartProgress = 1f - openAnimationProgress;
+            mRemainingProgress = openAnimationProgress;
+        }
+
+        @Override
+        public float getInterpolation(float v) {
+            return mStartProgress + super.getInterpolation(v) * mRemainingProgress;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
new file mode 100644
index 0000000..b5a59b0
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.popup;
+
+import android.content.ComponentName;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.graphics.LauncherIcons;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutInfoCompat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Contains logic relevant to populating a {@link PopupContainerWithArrow}. In particular,
+ * this class determines which items appear in the container, and in what order.
+ */
+public class PopupPopulator {
+
+    public static final int MAX_ITEMS = 4;
+    @VisibleForTesting static final int NUM_DYNAMIC = 2;
+
+    public enum Item {
+        SHORTCUT(R.layout.deep_shortcut);
+
+        public final int layoutId;
+
+        Item(int layoutId) {
+            this.layoutId = layoutId;
+        }
+    }
+
+    public static Item[] getItemsToPopulate(List<String> shortcutIds) {
+        int numItems = Math.min(MAX_ITEMS, shortcutIds.size());
+        Item[] items = new Item[numItems];
+        for (int i = 0; i < numItems; i++) {
+            items[i] = Item.SHORTCUT;
+        }
+        return items;
+    }
+
+    public static Item[] reverseItems(Item[] items) {
+        if (items == null) return null;
+        int numItems = items.length;
+        Item[] reversedArray = new Item[numItems];
+        for (int i = 0; i < numItems; i++) {
+            reversedArray[i] = items[numItems - i - 1];
+        }
+        return reversedArray;
+    }
+
+    /**
+     * Sorts shortcuts in rank order, with manifest shortcuts coming before dynamic shortcuts.
+     */
+    private static final Comparator<ShortcutInfoCompat> SHORTCUT_RANK_COMPARATOR
+            = new Comparator<ShortcutInfoCompat>() {
+        @Override
+        public int compare(ShortcutInfoCompat a, ShortcutInfoCompat b) {
+            if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) {
+                return -1;
+            }
+            if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) {
+                return 1;
+            }
+            return Integer.compare(a.getRank(), b.getRank());
+        }
+    };
+
+    /**
+     * Filters the shortcuts so that only MAX_ITEMS or fewer shortcuts are retained.
+     * We want the filter to include both static and dynamic shortcuts, so we always
+     * include NUM_DYNAMIC dynamic shortcuts, if at least that many are present.
+     *
+     * @return a subset of shortcuts, in sorted order, with size <= MAX_ITEMS.
+     */
+    public static List<ShortcutInfoCompat> sortAndFilterShortcuts(
+            List<ShortcutInfoCompat> shortcuts) {
+        Collections.sort(shortcuts, SHORTCUT_RANK_COMPARATOR);
+        if (shortcuts.size() <= MAX_ITEMS) {
+            return shortcuts;
+        }
+
+        // The list of shortcuts is now sorted with static shortcuts followed by dynamic
+        // shortcuts. We want to preserve this order, but only keep MAX_ITEMS.
+        List<ShortcutInfoCompat> filteredShortcuts = new ArrayList<>(MAX_ITEMS);
+        int numDynamic = 0;
+        int size = shortcuts.size();
+        for (int i = 0; i < size; i++) {
+            ShortcutInfoCompat shortcut = shortcuts.get(i);
+            int filteredSize = filteredShortcuts.size();
+            if (filteredSize < MAX_ITEMS) {
+                // Always add the first MAX_ITEMS to the filtered list.
+                filteredShortcuts.add(shortcut);
+                if (shortcut.isDynamic()) {
+                    numDynamic++;
+                }
+                continue;
+            }
+            // At this point, we have MAX_ITEMS already, but they may all be static.
+            // If there are dynamic shortcuts, remove static shortcuts to add them.
+            if (shortcut.isDynamic() && numDynamic < NUM_DYNAMIC) {
+                numDynamic++;
+                int lastStaticIndex = filteredSize - numDynamic;
+                filteredShortcuts.remove(lastStaticIndex);
+                filteredShortcuts.add(shortcut);
+            }
+        }
+        return filteredShortcuts;
+    }
+
+    public static Runnable createUpdateRunnable(final Launcher launcher, ItemInfo originalInfo,
+            final Handler uiHandler, final PopupContainerWithArrow container,
+            final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews) {
+        final ComponentName activity = originalInfo.getTargetComponent();
+        final UserHandle user = originalInfo.user;
+        return new Runnable() {
+            @Override
+            public void run() {
+                final List<ShortcutInfoCompat> shortcuts = PopupPopulator.sortAndFilterShortcuts(
+                        DeepShortcutManager.getInstance(launcher).queryForShortcutsContainer(
+                                activity, shortcutIds, user));
+                for (int i = 0; i < shortcuts.size() && i < shortcutViews.size(); i++) {
+                    final ShortcutInfoCompat shortcut = shortcuts.get(i);
+                    ShortcutInfo si = new ShortcutInfo(shortcut, launcher);
+                    // Use unbadged icon for the menu.
+                    si.iconBitmap = LauncherIcons.createShortcutIcon(
+                            shortcut, launcher, false /* badged */);
+                    uiHandler.post(new UpdateShortcutChild(container, shortcutViews.get(i),
+                            si, shortcut));
+                }
+            }
+        };
+    }
+
+    /** Updates the child of this container at the given index based on the given shortcut info. */
+    private static class UpdateShortcutChild implements Runnable {
+        private final PopupContainerWithArrow mContainer;
+        private final DeepShortcutView mShortcutChild;
+        private final ShortcutInfo mShortcutChildInfo;
+        private final ShortcutInfoCompat mDetail;
+
+        public UpdateShortcutChild(PopupContainerWithArrow container, DeepShortcutView shortcutChild,
+                ShortcutInfo shortcutChildInfo, ShortcutInfoCompat detail) {
+            mContainer = container;
+            mShortcutChild = shortcutChild;
+            mShortcutChildInfo = shortcutChildInfo;
+            mDetail = detail;
+        }
+
+        @Override
+        public void run() {
+            mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer);
+        }
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index 6e98100..2f07c9a 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -16,37 +16,27 @@
 
 package com.android.launcher3.shortcuts;
 
-import android.animation.Animator;
-import android.animation.ValueAnimator;
 import android.content.Context;
-import android.graphics.Point;
 import android.graphics.Rect;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
-import android.widget.FrameLayout;
 
 import com.android.launcher3.Launcher;
-import com.android.launcher3.LogAccelerateInterpolator;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.PillRevealOutlineProvider;
-import com.android.launcher3.util.PillWidthRevealOutlineProvider;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.popup.PopupItemView;
 
 /**
  * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
  * This lets us animate the DeepShortcutView (icon and text) separately from the background.
  */
-public class DeepShortcutView extends FrameLayout implements ValueAnimator.AnimatorUpdateListener {
-
-    private static final Point sTempPoint = new Point();
+public class DeepShortcutView extends PopupItemView {
 
     private final Rect mPillRect;
 
     private DeepShortcutTextView mBubbleText;
-    private View mIconView;
-    private float mOpenAnimationProgress;
 
     private ShortcutInfo mInfo;
     private ShortcutInfoCompat mDetail;
@@ -68,7 +58,6 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mIconView = findViewById(R.id.deep_shortcut_icon);
         mBubbleText = (DeepShortcutTextView) findViewById(R.id.deep_shortcut);
     }
 
@@ -91,8 +80,8 @@
     }
 
     /** package private **/
-    void applyShortcutInfo(ShortcutInfo info, ShortcutInfoCompat detail,
-            DeepShortcutsContainer container) {
+    public void applyShortcutInfo(ShortcutInfo info, ShortcutInfoCompat detail,
+            PopupContainerWithArrow container) {
         mInfo = info;
         mDetail = detail;
         mBubbleText.applyFromShortcutInfo(info);
@@ -127,128 +116,4 @@
     public View getIconView() {
         return mIconView;
     }
-
-    /**
-     * Creates an animator to play when the shortcut container is being opened.
-     */
-    public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) {
-        Point center = getIconCenter();
-        ValueAnimator openAnimator =  new ZoomRevealOutlineProvider(center.x, center.y,
-                mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft)
-                        .createRevealAnimator(this, false);
-        mOpenAnimationProgress = 0f;
-        openAnimator.addUpdateListener(this);
-        return openAnimator;
-    }
-
-    @Override
-    public void onAnimationUpdate(ValueAnimator valueAnimator) {
-        mOpenAnimationProgress = valueAnimator.getAnimatedFraction();
-    }
-
-    public boolean isOpenOrOpening() {
-        return mOpenAnimationProgress > 0;
-    }
-
-    /**
-     * Creates an animator to play when the shortcut container is being closed.
-     */
-    public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft,
-            long duration) {
-        Point center = getIconCenter();
-        ValueAnimator closeAnimator =  new ZoomRevealOutlineProvider(center.x, center.y,
-                mPillRect, this, mIconView, isContainerAboveIcon, pivotLeft)
-                        .createRevealAnimator(this, true);
-        // Scale down the duration and interpolator according to the progress
-        // that the open animation was at when the close started.
-        closeAnimator.setDuration((long) (duration * mOpenAnimationProgress));
-        closeAnimator.setInterpolator(new CloseInterpolator(mOpenAnimationProgress));
-        return closeAnimator;
-    }
-
-    /**
-     * Creates an animator which clips the container to form a circle around the icon.
-     */
-    public Animator collapseToIcon() {
-        int halfHeight = getMeasuredHeight() / 2;
-        int iconCenterX = getIconCenter().x;
-        return new PillWidthRevealOutlineProvider(mPillRect,
-                iconCenterX - halfHeight, iconCenterX + halfHeight)
-                        .createRevealAnimator(this, true);
-    }
-
-    /**
-     * Returns the position of the center of the icon relative to the container.
-     */
-    public Point getIconCenter() {
-        sTempPoint.y = sTempPoint.x = getMeasuredHeight() / 2;
-        if (Utilities.isRtl(getResources())) {
-            sTempPoint.x = getMeasuredWidth() - sTempPoint.x;
-        }
-        return sTempPoint;
-    }
-
-    /**
-     * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height.
-     */
-    private static class ZoomRevealOutlineProvider extends PillRevealOutlineProvider {
-
-        private final View mTranslateView;
-        private final View mZoomView;
-
-        private final float mFullHeight;
-        private final float mTranslateYMultiplier;
-
-        private final boolean mPivotLeft;
-        private final float mTranslateX;
-
-        public ZoomRevealOutlineProvider(int x, int y, Rect pillRect,
-                View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) {
-            super(x, y, pillRect);
-            mTranslateView = translateView;
-            mZoomView = zoomView;
-            mFullHeight = pillRect.height();
-
-            mTranslateYMultiplier = isContainerAboveIcon ? 0.5f : -0.5f;
-
-            mPivotLeft = pivotLeft;
-            mTranslateX = pivotLeft ? pillRect.height() / 2 : pillRect.right - pillRect.height() / 2;
-        }
-
-        @Override
-        public void setProgress(float progress) {
-            super.setProgress(progress);
-
-            mZoomView.setScaleX(progress);
-            mZoomView.setScaleY(progress);
-
-            float height = mOutline.height();
-            mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height));
-
-            float pivotX = mPivotLeft ? (mOutline.left + height / 2) : (mOutline.right - height / 2);
-            mTranslateView.setTranslationX(mTranslateX - pivotX);
-        }
-    }
-
-    /**
-     * An interpolator that reverses the current open animation progress.
-     */
-    private static class CloseInterpolator extends LogAccelerateInterpolator {
-        private float mStartProgress;
-        private float mRemainingProgress;
-
-        /**
-         * @param openAnimationProgress The progress that the open interpolator ended at.
-         */
-        public CloseInterpolator(float openAnimationProgress) {
-            super(100, 0);
-            mStartProgress = 1f - openAnimationProgress;
-            mRemainingProgress = openAnimationProgress;
-        }
-
-        @Override
-        public float getInterpolation(float v) {
-            return mStartProgress + super.getInterpolation(v) * mRemainingProgress;
-        }
-    }
 }
diff --git a/src/com/android/launcher3/shortcuts/ShortcutFilter.java b/src/com/android/launcher3/shortcuts/ShortcutFilter.java
deleted file mode 100644
index ec68817..0000000
--- a/src/com/android/launcher3/shortcuts/ShortcutFilter.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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.support.annotation.VisibleForTesting;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Sorts and filters shortcuts.
- */
-public class ShortcutFilter {
-
-    public static final int MAX_SHORTCUTS = 4;
-    @VisibleForTesting static final int NUM_DYNAMIC = 2;
-
-    /**
-     * Sorts shortcuts in rank order, with manifest shortcuts coming before dynamic shortcuts.
-     */
-    private static final Comparator<ShortcutInfoCompat> RANK_COMPARATOR
-            = new Comparator<ShortcutInfoCompat>() {
-        @Override
-        public int compare(ShortcutInfoCompat a, ShortcutInfoCompat b) {
-            if (a.isDeclaredInManifest() && !b.isDeclaredInManifest()) {
-                return -1;
-            }
-            if (!a.isDeclaredInManifest() && b.isDeclaredInManifest()) {
-                return 1;
-            }
-            return Integer.compare(a.getRank(), b.getRank());
-        }
-    };
-
-    /**
-     * Filters the shortcuts so that only MAX_SHORTCUTS or fewer shortcuts are retained.
-     * We want the filter to include both static and dynamic shortcuts, so we always
-     * include NUM_DYNAMIC dynamic shortcuts, if at least that many are present.
-     *
-     * @return a subset of shortcuts, in sorted order, with size <= MAX_SHORTCUTS.
-     */
-    public static List<ShortcutInfoCompat> sortAndFilterShortcuts(
-            List<ShortcutInfoCompat> shortcuts) {
-        Collections.sort(shortcuts, RANK_COMPARATOR);
-        if (shortcuts.size() <= MAX_SHORTCUTS) {
-            return shortcuts;
-        }
-
-        // The list of shortcuts is now sorted with static shortcuts followed by dynamic
-        // shortcuts. We want to preserve this order, but only keep MAX_SHORTCUTS.
-        List<ShortcutInfoCompat> filteredShortcuts = new ArrayList<>(MAX_SHORTCUTS);
-        int numDynamic = 0;
-        int size = shortcuts.size();
-        for (int i = 0; i < size; i++) {
-            ShortcutInfoCompat shortcut = shortcuts.get(i);
-            int filteredSize = filteredShortcuts.size();
-            if (filteredSize < MAX_SHORTCUTS) {
-                // Always add the first MAX_SHORTCUTS to the filtered list.
-                filteredShortcuts.add(shortcut);
-                if (shortcut.isDynamic()) {
-                    numDynamic++;
-                }
-                continue;
-            }
-            // At this point, we have MAX_SHORTCUTS already, but they may all be static.
-            // If there are dynamic shortcuts, remove static shortcuts to add them.
-            if (shortcut.isDynamic() && numDynamic < NUM_DYNAMIC) {
-                numDynamic++;
-                int lastStaticIndex = filteredSize - numDynamic;
-                filteredShortcuts.remove(lastStaticIndex);
-                filteredShortcuts.add(shortcut);
-            }
-        }
-        return filteredShortcuts;
-    }
-}