Converting PopupContainerWithArrow into a base class so that it is easier
to create other types of popup

Bug: 67585158
Change-Id: I966ae7bb90f941951b26feaf71b3ea30c3f3c0cc
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 4996818..0fbad52 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -38,14 +38,14 @@
 
     @IntDef(flag = true, value = {
             TYPE_FOLDER,
-            TYPE_POPUP_CONTAINER_WITH_ARROW,
+            TYPE_ACTION_POPUP,
             TYPE_WIDGETS_BOTTOM_SHEET,
             TYPE_WIDGET_RESIZE_FRAME
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface FloatingViewType {}
     public static final int TYPE_FOLDER = 1 << 0;
-    public static final int TYPE_POPUP_CONTAINER_WITH_ARROW = 1 << 1;
+    public static final int TYPE_ACTION_POPUP = 1 << 1;
     public static final int TYPE_WIDGETS_BOTTOM_SHEET = 1 << 2;
     public static final int TYPE_WIDGET_RESIZE_FRAME = 1 << 3;
 
@@ -138,7 +138,7 @@
     }
 
     public static AbstractFloatingView getTopOpenView(Launcher launcher) {
-        return getOpenView(launcher, TYPE_FOLDER | TYPE_POPUP_CONTAINER_WITH_ARROW
+        return getOpenView(launcher, TYPE_FOLDER | TYPE_ACTION_POPUP
                 | TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME);
     }
 }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 2945b22..1bb4807 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -87,7 +87,6 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.anim.AnimationLayerSet;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.LauncherAppsCompatVO;
@@ -110,6 +109,7 @@
 import com.android.launcher3.model.WidgetItem;
 import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.pageindicators.PageIndicator;
+import com.android.launcher3.popup.BaseActionPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
@@ -1347,9 +1347,9 @@
                 mWorkspace.updateIconBadges(updatedBadges);
                 mAppsView.updateIconBadges(updatedBadges);
 
-                PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(Launcher.this);
-                if (popup != null) {
-                    popup.updateNotificationHeader(updatedBadges);
+                BaseActionPopup popup = BaseActionPopup.getOpen(Launcher.this);
+                if (popup instanceof PopupContainerWithArrow) {
+                    ((PopupContainerWithArrow) popup).updateNotificationHeader(updatedBadges);
                 }
             }
         };
@@ -3558,7 +3558,7 @@
                             && mAccessibilityDelegate.performAction(focusedView,
                                     (ItemInfo) focusedView.getTag(),
                                     LauncherAccessibilityDelegate.DEEP_SHORTCUTS)) {
-                        PopupContainerWithArrow.getOpen(this).requestFocus();
+                        BaseActionPopup.getOpen(this).requestFocus();
                         return true;
                     }
                     break;
diff --git a/src/com/android/launcher3/keyboard/CustomActionsPopup.java b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
index 938955c..150522e 100644
--- a/src/com/android/launcher3/keyboard/CustomActionsPopup.java
+++ b/src/com/android/launcher3/keyboard/CustomActionsPopup.java
@@ -27,7 +27,7 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.popup.BaseActionPopup;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -46,7 +46,7 @@
     public CustomActionsPopup(Launcher launcher, View icon) {
         mLauncher = launcher;
         mIcon = icon;
-        PopupContainerWithArrow container = PopupContainerWithArrow.getOpen(launcher);
+        BaseActionPopup container = BaseActionPopup.getOpen(launcher);
         if (container != null) {
             mDelegate = container.getAccessibilityDelegate();
         } else {
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
index ad07d37..3cf3ff6 100644
--- a/src/com/android/launcher3/notification/NotificationFooterLayout.java
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.anim.PropertyResetListener;
+import com.android.launcher3.popup.BaseActionPopup;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 
 import java.util.ArrayList;
@@ -193,16 +194,17 @@
 
     private void removeViewFromIconRow(View child) {
         mIconRow.removeView(child);
-        mNotifications.remove((NotificationInfo) child.getTag());
+        mNotifications.remove(child.getTag());
         updateOverflowEllipsisVisibility();
         if (mIconRow.getChildCount() == 0) {
             // There are no more icons in the footer, so hide it.
-            PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(
+            BaseActionPopup popup = BaseActionPopup.getOpen(
                     Launcher.getLauncher(getContext()));
-            if (popup != null) {
+            if (popup instanceof PopupContainerWithArrow) {
                 final int newHeight = getResources().getDimensionPixelSize(
                         R.dimen.notification_empty_footer_height);
-                Animator collapseFooter = popup.reduceNotificationViewHeight(getHeight() - newHeight,
+                Animator collapseFooter = ((PopupContainerWithArrow) popup)
+                        .reduceNotificationViewHeight(getHeight() - newHeight,
                         getResources().getInteger(R.integer.config_removeNotificationViewDuration));
                 collapseFooter.addListener(new AnimatorListenerAdapter() {
                     @Override
diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
index 6e36f4f..8ef10e3 100644
--- a/src/com/android/launcher3/notification/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -31,7 +31,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.util.PackageUserKey;
 
 /**
@@ -110,7 +109,7 @@
             launcher.getPopupDataProvider().cancelNotification(notificationKey);
         }
         AbstractFloatingView.closeOpenContainer(launcher, AbstractFloatingView
-                .TYPE_POPUP_CONTAINER_WITH_ARROW);
+                .TYPE_ACTION_POPUP);
     }
 
     public Drawable getIconForBackground(Context context, int background) {
diff --git a/src/com/android/launcher3/popup/BaseActionPopup.java b/src/com/android/launcher3/popup/BaseActionPopup.java
new file mode 100644
index 0000000..7ffe2ef
--- /dev/null
+++ b/src/com/android/launcher3/popup/BaseActionPopup.java
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.popup;
+
+import static com.android.launcher3.popup.PopupPopulator.MAX_SHORTCUTS_IF_NOTIFICATIONS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.CornerPathEffect;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Build;
+import android.support.annotation.IntDef;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.widget.TextView;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
+import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.launcher3.dragndrop.DragLayer;
+import com.android.launcher3.graphics.TriangleShape;
+import com.android.launcher3.logging.LoggerUtils;
+import com.android.launcher3.notification.NotificationItemView;
+import com.android.launcher3.shortcuts.DeepShortcutView;
+import com.android.launcher3.shortcuts.ShortcutsItemView;
+import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.util.Themes;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * Base popup container for showing shortcuts to deep links within apps.
+ */
+@TargetApi(Build.VERSION_CODES.N)
+public class BaseActionPopup<V extends TextView> extends AbstractFloatingView {
+
+    public static final int ROUNDED_TOP_CORNERS = 1 << 0;
+    public static final int ROUNDED_BOTTOM_CORNERS = 1 << 1;
+
+    @IntDef(flag = true, value = {
+            ROUNDED_TOP_CORNERS,
+            ROUNDED_BOTTOM_CORNERS
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public  @interface RoundedCornerFlags {}
+
+    protected final Launcher mLauncher;
+    protected final LauncherAccessibilityDelegate mAccessibilityDelegate;
+    private final boolean mIsRtl;
+
+    public ShortcutsItemView mShortcutsItemView;
+
+    protected V mOriginalIcon;
+    private final Rect mTempRect = new Rect();
+    private PointF mInterceptTouchDown = new PointF();
+    private boolean mIsLeftAligned;
+    protected boolean mIsAboveIcon;
+    protected View mArrow;
+    private int mGravity;
+
+    protected Animator mOpenCloseAnimator;
+    protected boolean mDeferContainerRemoval;
+    private final Rect mStartRect = new Rect();
+    private final Rect mEndRect = new Rect();
+
+    public BaseActionPopup(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        mLauncher = Launcher.getLauncher(context);
+
+        mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
+        mIsRtl = Utilities.isRtl(getResources());
+    }
+
+    public BaseActionPopup(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public BaseActionPopup(Context context) {
+        this(context, null, 0);
+    }
+
+    public LauncherAccessibilityDelegate getAccessibilityDelegate() {
+        return mAccessibilityDelegate;
+    }
+
+    protected PopupItemView getItemViewAt(int index) {
+        if (!mIsAboveIcon) {
+            // Opening down, so arrow is the first view.
+            index++;
+        }
+        return (PopupItemView) getChildAt(index);
+    }
+
+    protected int getItemCount() {
+        // All children except the arrow are items.
+        return getChildCount() - 1;
+    }
+
+    protected void animateOpen() {
+        setVisibility(View.VISIBLE);
+        mIsOpen = true;
+
+        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
+        final Resources res = getResources();
+        final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
+        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
+
+        // Rectangular reveal.
+        int itemsTotalHeight = 0;
+        for (int i = 0; i < getItemCount(); i++) {
+            itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
+        }
+        Point startPoint = computeAnimStartPoint(itemsTotalHeight);
+        int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
+        float radius = getItemViewAt(0).getBackgroundRadius();
+        mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
+        mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
+        final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider
+                (radius, radius, mStartRect, mEndRect).createRevealAnimator(this, false);
+        revealAnim.setDuration(revealDuration);
+        revealAnim.setInterpolator(revealInterpolator);
+
+        Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1);
+        fadeIn.setDuration(revealDuration);
+        fadeIn.setInterpolator(revealInterpolator);
+        openAnim.play(fadeIn);
+
+        // Animate the arrow.
+        mArrow.setScaleX(0);
+        mArrow.setScaleY(0);
+        Animator arrowScale = createArrowScaleAnim(1).setDuration(res.getInteger(
+                R.integer.config_popupArrowOpenDuration));
+
+        openAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenCloseAnimator = null;
+                Utilities.sendCustomAccessibilityEvent(
+                        BaseActionPopup.this,
+                        AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
+                        getContext().getString(R.string.action_deep_shortcut));
+            }
+        });
+
+        mOpenCloseAnimator = openAnim;
+        openAnim.playSequentially(revealAnim, arrowScale);
+        openAnim.start();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        enforceContainedWithinScreen(l, r);
+    }
+
+    private void enforceContainedWithinScreen(int left, int right) {
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        if (getTranslationX() + left < 0 ||
+                getTranslationX() + right > dragLayer.getWidth()) {
+            // If we are still off screen, center horizontally too.
+            mGravity |= Gravity.CENTER_HORIZONTAL;
+        }
+
+        if (Gravity.isHorizontal(mGravity)) {
+            setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
+        }
+        if (Gravity.isVertical(mGravity)) {
+            setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
+        }
+    }
+
+    /**
+     * Returns the point at which the center of the arrow merges with the first popup item.
+     */
+    private Point computeAnimStartPoint(int itemsTotalHeight) {
+        int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
+                R.dimen.popup_arrow_horizontal_center_start:
+                R.dimen.popup_arrow_horizontal_center_end);
+        if (!mIsLeftAligned) {
+            arrowCenterX = getMeasuredWidth() - arrowCenterX;
+        }
+        int arrowHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
+                - itemsTotalHeight;
+        // The y-coordinate of edge between the arrow and the first popup item.
+        int arrowEdge = getPaddingTop() + (mIsAboveIcon ? itemsTotalHeight : arrowHeight);
+        return new Point(arrowCenterX, arrowEdge);
+    }
+
+    /**
+     * Orients this container above or below the given icon, aligning with the left or right.
+     *
+     * These are the preferred orientations, in order (RTL prefers right-aligned over left):
+     * - Above and left-aligned
+     * - Above and right-aligned
+     * - Below and left-aligned
+     * - Below and right-aligned
+     *
+     * So we always align left if there is enough horizontal space
+     * and align above if there is enough vertical space.
+     */
+    protected void orientAboutIcon(int arrowHeight) {
+        int width = getMeasuredWidth();
+        int height = getMeasuredHeight() + arrowHeight;
+
+        DragLayer dragLayer = mLauncher.getDragLayer();
+        dragLayer.getDescendantRectRelativeToSelf(mOriginalIcon, mTempRect);
+        Rect insets = dragLayer.getInsets();
+
+        // Align left (right in RTL) if there is room.
+        int leftAlignedX = mTempRect.left + mOriginalIcon.getPaddingLeft();
+        int rightAlignedX = mTempRect.right - width - mOriginalIcon.getPaddingRight();
+        int x = leftAlignedX;
+        boolean canBeLeftAligned = leftAlignedX + width + insets.left
+                < dragLayer.getRight() - insets.right;
+        boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
+        if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
+            x = rightAlignedX;
+        }
+        mIsLeftAligned = x == leftAlignedX;
+        if (mIsRtl) {
+            x -= dragLayer.getWidth() - width;
+        }
+
+        // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
+        int iconWidth = mOriginalIcon.getWidth()
+                - mOriginalIcon.getTotalPaddingLeft() - mOriginalIcon.getTotalPaddingRight();
+        iconWidth *= mOriginalIcon.getScaleX();
+        Resources resources = getResources();
+        int xOffset;
+        if (isAlignedWithStart()) {
+            // Aligning with the shortcut icon.
+            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
+            int shortcutPaddingStart = resources.getDimensionPixelSize(
+                    R.dimen.popup_padding_start);
+            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
+        } else {
+            // Aligning with the drag handle.
+            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
+                    R.dimen.deep_shortcut_drag_handle_size);
+            int shortcutPaddingEnd = resources.getDimensionPixelSize(
+                    R.dimen.popup_padding_end);
+            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
+        }
+        x += mIsLeftAligned ? xOffset : -xOffset;
+
+        // Open above icon if there is room.
+        int iconHeight = getIconHeightForPopupPlacement();
+        int y = mTempRect.top + mOriginalIcon.getPaddingTop() - height;
+        mIsAboveIcon = y > dragLayer.getTop() + insets.top;
+        if (!mIsAboveIcon) {
+            y = mTempRect.top + mOriginalIcon.getPaddingTop() + iconHeight;
+        }
+
+        // Insets are added later, so subtract them now.
+        if (mIsRtl) {
+            x += insets.right;
+        } else {
+            x -= insets.left;
+        }
+        y -= insets.top;
+
+        mGravity = 0;
+        if (y + height > dragLayer.getBottom() - insets.bottom) {
+            // The container is opening off the screen, so just center it in the drag layer instead.
+            mGravity = Gravity.CENTER_VERTICAL;
+            // Put the container next to the icon, preferring the right side in ltr (left in rtl).
+            int rightSide = leftAlignedX + iconWidth - insets.left;
+            int leftSide = rightAlignedX - iconWidth - insets.left;
+            if (!mIsRtl) {
+                if (rightSide + width < dragLayer.getRight()) {
+                    x = rightSide;
+                    mIsLeftAligned = true;
+                } else {
+                    x = leftSide;
+                    mIsLeftAligned = false;
+                }
+            } else {
+                if (leftSide > dragLayer.getLeft()) {
+                    x = leftSide;
+                    mIsLeftAligned = false;
+                } else {
+                    x = rightSide;
+                    mIsLeftAligned = true;
+                }
+            }
+            mIsAboveIcon = true;
+        }
+
+        setX(x);
+        setY(y);
+    }
+
+    protected int getIconHeightForPopupPlacement() {
+        return mOriginalIcon.getHeight();
+    }
+
+    protected boolean isAlignedWithStart() {
+        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
+    }
+
+    /**
+     * Adds an arrow view pointing at the original icon.
+     * @param horizontalOffset the horizontal offset of the arrow, so that it
+     *                              points at the center of the original icon
+     */
+    protected View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
+        LayoutParams layoutParams = new LayoutParams(width, height);
+        if (mIsLeftAligned) {
+            layoutParams.gravity = Gravity.LEFT;
+            layoutParams.leftMargin = horizontalOffset;
+        } else {
+            layoutParams.gravity = Gravity.RIGHT;
+            layoutParams.rightMargin = horizontalOffset;
+        }
+        if (mIsAboveIcon) {
+            layoutParams.topMargin = verticalOffset;
+        } else {
+            layoutParams.bottomMargin = verticalOffset;
+        }
+
+        View arrowView = new View(getContext());
+        if (Gravity.isVertical(mGravity)) {
+            // This is only true if there wasn't room for the container next to the icon,
+            // so we centered it instead. In that case we don't want to show the arrow.
+            arrowView.setVisibility(INVISIBLE);
+        } else {
+            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                    width, height, !mIsAboveIcon));
+            Paint arrowPaint = arrowDrawable.getPaint();
+            arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary));
+            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
+            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
+            arrowPaint.setPathEffect(new CornerPathEffect(radius));
+            arrowView.setBackground(arrowDrawable);
+            arrowView.setElevation(getElevation());
+        }
+        addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
+        return arrowView;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            mInterceptTouchDown.set(ev.getX(), ev.getY());
+            return false;
+        }
+        // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
+        return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
+                > ViewConfiguration.get(getContext()).getScaledTouchSlop();
+    }
+
+    protected ObjectAnimator createArrowScaleAnim(float scale) {
+        return LauncherAnimUtils.ofPropertyValuesHolder(
+                mArrow, new PropertyListBuilder().scale(scale).build());
+    }
+
+    @Override
+    protected void handleClose(boolean animate) {
+        if (animate) {
+            animateClose();
+        } else {
+            closeComplete();
+        }
+    }
+
+    protected void animateClose() {
+        if (!mIsOpen) {
+            return;
+        }
+        mEndRect.setEmpty();
+        if (mOpenCloseAnimator != null) {
+            Outline outline = new Outline();
+            getOutlineProvider().getOutline(this, outline);
+            outline.getRect(mEndRect);
+            mOpenCloseAnimator.cancel();
+        }
+        mIsOpen = false;
+
+        final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
+        prepareCloseAnimator(closeAnim);
+
+        closeAnim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenCloseAnimator = null;
+                if (mDeferContainerRemoval) {
+                    setVisibility(INVISIBLE);
+                } else {
+                    closeComplete();
+                }
+            }
+        });
+        mOpenCloseAnimator = closeAnim;
+        closeAnim.start();
+    }
+
+    protected void prepareCloseAnimator(AnimatorSet closeAnim) {
+        final Resources res = getResources();
+        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
+
+        // Rectangular reveal (reversed).
+        int itemsTotalHeight = 0;
+        for (int i = 0; i < getItemCount(); i++) {
+            itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
+        }
+        Point startPoint = computeAnimStartPoint(itemsTotalHeight);
+        int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
+        float radius = getItemViewAt(0).getBackgroundRadius();
+        mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
+        if (mEndRect.isEmpty()) {
+            mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
+        }
+        final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider(
+                radius, radius, mStartRect, mEndRect).createRevealAnimator(this, true);
+        revealAnim.setInterpolator(revealInterpolator);
+        closeAnim.play(revealAnim);
+
+        Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
+        fadeOut.setInterpolator(revealInterpolator);
+        closeAnim.play(fadeOut);
+        closeAnim.setDuration((long) res.getInteger(R.integer.config_popupOpenCloseDuration));
+    }
+
+    /**
+     * Closes the folder without animation.
+     */
+    protected void closeComplete() {
+        if (mOpenCloseAnimator != null) {
+            mOpenCloseAnimator.cancel();
+            mOpenCloseAnimator = null;
+        }
+        mIsOpen = false;
+        mDeferContainerRemoval = false;
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    @Override
+    protected boolean isOfType(int type) {
+        return (type & TYPE_ACTION_POPUP) != 0;
+    }
+
+    /**
+     * Returns a DeepShortcutsContainer which is already open or null
+     */
+    public static BaseActionPopup getOpen(Launcher launcher) {
+        return getOpenView(launcher, TYPE_ACTION_POPUP);
+    }
+
+    @Override
+    public void logActionCommand(int command) {
+        mLauncher.getUserEventDispatcher().logActionCommand(
+                command, mOriginalIcon, ContainerType.DEEPSHORTCUTS);
+    }
+
+    @Override
+    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            DragLayer dl = mLauncher.getDragLayer();
+            if (!dl.isEventOverView(this, ev)) {
+                mLauncher.getUserEventDispatcher().logActionTapOutside(
+                        LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS));
+                close(true);
+
+                // We let touches on the original icon go through so that users can launch
+                // the app with one tap if they don't find a shortcut they want.
+                return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev);
+            }
+        }
+        return false;
+    }
+
+    public void populateAndShow(V originalIcon, PopupPopulator.Item[] itemsToPopulate) {
+        setVisibility(View.INVISIBLE);
+        mLauncher.getDragLayer().addView(this);
+
+        final Resources resources = getResources();
+        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
+        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
+        final int arrowVerticalOffset = resources.getDimensionPixelSize(
+                R.dimen.popup_arrow_vertical_offset);
+
+        mOriginalIcon = originalIcon;
+
+        // Add dummy views first, and populate with real info when ready.
+        addDummyViews(itemsToPopulate);
+
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        orientAboutIcon(arrowHeight + arrowVerticalOffset);
+
+        boolean reverseOrder = mIsAboveIcon;
+        if (reverseOrder) {
+            removeAllViews();
+            mShortcutsItemView = null;
+            itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
+            addDummyViews(itemsToPopulate);
+
+            measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+            orientAboutIcon(arrowHeight + arrowVerticalOffset);
+        }
+
+        // Add the arrow.
+        final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ?
+                R.dimen.popup_arrow_horizontal_offset_start :
+                R.dimen.popup_arrow_horizontal_offset_end);
+        mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
+        mArrow.setPivotX(arrowWidth / 2);
+        mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
+
+        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        animateOpen();
+    }
+
+    protected void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate) {
+        final LayoutInflater inflater = mLauncher.getLayoutInflater();
+        int shortcutsItemRoundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
+        int numItems = itemTypesToPopulate.length;
+        for (int i = 0; i < numItems; i++) {
+            PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
+            PopupPopulator.Item prevItemTypeToPopulate =
+                    i > 0 ? itemTypesToPopulate[i - 1] : null;
+            PopupPopulator.Item nextItemTypeToPopulate =
+                    i < numItems - 1 ? itemTypesToPopulate[i + 1] : null;
+            final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
+
+            boolean shouldUnroundTopCorners = prevItemTypeToPopulate != null
+                    && itemTypeToPopulate.isShortcut ^ prevItemTypeToPopulate.isShortcut;
+            boolean shouldUnroundBottomCorners = nextItemTypeToPopulate != null
+                    && itemTypeToPopulate.isShortcut ^ nextItemTypeToPopulate.isShortcut;
+
+            onViewInflated(item, itemTypeToPopulate,
+                    shouldUnroundTopCorners, shouldUnroundBottomCorners);
+
+            if (itemTypeToPopulate.isShortcut) {
+                if (mShortcutsItemView == null) {
+                    mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
+                            R.layout.shortcuts_item, this, false);
+                    addView(mShortcutsItemView);
+                    if (shouldUnroundTopCorners) {
+                        shortcutsItemRoundedCorners &= ~ROUNDED_TOP_CORNERS;
+                    }
+                }
+                mShortcutsItemView.addShortcutView(item, itemTypeToPopulate);
+                if (shouldUnroundBottomCorners) {
+                    shortcutsItemRoundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
+                }
+            } else {
+                addView(item);
+            }
+        }
+        int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary);
+        mShortcutsItemView.setBackgroundWithCorners(backgroundColor, shortcutsItemRoundedCorners);
+    }
+
+    protected void onViewInflated(View view, PopupPopulator.Item itemType,
+            boolean shouldUnroundTopCorners, boolean shouldUnroundBottomCorners) {
+
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 5c49b4b..68b547d 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -25,32 +25,17 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.CornerPathEffect;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.drawable.ShapeDrawable;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
-import android.support.annotation.IntDef;
 import android.util.AttributeSet;
-import android.view.Gravity;
 import android.view.LayoutInflater;
-import android.view.MotionEvent;
 import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.animation.AccelerateDecelerateInterpolator;
 
-import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
@@ -59,81 +44,42 @@
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
-import com.android.launcher3.accessibility.ShortcutMenuAccessibilityDelegate;
-import com.android.launcher3.anim.PropertyListBuilder;
 import com.android.launcher3.anim.PropertyResetListener;
-import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
 import com.android.launcher3.badge.BadgeInfo;
 import com.android.launcher3.dragndrop.DragController;
-import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.graphics.TriangleShape;
-import com.android.launcher3.logging.LoggerUtils;
 import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.notification.NotificationKeyData;
+import com.android.launcher3.popup.PopupPopulator.Item;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutsItemView;
 import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Themes;
 
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
- * A container for shortcuts to deep links within apps.
+ * A container for shortcuts to deep links and notifications associated with an app.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
+public class PopupContainerWithArrow extends BaseActionPopup<BubbleTextView> implements DragSource,
         DragController.DragListener {
 
-    public static final int ROUNDED_TOP_CORNERS = 1 << 0;
-    public static final int ROUNDED_BOTTOM_CORNERS = 1 << 1;
-
-    @IntDef(flag = true, value = {
-            ROUNDED_TOP_CORNERS,
-            ROUNDED_BOTTOM_CORNERS
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    public  @interface RoundedCornerFlags {}
-
-    protected final Launcher mLauncher;
     private final int mStartDragThreshold;
-    private LauncherAccessibilityDelegate mAccessibilityDelegate;
-    private final boolean mIsRtl;
 
-    public ShortcutsItemView mShortcutsItemView;
     private NotificationItemView mNotificationItemView;
-
-    protected BubbleTextView mOriginalIcon;
-    private final Rect mTempRect = new Rect();
-    private PointF mInterceptTouchDown = new PointF();
-    private boolean mIsLeftAligned;
-    protected boolean mIsAboveIcon;
-    private View mArrow;
-    private int mGravity;
-
-    protected Animator mOpenCloseAnimator;
-    private boolean mDeferContainerRemoval;
     private AnimatorSet mReduceHeightAnimatorSet;
-    private final Rect mStartRect = new Rect();
-    private final Rect mEndRect = new Rect();
+    private int mNumNotifications;
 
     public PopupContainerWithArrow(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
-        mLauncher = Launcher.getLauncher(context);
-
         mStartDragThreshold = getResources().getDimensionPixelSize(
                 R.dimen.deep_shortcuts_start_drag_threshold);
-        mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(mLauncher);
-        mIsRtl = Utilities.isRtl(getResources());
     }
 
     public PopupContainerWithArrow(Context context, AttributeSet attrs) {
@@ -144,10 +90,6 @@
         this(context, null, 0);
     }
 
-    public LauncherAccessibilityDelegate getAccessibilityDelegate() {
-        return mAccessibilityDelegate;
-    }
-
     /**
      * Shows the notifications and deep shortcuts associated with {@param icon}.
      * @return the container if shown or null.
@@ -174,49 +116,24 @@
         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, notificationKeys, systemShortcuts);
         return container;
     }
 
-    public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
+    private void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
             final List<NotificationKeyData> notificationKeys, List<SystemShortcut> systemShortcuts) {
-        final Resources resources = getResources();
-        final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
-        final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
-        final int arrowVerticalOffset = resources.getDimensionPixelSize(
-                R.dimen.popup_arrow_vertical_offset);
-
-        mOriginalIcon = originalIcon;
-
-        // Add dummy views first, and populate with real info when ready.
+        mNumNotifications = notificationKeys.size();
         PopupPopulator.Item[] itemsToPopulate = PopupPopulator
                 .getItemsToPopulate(shortcutIds, notificationKeys, systemShortcuts);
-        addDummyViews(itemsToPopulate, notificationKeys.size());
-
-        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-        orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
-
-        boolean reverseOrder = mIsAboveIcon;
-        if (reverseOrder) {
-            removeAllViews();
-            mNotificationItemView = null;
-            mShortcutsItemView = null;
-            itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
-            addDummyViews(itemsToPopulate, notificationKeys.size());
-
-            measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-            orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
-        }
+        populateAndShow(originalIcon, itemsToPopulate);
 
         ItemInfo originalItemInfo = (ItemInfo) originalIcon.getTag();
         List<DeepShortcutView> shortcutViews = mShortcutsItemView == null
                 ? Collections.EMPTY_LIST
-                : mShortcutsItemView.getDeepShortcutViews(reverseOrder);
+                : mShortcutsItemView.getDeepShortcutViews(mIsAboveIcon);
         List<View> systemShortcutViews = mShortcutsItemView == null
                 ? Collections.EMPTY_LIST
-                : mShortcutsItemView.getSystemShortcutViews(reverseOrder);
+                : mShortcutsItemView.getSystemShortcutViews(mIsAboveIcon);
         if (mNotificationItemView != null) {
             updateNotificationHeader();
         }
@@ -232,17 +149,6 @@
                     numNotifications, originalIcon.getContentDescription().toString()));
         }
 
-        // Add the arrow.
-        final int arrowHorizontalOffset = resources.getDimensionPixelSize(isAlignedWithStart() ?
-                R.dimen.popup_arrow_horizontal_offset_start :
-                R.dimen.popup_arrow_horizontal_offset_end);
-        mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
-        mArrow.setPivotX(arrowWidth / 2);
-        mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
-
-        measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
-        animateOpen();
-
         mLauncher.getDragController().addDragListener(this);
         mOriginalIcon.forceHideBadge(true);
 
@@ -254,6 +160,60 @@
                 systemShortcuts, systemShortcutViews));
     }
 
+    @Override
+    protected void addDummyViews(Item[] itemTypesToPopulate) {
+        mNotificationItemView = null;
+        super.addDummyViews(itemTypesToPopulate);
+        if (mNumNotifications > 0) {
+            mShortcutsItemView.hideShortcuts(mIsAboveIcon, MAX_SHORTCUTS_IF_NOTIFICATIONS);
+        }
+    }
+
+    @Override
+    protected void onViewInflated(View view, Item itemType,
+            boolean shouldUnroundTopCorners, boolean shouldUnroundBottomCorners) {
+        if (itemType == PopupPopulator.Item.NOTIFICATION) {
+            mNotificationItemView = (NotificationItemView) view;
+            boolean notificationFooterHasIcons = mNumNotifications > 1;
+            int footerHeight = getResources().getDimensionPixelSize(
+                    notificationFooterHasIcons ? R.dimen.notification_footer_height
+                            : R.dimen.notification_empty_footer_height);
+            view.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
+            if (notificationFooterHasIcons) {
+                mNotificationItemView.findViewById(R.id.divider).setVisibility(VISIBLE);
+            }
+
+            int roundedCorners = ROUNDED_TOP_CORNERS | ROUNDED_BOTTOM_CORNERS;
+            if (shouldUnroundTopCorners) {
+                roundedCorners &= ~ROUNDED_TOP_CORNERS;
+                mNotificationItemView.findViewById(R.id.gutter_top).setVisibility(VISIBLE);
+            }
+            if (shouldUnroundBottomCorners) {
+                roundedCorners &= ~ROUNDED_BOTTOM_CORNERS;
+                mNotificationItemView.findViewById(R.id.gutter_bottom).setVisibility(VISIBLE);
+            }
+            int backgroundColor = Themes.getAttrColor(mLauncher, R.attr.popupColorTertiary);
+            mNotificationItemView.setBackgroundWithCorners(backgroundColor, roundedCorners);
+
+            mNotificationItemView.getMainView().setAccessibilityDelegate(mAccessibilityDelegate);
+        } else if (itemType == PopupPopulator.Item.SHORTCUT) {
+            view.setAccessibilityDelegate(mAccessibilityDelegate);
+        }
+
+        if (itemType != PopupPopulator.Item.SYSTEM_SHORTCUT_ICON && itemType.isShortcut
+                && mNumNotifications > 0) {
+            int prevHeight = view.getLayoutParams().height;
+            // Condense shortcuts height when there are notifications.
+            view.getLayoutParams().height = getResources().getDimensionPixelSize(
+                    R.dimen.bg_popup_item_condensed_height);
+            if (view instanceof DeepShortcutView) {
+                float iconScale = (float) view.getLayoutParams().height / prevHeight;
+                ((DeepShortcutView) view).getIconView().setScaleX(iconScale);
+                ((DeepShortcutView) view).getIconView().setScaleY(iconScale);
+            }
+        }
+    }
+
     private void addDummyViews(PopupPopulator.Item[] itemTypesToPopulate, int numNotifications) {
         final Resources res = getResources();
         final LayoutInflater inflater = mLauncher.getLayoutInflater();
@@ -337,261 +297,18 @@
         }
     }
 
-    protected PopupItemView getItemViewAt(int index) {
-        if (!mIsAboveIcon) {
-            // Opening down, so arrow is the first view.
-            index++;
+    @Override
+    protected void onWidgetsBound() {
+        if (mShortcutsItemView != null) {
+            mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
         }
-        return (PopupItemView) getChildAt(index);
-    }
-
-    protected int getItemCount() {
-        // All children except the arrow are items.
-        return getChildCount() - 1;
-    }
-
-    private void animateOpen() {
-        setVisibility(View.VISIBLE);
-        mIsOpen = true;
-
-        final AnimatorSet openAnim = LauncherAnimUtils.createAnimatorSet();
-        final Resources res = getResources();
-        final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
-        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
-
-        // Rectangular reveal.
-        int itemsTotalHeight = 0;
-        for (int i = 0; i < getItemCount(); i++) {
-            itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
-        }
-        Point startPoint = computeAnimStartPoint(itemsTotalHeight);
-        int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
-        float radius = getItemViewAt(0).getBackgroundRadius();
-        mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
-        mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
-        final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider
-                (radius, radius, mStartRect, mEndRect).createRevealAnimator(this, false);
-        revealAnim.setDuration(revealDuration);
-        revealAnim.setInterpolator(revealInterpolator);
-
-        Animator fadeIn = ObjectAnimator.ofFloat(this, ALPHA, 0, 1);
-        fadeIn.setDuration(revealDuration);
-        fadeIn.setInterpolator(revealInterpolator);
-        openAnim.play(fadeIn);
-
-        // Animate the arrow.
-        mArrow.setScaleX(0);
-        mArrow.setScaleY(0);
-        Animator arrowScale = createArrowScaleAnim(1).setDuration(res.getInteger(
-                R.integer.config_popupArrowOpenDuration));
-
-        openAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator = null;
-                Utilities.sendCustomAccessibilityEvent(
-                        PopupContainerWithArrow.this,
-                        AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
-                        getContext().getString(R.string.action_deep_shortcut));
-            }
-        });
-
-        mOpenCloseAnimator = openAnim;
-        openAnim.playSequentially(revealAnim, arrowScale);
-        openAnim.start();
     }
 
     @Override
-    protected void onLayout(boolean changed, int l, int t, int r, int b) {
-        super.onLayout(changed, l, t, r, b);
-        enforceContainedWithinScreen(l, r);
-
-    }
-
-    private void enforceContainedWithinScreen(int left, int right) {
-        DragLayer dragLayer = mLauncher.getDragLayer();
-        if (getTranslationX() + left < 0 ||
-                getTranslationX() + right > dragLayer.getWidth()) {
-            // If we are still off screen, center horizontally too.
-            mGravity |= Gravity.CENTER_HORIZONTAL;
-        }
-
-        if (Gravity.isHorizontal(mGravity)) {
-            setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
-        }
-        if (Gravity.isVertical(mGravity)) {
-            setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
-        }
-    }
-
-    /**
-     * Returns the point at which the center of the arrow merges with the first popup item.
-     */
-    private Point computeAnimStartPoint(int itemsTotalHeight) {
-        int arrowCenterX = getResources().getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ?
-                R.dimen.popup_arrow_horizontal_center_start:
-                R.dimen.popup_arrow_horizontal_center_end);
-        if (!mIsLeftAligned) {
-            arrowCenterX = getMeasuredWidth() - arrowCenterX;
-        }
-        int arrowHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom()
-                - itemsTotalHeight;
-        // The y-coordinate of edge between the arrow and the first popup item.
-        int arrowEdge = getPaddingTop() + (mIsAboveIcon ? itemsTotalHeight : arrowHeight);
-        return new Point(arrowCenterX, arrowEdge);
-    }
-
-    /**
-     * Orients this container above or below the given icon, aligning with the left or right.
-     *
-     * These are the preferred orientations, in order (RTL prefers right-aligned over left):
-     * - Above and left-aligned
-     * - Above and right-aligned
-     * - Below and left-aligned
-     * - Below and right-aligned
-     *
-     * So we always align left if there is enough horizontal space
-     * and align above if there is enough vertical space.
-     */
-    private void orientAboutIcon(BubbleTextView icon, int arrowHeight) {
-        int width = getMeasuredWidth();
-        int height = getMeasuredHeight() + arrowHeight;
-
-        DragLayer dragLayer = mLauncher.getDragLayer();
-        dragLayer.getDescendantRectRelativeToSelf(icon, mTempRect);
-        Rect insets = dragLayer.getInsets();
-
-        // Align left (right in RTL) if there is room.
-        int leftAlignedX = mTempRect.left + icon.getPaddingLeft();
-        int rightAlignedX = mTempRect.right - width - icon.getPaddingRight();
-        int x = leftAlignedX;
-        boolean canBeLeftAligned = leftAlignedX + width + insets.left
-                < dragLayer.getRight() - insets.right;
-        boolean canBeRightAligned = rightAlignedX > dragLayer.getLeft() + insets.left;
-        if (!canBeLeftAligned || (mIsRtl && canBeRightAligned)) {
-            x = rightAlignedX;
-        }
-        mIsLeftAligned = x == leftAlignedX;
-        if (mIsRtl) {
-            x -= dragLayer.getWidth() - width;
-        }
-
-        // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
-        int iconWidth = icon.getWidth() - icon.getTotalPaddingLeft() - icon.getTotalPaddingRight();
-        iconWidth *= icon.getScaleX();
-        Resources resources = getResources();
-        int xOffset;
-        if (isAlignedWithStart()) {
-            // Aligning with the shortcut icon.
-            int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size);
-            int shortcutPaddingStart = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_start);
-            xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart;
-        } else {
-            // Aligning with the drag handle.
-            int shortcutDragHandleWidth = resources.getDimensionPixelSize(
-                    R.dimen.deep_shortcut_drag_handle_size);
-            int shortcutPaddingEnd = resources.getDimensionPixelSize(
-                    R.dimen.popup_padding_end);
-            xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd;
-        }
-        x += mIsLeftAligned ? xOffset : -xOffset;
-
-        // Open above icon if there is room.
-        int iconHeight = icon.getIcon() != null
-                ? icon.getIcon().getBounds().height()
-                : icon.getHeight();
-        int y = mTempRect.top + icon.getPaddingTop() - height;
-        mIsAboveIcon = y > dragLayer.getTop() + insets.top;
-        if (!mIsAboveIcon) {
-            y = mTempRect.top + icon.getPaddingTop() + iconHeight;
-        }
-
-        // Insets are added later, so subtract them now.
-        if (mIsRtl) {
-            x += insets.right;
-        } else {
-            x -= insets.left;
-        }
-        y -= insets.top;
-
-        mGravity = 0;
-        if (y + height > dragLayer.getBottom() - insets.bottom) {
-            // The container is opening off the screen, so just center it in the drag layer instead.
-            mGravity = Gravity.CENTER_VERTICAL;
-            // Put the container next to the icon, preferring the right side in ltr (left in rtl).
-            int rightSide = leftAlignedX + iconWidth - insets.left;
-            int leftSide = rightAlignedX - iconWidth - insets.left;
-            if (!mIsRtl) {
-                if (rightSide + width < dragLayer.getRight()) {
-                    x = rightSide;
-                    mIsLeftAligned = true;
-                } else {
-                    x = leftSide;
-                    mIsLeftAligned = false;
-                }
-            } else {
-                if (leftSide > dragLayer.getLeft()) {
-                    x = leftSide;
-                    mIsLeftAligned = false;
-                } else {
-                    x = rightSide;
-                    mIsLeftAligned = true;
-                }
-            }
-            mIsAboveIcon = true;
-        }
-
-        setX(x);
-        setY(y);
-    }
-
-    private boolean isAlignedWithStart() {
-        return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl;
-    }
-
-    /**
-     * Adds an arrow view pointing at the original icon.
-     * @param horizontalOffset the horizontal offset of the arrow, so that it
-     *                              points at the center of the original icon
-     */
-    private View addArrowView(int horizontalOffset, int verticalOffset, int width, int height) {
-        LayoutParams layoutParams = new LayoutParams(width, height);
-        if (mIsLeftAligned) {
-            layoutParams.gravity = Gravity.LEFT;
-            layoutParams.leftMargin = horizontalOffset;
-        } else {
-            layoutParams.gravity = Gravity.RIGHT;
-            layoutParams.rightMargin = horizontalOffset;
-        }
-        if (mIsAboveIcon) {
-            layoutParams.topMargin = verticalOffset;
-        } else {
-            layoutParams.bottomMargin = verticalOffset;
-        }
-
-        View arrowView = new View(getContext());
-        if (Gravity.isVertical(mGravity)) {
-            // This is only true if there wasn't room for the container next to the icon,
-            // so we centered it instead. In that case we don't want to show the arrow.
-            arrowView.setVisibility(INVISIBLE);
-        } else {
-            ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
-                    width, height, !mIsAboveIcon));
-            Paint arrowPaint = arrowDrawable.getPaint();
-            // Note that we have to use getChildAt() instead of getItemViewAt(),
-            // since the latter expects the arrow which hasn't been added yet.
-            PopupItemView itemAttachedToArrow = (PopupItemView)
-                    (getChildAt(mIsAboveIcon ? getChildCount() - 1 : 0));
-            arrowPaint.setColor(Themes.getAttrColor(mLauncher, R.attr.popupColorPrimary));
-            // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable.
-            int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
-            arrowPaint.setPathEffect(new CornerPathEffect(radius));
-            arrowView.setBackground(arrowDrawable);
-            arrowView.setElevation(getElevation());
-        }
-        addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
-        return arrowView;
+    protected int getIconHeightForPopupPlacement() {
+        return mOriginalIcon.getIcon() != null
+                ? mOriginalIcon.getIcon().getBounds().height()
+                : mOriginalIcon.getHeight();
     }
 
     /**
@@ -638,17 +355,6 @@
         };
     }
 
-    @Override
-    public boolean onInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            mInterceptTouchDown.set(ev.getX(), ev.getY());
-            return false;
-        }
-        // Stop sending touch events to deep shortcut views if user moved beyond touch slop.
-        return Math.hypot(mInterceptTouchDown.x - ev.getX(), mInterceptTouchDown.y - ev.getY())
-                > ViewConfiguration.get(getContext()).getScaledTouchSlop();
-    }
-
     /**
      * Updates the notification header if the original icon's badge updated.
      */
@@ -719,18 +425,6 @@
                 badgeInfo.getNotificationKeys()));
     }
 
-    @Override
-    protected void onWidgetsBound() {
-        if (mShortcutsItemView != null) {
-            mShortcutsItemView.enableWidgetsIfExist(mOriginalIcon);
-        }
-    }
-
-    private ObjectAnimator createArrowScaleAnim(float scale) {
-        return LauncherAnimUtils.ofPropertyValuesHolder(
-                mArrow, new PropertyListBuilder().scale(scale).build());
-    }
-
     public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
         return adjustItemHeights(heightToRemove, 0, duration);
     }
@@ -832,124 +526,20 @@
     }
 
     @Override
-    protected void handleClose(boolean animate) {
-        if (animate) {
-            animateClose();
-        } else {
-            closeComplete();
-        }
-    }
-
-    protected void animateClose() {
-        if (!mIsOpen) {
-            return;
-        }
-        mEndRect.setEmpty();
-        if (mOpenCloseAnimator != null) {
-            Outline outline = new Outline();
-            getOutlineProvider().getOutline(this, outline);
-            outline.getRect(mEndRect);
-            mOpenCloseAnimator.cancel();
-        }
-        mIsOpen = false;
-
-        final AnimatorSet closeAnim = LauncherAnimUtils.createAnimatorSet();
-        final Resources res = getResources();
-        final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration);
-        final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator();
-
-        // Rectangular reveal (reversed).
-        int itemsTotalHeight = 0;
-        for (int i = 0; i < getItemCount(); i++) {
-            itemsTotalHeight += getItemViewAt(i).getMeasuredHeight();
-        }
-        Point startPoint = computeAnimStartPoint(itemsTotalHeight);
-        int top = mIsAboveIcon ? getPaddingTop() : startPoint.y;
-        float radius = getItemViewAt(0).getBackgroundRadius();
-        mStartRect.set(startPoint.x, startPoint.y, startPoint.x, startPoint.y);
-        if (mEndRect.isEmpty()) {
-            mEndRect.set(0, top, getMeasuredWidth(), top + itemsTotalHeight);
-        }
-        final ValueAnimator revealAnim = new RoundedRectRevealOutlineProvider(
-                radius, radius, mStartRect, mEndRect).createRevealAnimator(this, true);
-        revealAnim.setDuration(revealDuration);
-        revealAnim.setInterpolator(revealInterpolator);
-        closeAnim.play(revealAnim);
-
-        Animator fadeOut = ObjectAnimator.ofFloat(this, ALPHA, 0);
-        fadeOut.setDuration(revealDuration);
-        fadeOut.setInterpolator(revealInterpolator);
-        closeAnim.play(fadeOut);
-
+    protected void prepareCloseAnimator(AnimatorSet closeAnim) {
         // Animate original icon's text back in.
-        Animator fadeText = mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */);
-        fadeText.setDuration(revealDuration);
-        closeAnim.play(fadeText);
+        closeAnim.play(mOriginalIcon.createTextAlphaAnimator(true /* fadeIn */));
 
-        closeAnim.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mOpenCloseAnimator = null;
-                if (mDeferContainerRemoval) {
-                    setVisibility(INVISIBLE);
-                } else {
-                    closeComplete();
-                }
-            }
-        });
-        mOpenCloseAnimator = closeAnim;
-        closeAnim.start();
         mOriginalIcon.forceHideBadge(false);
+        super.prepareCloseAnimator(closeAnim);
     }
 
-    /**
-     * Closes the folder without animation.
-     */
+    @Override
     protected void closeComplete() {
-        if (mOpenCloseAnimator != null) {
-            mOpenCloseAnimator.cancel();
-            mOpenCloseAnimator = null;
-        }
-        mIsOpen = false;
-        mDeferContainerRemoval = false;
         mOriginalIcon.setTextVisibility(mOriginalIcon.shouldTextBeVisible());
         mOriginalIcon.forceHideBadge(false);
+
         mLauncher.getDragController().removeDragListener(this);
-        mLauncher.getDragLayer().removeView(this);
-    }
-
-    @Override
-    protected boolean isOfType(int type) {
-        return (type & TYPE_POPUP_CONTAINER_WITH_ARROW) != 0;
-    }
-
-    /**
-     * Returns a DeepShortcutsContainer which is already open or null
-     */
-    public static PopupContainerWithArrow getOpen(Launcher launcher) {
-        return getOpenView(launcher, TYPE_POPUP_CONTAINER_WITH_ARROW);
-    }
-
-    @Override
-    public void logActionCommand(int command) {
-        mLauncher.getUserEventDispatcher().logActionCommand(
-                command, mOriginalIcon, ContainerType.DEEPSHORTCUTS);
-    }
-
-    @Override
-    public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
-        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
-            DragLayer dl = mLauncher.getDragLayer();
-            if (!dl.isEventOverView(this, ev)) {
-                mLauncher.getUserEventDispatcher().logActionTapOutside(
-                        LoggerUtils.newContainerTarget(ContainerType.DEEPSHORTCUTS));
-                close(true);
-
-                // We let touches on the original icon go through so that users can launch
-                // the app with one tap if they don't find a shortcut they want.
-                return mOriginalIcon == null || !dl.isEventOverView(mOriginalIcon, ev);
-            }
-        }
-        return false;
+        super.closeComplete();
     }
 }
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index c921b4b..aeb7134 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -102,11 +102,7 @@
                 mPackageUserToBadgeInfos.remove(removedPackageUserKey);
             }
             updateLauncherIconBadges(Utilities.singletonHashSet(removedPackageUserKey));
-
-            PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
-            if (openContainer != null) {
-                openContainer.trimNotifications(mPackageUserToBadgeInfos);
-            }
+            trimNotifications(mPackageUserToBadgeInfos);
         }
     }
 
@@ -143,10 +139,13 @@
         if (!updatedBadges.isEmpty()) {
             updateLauncherIconBadges(updatedBadges.keySet());
         }
+        trimNotifications(updatedBadges);
+    }
 
-        PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
-        if (openContainer != null) {
-            openContainer.trimNotifications(updatedBadges);
+    private void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
+        BaseActionPopup openContainer = BaseActionPopup.getOpen(mLauncher);
+        if (openContainer instanceof PopupContainerWithArrow) {
+            ((PopupContainerWithArrow) openContainer).trimNotifications(updatedBadges);
         }
     }
 
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
index 8ec051b..75c3f26 100644
--- a/src/com/android/launcher3/popup/PopupItemView.java
+++ b/src/com/android/launcher3/popup/PopupItemView.java
@@ -32,7 +32,7 @@
 
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
-import com.android.launcher3.popup.PopupContainerWithArrow.RoundedCornerFlags;
+import com.android.launcher3.popup.BaseActionPopup.RoundedCornerFlags;
 
 import static com.android.launcher3.popup.PopupContainerWithArrow.ROUNDED_BOTTOM_CORNERS;
 import static com.android.launcher3.popup.PopupContainerWithArrow.ROUNDED_TOP_CORNERS;