Merge "Moved hide keyboard to its own method" into ub-launcher3-calgary
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 6897269..6bfd069 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -164,7 +164,9 @@
          of the shortcut container before the container is removed. -->
     <dimen name="deep_shortcuts_start_drag_threshold">35dp</dimen>
     <dimen name="deep_shortcut_icon_size">36dp</dimen>
-    <dimen name="deep_shortcuts_arrow_horizontal_offset">19dp</dimen>
     <dimen name="deep_shortcut_anim_translation_y">5dp</dimen>
+    <dimen name="deep_shortcuts_arrow_width">10dp</dimen>
+    <dimen name="deep_shortcuts_arrow_height">8dp</dimen>
+    <dimen name="deep_shortcuts_arrow_vertical_offset">-2dp</dimen>
 
 </resources>
diff --git a/src/com/android/launcher3/InstallShortcutReceiver.java b/src/com/android/launcher3/InstallShortcutReceiver.java
index 921e90c..df87cc2 100644
--- a/src/com/android/launcher3/InstallShortcutReceiver.java
+++ b/src/com/android/launcher3/InstallShortcutReceiver.java
@@ -348,7 +348,7 @@
 
         public ShortcutInfo getShortcutInfo() {
             if (activityInfo != null) {
-                return ShortcutInfo.fromActivityInfo(activityInfo, mContext);
+                return new ShortcutInfo(activityInfo, mContext);
             } else {
                 return LauncherAppState.getInstance().getModel().infoFromShortcutIntent(mContext, data);
             }
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index ddd60c8..4561111 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -1957,8 +1957,7 @@
                                         }
                                     }
                                     incrementPinnedShortcutCount(key, shouldPin);
-                                    info = ShortcutInfo.fromDeepShortcutInfo(
-                                            pinnedShortcut, context);
+                                    info = new ShortcutInfo(pinnedShortcut, context);
                                 } else { // item type == ITEM_TYPE_SHORTCUT
                                     info = getShortcutInfo(c, context, titleIndex, cursorIconInfo);
 
@@ -3291,6 +3290,22 @@
         }
     }
 
+    /**
+     * Repopulates the shortcut info, possibly updating any icon already on the workspace.
+     */
+    public void updateShortcutInfo(final ShortcutInfoCompat fullDetail, final ShortcutInfo info) {
+        enqueueItemUpdatedTask(new Runnable() {
+            @Override
+            public void run() {
+                info.updateFromDeepShortcutInfo(
+                        fullDetail, LauncherAppState.getInstance().getContext());
+                ArrayList<ShortcutInfo> update = new ArrayList<ShortcutInfo>();
+                update.add(info);
+                bindUpdatedShortcuts(update, fullDetail.getUserHandle());
+            }
+        });
+    }
+
     private class ShortcutsChangedTask implements Runnable {
         private String mPackageName;
         private List<ShortcutInfoCompat> mShortcuts;
diff --git a/src/com/android/launcher3/ShortcutInfo.java b/src/com/android/launcher3/ShortcutInfo.java
index a9f73c2..0cc5a1b 100644
--- a/src/com/android/launcher3/ShortcutInfo.java
+++ b/src/com/android/launcher3/ShortcutInfo.java
@@ -162,19 +162,17 @@
         this.user = user;
     }
 
-    public ShortcutInfo(Context context, ShortcutInfo info) {
+    public ShortcutInfo(ShortcutInfo info) {
         super(info);
-        title = Utilities.trim(info.title);
+        title = info.title;
         intent = new Intent(info.intent);
-        if (info.iconResource != null) {
-            iconResource = new Intent.ShortcutIconResource();
-            iconResource.packageName = info.iconResource.packageName;
-            iconResource.resourceName = info.iconResource.resourceName;
-        }
+        iconResource = info.iconResource;
         mIcon = info.mIcon; // TODO: should make a copy here.  maybe we don't need this ctor at all
         flags = info.flags;
-        user = info.user;
         status = info.status;
+        mInstallProgress = info.mInstallProgress;
+        isDisabled = info.isDisabled;
+        usingFallbackIcon = info.usingFallbackIcon;
     }
 
     /** TODO: Remove this.  It's only called by ApplicationInfo.makeShortcut. */
@@ -186,6 +184,28 @@
         isDisabled = info.isDisabled;
     }
 
+    public ShortcutInfo(LauncherActivityInfoCompat info, Context context) {
+        user = info.getUser();
+        title = Utilities.trim(info.getLabel());
+        contentDescription = UserManagerCompat.getInstance(context)
+                .getBadgedLabelForUser(info.getLabel(), info.getUser());
+        intent = AppInfo.makeLaunchIntent(context, info, info.getUser());
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+        flags = AppInfo.initFlags(info);
+    }
+
+    /**
+     * Creates a {@link ShortcutInfo} from a {@link ShortcutInfoCompat}.
+     */
+    @TargetApi(Build.VERSION_CODES.N)
+    public ShortcutInfo(ShortcutInfoCompat shortcutInfo, Context context) {
+        user = shortcutInfo.getUserHandle();
+        itemType = LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
+        intent = shortcutInfo.makeIntent(context);
+        flags = 0;
+        updateFromDeepShortcutInfo(shortcutInfo, context);
+    }
+
     public void setIcon(Bitmap b) {
         mIcon = b;
     }
@@ -265,33 +285,6 @@
         return usingLowResIcon && container >= 0 && rank >= FolderIcon.NUM_ITEMS_IN_PREVIEW;
     }
 
-    public static ShortcutInfo fromActivityInfo(LauncherActivityInfoCompat info, Context context) {
-        final ShortcutInfo shortcut = new ShortcutInfo();
-        shortcut.user = info.getUser();
-        shortcut.title = Utilities.trim(info.getLabel());
-        shortcut.contentDescription = UserManagerCompat.getInstance(context)
-                .getBadgedLabelForUser(info.getLabel(), info.getUser());
-        shortcut.intent = AppInfo.makeLaunchIntent(context, info, info.getUser());
-        shortcut.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
-        shortcut.flags = AppInfo.initFlags(info);
-        return shortcut;
-    }
-
-    /**
-     * Creates a {@link ShortcutInfo} from a {@link ShortcutInfoCompat}. Pardon the overloaded name.
-     */
-    @TargetApi(Build.VERSION_CODES.N)
-    public static ShortcutInfo fromDeepShortcutInfo(ShortcutInfoCompat shortcutInfo,
-            Context context) {
-        ShortcutInfo si = new ShortcutInfo();
-        si.user = shortcutInfo.getUserHandle();
-        si.itemType = LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
-        si.intent = shortcutInfo.makeIntent(context);
-        si.flags = 0;
-        si.updateFromDeepShortcutInfo(shortcutInfo, context);
-        return si;
-    }
-
     public void updateFromDeepShortcutInfo(ShortcutInfoCompat shortcutInfo, Context context) {
         title = shortcutInfo.getShortLabel();
 
@@ -306,11 +299,14 @@
         Drawable unbadgedIcon = launcherAppState.getShortcutManager()
                 .getShortcutIconDrawable(shortcutInfo,
                         launcherAppState.getInvariantDeviceProfile().fillResIconDpi);
-        Bitmap icon = unbadgedIcon == null ? null
-                : Utilities.createBadgedIconBitmapWithShadow(unbadgedIcon, user, context);
+        Bitmap icon = unbadgedIcon == null ? null : getBadgedIcon(unbadgedIcon, context);
         setIcon(icon != null ? icon : launcherAppState.getIconCache().getDefaultIcon(user));
     }
 
+    protected Bitmap getBadgedIcon(Drawable unbadgedIcon, Context context) {
+        return Utilities.createBadgedIconBitmapWithShadow(unbadgedIcon, user, context);
+    }
+
     /** Returns the ShortcutInfo id associated with the deep shortcut. */
     public String getDeepShortcutId() {
         return itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT ?
@@ -322,4 +318,3 @@
         return isDisabled != 0;
     }
 }
-
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index ef78195..f09b7cc 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -264,18 +264,25 @@
     }
 
     /**
+     * Creates a normalized bitmap suitable for the all apps view. The bitmap is also visually
+     * normalized with other icons and has enough spacing to add shadow.
+     */
+    public static Bitmap createScaledBitmapWithoutShadow(Drawable icon, Context context) {
+        RectF iconBounds = new RectF();
+        float scale = FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ?
+                1 : IconNormalizer.getInstance().getScale(icon, iconBounds);
+        scale = Math.min(scale, ShadowGenerator.getScaleForBounds(iconBounds));
+        return createIconBitmap(icon, context, scale);
+    }
+
+    /**
      * Same as {@link #createBadgedIconBitmap} but adds a shadow before badging the icon
      */
     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
     public static Bitmap createBadgedIconBitmapWithShadow(
             Drawable icon, UserHandleCompat user, Context context) {
-        RectF iconBounds = new RectF();
-        float scale = FeatureFlags.LAUNCHER3_DISABLE_ICON_NORMALIZATION ?
-                1 : IconNormalizer.getInstance().getScale(icon, iconBounds);
-        scale = Math.min(scale, ShadowGenerator.getScaleForBounds(iconBounds));
-
-        Bitmap bitmap = createIconBitmap(icon, context, scale);
-        bitmap = ShadowGenerator.getInstance().recreateIcon(bitmap);
+        Bitmap bitmap = ShadowGenerator.getInstance().recreateIcon(
+                createScaledBitmapWithoutShadow(icon, context));
         if (Utilities.ATLEAST_LOLLIPOP && user != null
                 && !UserHandleCompat.myUserHandle().equals(user)) {
             BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 946c306..4ed2467 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2301,11 +2301,19 @@
     }
 
     public void beginDragShared(View child, DragSource source, boolean accessible) {
-        beginDragShared(child, new Point(), source, accessible, new DragPreviewProvider(child));
+        Object dragObject = child.getTag();
+        if (!(dragObject instanceof ItemInfo)) {
+            String msg = "Drag started with a view that has no tag set. This "
+                    + "will cause a crash (issue 11627249) down the line. "
+                    + "View: " + child + "  tag: " + child.getTag();
+            throw new IllegalStateException(msg);
+        }
+        beginDragShared(child, new Point(), source, accessible,
+                (ItemInfo) dragObject, new DragPreviewProvider(child));
     }
 
     public void beginDragShared(View child, Point relativeTouchPos, DragSource source,
-                                boolean accessible, DragPreviewProvider previewProvider) {
+            boolean accessible, ItemInfo dragObject, DragPreviewProvider previewProvider) {
         child.clearFocus();
         child.setPressed(false);
 
@@ -2362,20 +2370,12 @@
             icon.clearPressedBackground();
         }
 
-        Object dragObject = child.getTag();
-        if (!(dragObject instanceof ItemInfo)) {
-            String msg = "Drag started with a view that has no tag set. This "
-                    + "will cause a crash (issue 11627249) down the line. "
-                    + "View: " + child + "  tag: " + child.getTag();
-            throw new IllegalStateException(msg);
-        }
-
         if (child.getParent() instanceof ShortcutAndWidgetContainer) {
             mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
         }
 
         DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source,
-                (ItemInfo) dragObject, DragController.DRAG_ACTION_MOVE, dragVisualizeOffset,
+                dragObject, DragController.DRAG_ACTION_MOVE, dragVisualizeOffset,
                 dragRect, scale, accessible);
         dv.setIntrinsicIconScaleFactor(source.getIntrinsicIconScaleFactor());
 
diff --git a/src/com/android/launcher3/graphics/TriangleShape.java b/src/com/android/launcher3/graphics/TriangleShape.java
new file mode 100644
index 0000000..cce4e3c
--- /dev/null
+++ b/src/com/android/launcher3/graphics/TriangleShape.java
@@ -0,0 +1,56 @@
+/*
+ * 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.graphics;
+
+import android.graphics.Outline;
+import android.graphics.Path;
+import android.graphics.drawable.shapes.PathShape;
+import android.support.annotation.NonNull;
+
+/**
+ * Wrapper around {@link android.graphics.drawable.shapes.PathShape}
+ * that creates a shape with a triangular path (pointing up or down).
+ */
+public class TriangleShape extends PathShape {
+    private Path mTriangularPath;
+
+    public TriangleShape(Path path, float stdWidth, float stdHeight) {
+        super(path, stdWidth, stdHeight);
+        mTriangularPath = path;
+    }
+
+    public static TriangleShape create(float width, float height, boolean isPointingUp) {
+        Path triangularPath = new Path();
+        if (isPointingUp) {
+            triangularPath.moveTo(0, height);
+            triangularPath.lineTo(width, height);
+            triangularPath.lineTo(width / 2, 0);
+            triangularPath.close();
+        } else {
+            triangularPath.moveTo(0, 0);
+            triangularPath.lineTo(width / 2, height);
+            triangularPath.lineTo(width, 0);
+            triangularPath.close();
+        }
+        return new TriangleShape(triangularPath, width, height);
+    }
+
+    @Override
+    public void getOutline(@NonNull Outline outline) {
+        outline.setConvexPath(mTriangularPath);
+    }
+}
diff --git a/src/com/android/launcher3/pageindicators/PageIndicatorLineCaret.java b/src/com/android/launcher3/pageindicators/PageIndicatorLineCaret.java
index 0b94791..0d771ad 100644
--- a/src/com/android/launcher3/pageindicators/PageIndicatorLineCaret.java
+++ b/src/com/android/launcher3/pageindicators/PageIndicatorLineCaret.java
@@ -9,14 +9,12 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
-import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
 import android.support.v4.graphics.ColorUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Property;
-import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewConfiguration;
 
@@ -24,6 +22,7 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.dynamicui.ExtractedColors;
+import com.android.launcher3.util.TransformingTouchDelegate;
 
 /**
  * A PageIndicator that briefly shows a fraction of a line when moving between pages.
@@ -61,7 +60,7 @@
     private Paint mLinePaint;
     private Launcher mLauncher;
     private final int mLineHeight;
-    private final Rect mTouchHitRect = new Rect();
+    private final TransformingTouchDelegate mTouchDelegate;
     private final int mTouchExtensionHeight;
     private final int mCaretSizePx;
     private final int mCaretWorkspaceOffsetPx;
@@ -142,6 +141,13 @@
         mLineHeight = res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_line_height);
         mTouchExtensionHeight = res.getDimensionPixelSize(
                 R.dimen.dynamic_grid_page_indicator_extra_touch_height);
+        mTouchDelegate = new TransformingTouchDelegate(this);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        mLauncher.getDragLayer().setTouchDelegate(mTouchDelegate);
     }
 
     @Override
@@ -157,9 +163,8 @@
         View parent = mLauncher.getDragLayer();
         sTempCoords[0] = sTempCoords[1] = 0;
         Utilities.getDescendantCoordRelativeToAncestor(this, parent, sTempCoords, true);
-        mTouchHitRect.set(sTempCoords[0], sTempCoords[1], sTempCoords[0] + this.getWidth(),
+        mTouchDelegate.setBounds(sTempCoords[0], sTempCoords[1], sTempCoords[0] + this.getWidth(),
                 sTempCoords[1] + getHeight() + mTouchExtensionHeight);
-        parent.setTouchDelegate(new TouchDelegate(mTouchHitRect, this));
     }
 
     @Override
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index f9dd336..7cb2d43 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -23,7 +23,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.support.annotation.IntDef;
 import android.util.AttributeSet;
 import android.view.animation.DecelerateInterpolator;
 import android.widget.FrameLayout;
@@ -40,26 +39,8 @@
  */
 public class DeepShortcutView extends FrameLayout {
 
-    private static final float HOVER_SCALE = 1.05f;
-
-    // The direction this view should translate when animating the hover state.
-    // This allows hovered shortcuts to "push" other shortcuts away.
-    @IntDef({DIRECTION_UP, DIRECTION_NONE, DIRECTION_DOWN})
-    public @interface TranslationDirection {}
-
-    public static final int DIRECTION_UP = -1;
-    public static final int DIRECTION_NONE = 0;
-    public static final int DIRECTION_DOWN = 1;
-
-    @TranslationDirection
-    private int mTranslationDirection = DIRECTION_NONE;
-
-    private int mSpacing;
     private int mRadius;
     private Rect mPillRect;
-    private int mTop;
-    private boolean mIsHoveringOver = false;
-    private LauncherViewPropertyAnimator mHoverAnimator;
 
     private BubbleTextView mBubbleText;
 
@@ -74,10 +55,8 @@
     public DeepShortcutView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
-        mSpacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
         mRadius = getResources().getDimensionPixelSize(R.dimen.bg_pill_radius);
         mPillRect = new Rect();
-        mHoverAnimator = new LauncherViewPropertyAnimator(this);
     }
 
     @Override
@@ -110,11 +89,8 @@
 
     /**
      * Creates an animator to play when the shortcut container is being opened.
-     *
-     * @param animationIndex The index at which this animation will be started
-     *                       relative to other DeepShortcutView open animations.
      */
-    public Animator createOpenAnimation(int animationIndex, boolean isContainerAboveIcon) {
+    public Animator createOpenAnimation(long animationDelay, boolean isContainerAboveIcon) {
         final Resources res = getResources();
         setVisibility(INVISIBLE);
 
@@ -140,59 +116,9 @@
                 .scaleX(1).scaleY(1);
 
         openAnimation.playTogether(reveal, translationY, scale);
-        openAnimation.setStartDelay(animationIndex * res.getInteger(
-                R.integer.config_deepShortcutOpenStagger));
+        openAnimation.setStartDelay(animationDelay);
         openAnimation.setDuration(res.getInteger(R.integer.config_deepShortcutOpenDuration));
         openAnimation.setInterpolator(new DecelerateInterpolator());
         return openAnimation;
     }
-
-    /**
-     * Updates the state of this view based on touches over the container before user lifts finger.
-     *
-     * @param containerContainsTouch whether the {@link DeepShortcutsContainer} this shortcut
-     *                               is inside contains the current touch
-     * @param isBelowHoveredShortcut whether a sibling shortcut before this one in the
-     *                               view hierarchy is being hovered over
-     * @param touchY the y coordinate of the touch, relative to the {@link DeepShortcutsContainer}
-     *               this shortcut is inside
-     * @return whether this shortcut is being hovered over
-     */
-    public boolean updateHoverState(boolean containerContainsTouch, boolean isBelowHoveredShortcut,
-            float touchY) {
-        if (!containerContainsTouch) {
-            mIsHoveringOver = false;
-            mTranslationDirection = DIRECTION_NONE;
-        } else if (isBelowHoveredShortcut) {
-            mIsHoveringOver = false;
-            mTranslationDirection = DIRECTION_DOWN;
-        } else {
-            // Include space around the view when determining hover state to avoid gaps.
-            mTop = (int) (getY() - getTranslationY());
-            mIsHoveringOver = (touchY >= mTop - mSpacing / 2)
-                    && (touchY < mTop + getHeight() + mSpacing / 2);
-            mTranslationDirection = mIsHoveringOver ? DIRECTION_NONE : DIRECTION_UP;
-        }
-        animateHoverState();
-        return mIsHoveringOver;
-    }
-
-    /**
-     * If this shortcut is being hovered over, we scale it up. If another shortcut is being hovered
-     * over, we translate this one away from it to account for its increased size.
-     */
-    private void animateHoverState() {
-        if (mHoverAnimator.isRunning()) {
-            return;
-        }
-        float scale = mIsHoveringOver ? HOVER_SCALE : 1f;
-        float translateY = (HOVER_SCALE - 1f) * getHeight() * mTranslationDirection;
-        mHoverAnimator.scaleX(scale).scaleY(scale).translationY(translateY)
-                .setDuration(getResources().getInteger(R.integer.config_deepShortcutHoverDuration))
-                .start();
-    }
-
-    public boolean isHoveringOver() {
-        return mIsHoveringOver;
-    }
 }
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
index 92afeb9..c881c8c 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
@@ -17,19 +17,23 @@
 package com.android.launcher3.shortcuts;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.annotation.TargetApi;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -45,6 +49,7 @@
 import com.android.launcher3.LauncherAnimUtils;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherViewPropertyAnimator;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
@@ -55,6 +60,7 @@
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.graphics.ScaledPreviewProvider;
+import com.android.launcher3.graphics.TriangleShape;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
@@ -89,7 +95,7 @@
     private Point mIconLastTouchPos = new Point();
     private boolean mIsLeftAligned;
     private boolean mIsAboveIcon;
-    private boolean mIsAnimatingOpen;
+    private View mArrow;
 
     private boolean mSrcIconDragStarted;
 
@@ -168,8 +174,8 @@
                 Collections.sort(shortcuts, shortcutsComparator);
                 for (int i = 0; i < shortcuts.size(); i++) {
                     final ShortcutInfoCompat shortcut = shortcuts.get(i);
-                    final ShortcutInfo launcherShortcutInfo = ShortcutInfo
-                            .fromDeepShortcutInfo(shortcut, mLauncher);
+                    final ShortcutInfo launcherShortcutInfo =
+                            new UnbadgedShortcutInfo(shortcut, mLauncher);
                     CharSequence shortLabel = shortcut.getShortLabel();
                     CharSequence longLabel = shortcut.getLongLabel();
                     uiHandler.post(new UpdateShortcutChild(i, launcherShortcutInfo,
@@ -212,34 +218,56 @@
     }
 
     private DeepShortcutView getShortcutAt(int index) {
+        if (!mIsAboveIcon) {
+            // Opening down, so arrow is the first view.
+            index++;
+        }
         return (DeepShortcutView) getChildAt(index);
     }
 
+    private int getShortcutCount() {
+        // All children except the arrow are shortcuts.
+        return getChildCount() - 1;
+    }
+
     private void animateOpen(BubbleTextView originalIcon) {
         orientAboutIcon(originalIcon);
+        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);
+        int iconWidth = originalIcon.getWidth() - originalIcon.getTotalPaddingLeft()
+                - originalIcon.getTotalPaddingRight();
+        iconWidth *= originalIcon.getScaleX();
+        final int arrowHorizontalOffset = iconWidth / 2 - arrowWidth / 2;
+        final int arrowVerticalOffset = resources.getDimensionPixelSize(
+                R.dimen.deep_shortcuts_arrow_vertical_offset);
+        mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
 
         setVisibility(View.VISIBLE);
 
         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
-        final int numShortcuts = getChildCount();
-        final int arrowOffset = getResources().getDimensionPixelSize(
-                R.dimen.deep_shortcuts_arrow_horizontal_offset);
-        final int pivotX = mIsLeftAligned ? arrowOffset : getMeasuredWidth() - arrowOffset;
+        final int shortcutCount = getShortcutCount();
+        final int pivotX = mIsLeftAligned ? arrowHorizontalOffset
+                : getMeasuredWidth() - arrowHorizontalOffset;
         final int pivotY = getShortcutAt(0).getMeasuredHeight() / 2;
-        for (int i = 0; i < numShortcuts; i++) {
+        for (int i = 0; i < shortcutCount; i++) {
             DeepShortcutView deepShortcutView = getShortcutAt(i);
             deepShortcutView.setPivotX(pivotX);
             deepShortcutView.setPivotY(pivotY);
-            int animationIndex = mIsAboveIcon ? numShortcuts - i - 1 : i;
-            shortcutAnims.play(deepShortcutView.createOpenAnimation(animationIndex, mIsAboveIcon));
+            int animationIndex = mIsAboveIcon ? shortcutCount - i - 1 : i;
+            long animationDelay = animationIndex * getResources().getInteger(
+                    R.integer.config_deepShortcutOpenStagger);
+            shortcutAnims.play(deepShortcutView.createOpenAnimation(animationDelay, mIsAboveIcon));
         }
-        shortcutAnims.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationEnd(Animator animation) {
-                mIsAnimatingOpen = false;
-            }
-        });
-        mIsAnimatingOpen = true;
+        mArrow.setScaleX(0);
+        mArrow.setScaleY(0);
+        final long shortcutAnimDuration = shortcutAnims.getChildAnimations().get(0).getDuration();
+        final long arrowScaleDelay = shortcutAnimDuration / 6;
+        final long arrowScaleDuration = shortcutAnimDuration - arrowScaleDelay;
+        Animator arrowScale = new LauncherViewPropertyAnimator(mArrow).scaleX(1).scaleY(1);
+        arrowScale.setStartDelay(arrowScaleDelay);
+        arrowScale.setDuration(arrowScaleDuration);
+        shortcutAnims.play(arrowScale);
         shortcutAnims.start();
     }
 
@@ -254,8 +282,6 @@
      *
      * So we always align left if there is enough horizontal space
      * and align above if there is enough vertical space.
-     *
-     * TODO: draw arrow based on orientation.
      */
     private void orientAboutIcon(BubbleTextView icon) {
         int width = getMeasuredWidth();
@@ -294,6 +320,36 @@
         setY(y);
     }
 
+    /**
+     * 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) {
+        LinearLayout.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());
+        ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create(
+                width, height, !mIsAboveIcon));
+        arrowDrawable.getPaint().setColor(Color.WHITE);
+        arrowView.setBackground(arrowDrawable);
+        arrowView.setElevation(getElevation());
+        addView(arrowView, mIsAboveIcon ? getChildCount() : 0, layoutParams);
+        return arrowView;
+    }
+
     private void deferDrag(BubbleTextView originalIcon) {
         mDeferredDragIcon = originalIcon;
         showDragView(originalIcon);
@@ -348,7 +404,7 @@
         Utilities.translateEventCoordinates(this, mLauncher.getDragLayer(), ev);
         final int dragLayerX = (int) ev.getX();
         final int dragLayerY = (int) ev.getY();
-        int childCount = getChildCount();
+        int shortcutCount = getShortcutCount();
         if (action == MotionEvent.ACTION_MOVE) {
             if (mLastX != 0 || mLastY != 0) {
                 mDistanceDragged += Math.hypot(mLastX - x, mLastY - y);
@@ -356,48 +412,24 @@
             mLastX = x;
             mLastY = y;
 
-            boolean containerContainsTouch = x >= 0 && y >= 0 && x < getWidth() && y < getHeight();
-            if (shouldStartDeferredDrag((int) x, (int) y, containerContainsTouch)) {
+            if (shouldStartDeferredDrag((int) x, (int) y)) {
+            DeepShortcutView topShortcut = getShortcutAt(0);
+            DeepShortcutView bottomShortcut = getShortcutAt(shortcutCount - 1);
                 mSrcIconDragStarted = true;
                 cleanupDeferredDrag(true);
                 mDeferredDragIcon.getParent().requestDisallowInterceptTouchEvent(false);
                 mDeferredDragIcon.getOnLongClickListener().onLongClick(mDeferredDragIcon);
                 mLauncher.getDragController().onTouchEvent(ev);
                 return true;
-            } else {
-                // Determine whether touch is over a shortcut.
-                boolean hoveringOverShortcut = false;
-                for (int i = 0; i < childCount && !mIsAnimatingOpen; i++) {
-                    DeepShortcutView shortcut = getShortcutAt(i);
-                    if (shortcut.updateHoverState(containerContainsTouch, hoveringOverShortcut, y)) {
-                        hoveringOverShortcut = true;
-                    }
-                }
-
-                if (!hoveringOverShortcut && mDistanceDragged > mDragDeadzone) {
-                    // After dragging further than a small deadzone,
-                    // have the drag view follow the user's finger.
-                    mDragView.setVisibility(VISIBLE);
-                    mDragView.move(dragLayerX, dragLayerY);
-                    mDeferredDragIcon.setVisibility(INVISIBLE);
-                } else if (hoveringOverShortcut) {
-                    // Jump drag view back to original place on grid,
-                    // so user doesn't think they are still dragging.
-                    // TODO: can we improve this interaction? maybe with a ghost icon or similar?
-                    mDragView.setVisibility(INVISIBLE);
-                    mDeferredDragIcon.setVisibility(VISIBLE);
-                }
+            } else if (mDistanceDragged > mDragDeadzone) {
+                // After dragging further than a small deadzone,
+                // have the drag view follow the user's finger.
+                mDragView.setVisibility(VISIBLE);
+                mDragView.move(dragLayerX, dragLayerY);
+                mDeferredDragIcon.setVisibility(INVISIBLE);
             }
         } else if (action == MotionEvent.ACTION_UP) {
             cleanupDeferredDrag(true);
-            // Launch a shortcut if user was hovering over it.
-            for (int i = 0; i < childCount; i++) {
-                DeepShortcutView shortcut = getShortcutAt(i);
-                if (shortcut.isHoveringOver()) {
-                    shortcut.getBubbleText().performClick();
-                    break;
-                }
-            }
         } else if (action == MotionEvent.ACTION_CANCEL) {
             // Do not change the source icon visibility if we are already dragging the source icon.
             cleanupDeferredDrag(!mSrcIconDragStarted);
@@ -410,19 +442,14 @@
      * relative to the original icon and the shortcuts container.
      *
      * Current behavior:
-     * - Compute distance from original touch down to closest container edge.
-     * - Compute distance from latest touch (given x and y) and compare to original distance;
-     *   if the new distance is larger than a threshold, the deferred drag should start.
-     * - Never defer the drag if this container contains the touch.
+     * - Start the drag if the touch passes a certain distance from the original touch down.
      *
      * @param x the x touch coordinate relative to this container
      * @param y the y touch coordinate relative to this container
      */
-    private boolean shouldStartDeferredDrag(int x, int y, boolean containerContainsTouch) {
-        int closestEdgeY = mIsAboveIcon ? getMeasuredHeight() : 0;
-        double distToEdge = Math.abs(mTouchDown[1] - closestEdgeY);
-        double newDistToEdge = Math.hypot(x - mTouchDown[0], y - closestEdgeY);
-        return !containerContainsTouch && (newDistToEdge - distToEdge > mStartDragThreshold);
+    private boolean shouldStartDeferredDrag(int x, int y) {
+        double distFromTouchDown = Math.hypot(x - mTouchDown[0], y - mTouchDown[1]);
+        return distFromTouchDown > mStartDragThreshold;
     }
 
     public void cleanupDeferredDrag(boolean updateSrcVisibility) {
@@ -452,8 +479,14 @@
         // Return if global dragging is not enabled
         if (!mLauncher.isDraggingEnabled()) return false;
 
+        UnbadgedShortcutInfo unbadgedInfo = (UnbadgedShortcutInfo) v.getTag();
+        ShortcutInfo badged = new ShortcutInfo(unbadgedInfo);
+        // Queue an update task on the worker thread. This ensures that the badged
+        // shortcut eventually gets its icon updated.
+        mLauncher.getModel().updateShortcutInfo(unbadgedInfo.mDetail, badged);
+
         // Long clicked on a shortcut.
-        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false,
+        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false, badged,
                 new ScaledPreviewProvider(v));
         // TODO: support dragging from within folder without having to close it
         mLauncher.closeFolder();
@@ -536,4 +569,21 @@
         }
         return null;
     }
+
+    /**
+     * Extension of {@link ShortcutInfo} which does not badge the icons.
+     */
+    private static class UnbadgedShortcutInfo extends ShortcutInfo {
+        private final ShortcutInfoCompat mDetail;
+
+        public UnbadgedShortcutInfo(ShortcutInfoCompat shortcutInfo, Context context) {
+            super(shortcutInfo, context);
+            mDetail = shortcutInfo;
+        }
+
+        @Override
+        protected Bitmap getBadgedIcon(Drawable unbadgedIcon, Context context) {
+            return Utilities.createScaledBitmapWithoutShadow(unbadgedIcon, context);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
index f94595b..507939a 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutsContainerListener.java
@@ -4,7 +4,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
-import android.view.ViewParent;
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CheckLongPressHelper;
@@ -22,18 +21,12 @@
     /** Scaled touch slop, used for detecting movement outside bounds. */
     private final float mScaledTouchSlop;
 
-    /** Timeout before disallowing intercept on the source's parent. */
-    private final int mTapTimeout;
-
     /** Timeout before accepting a long-press to start forwarding. */
     private final int mLongPressTimeout;
 
     /** Source view from which events are forwarded. */
     private final BubbleTextView mSrcIcon;
 
-    /** Runnable used to prevent conflicts with scrolling parents. */
-    private Runnable mDisallowIntercept;
-
     /** Runnable used to trigger forwarding on long-press. */
     private Runnable mTriggerLongPress;
 
@@ -56,7 +49,6 @@
     public ShortcutsContainerListener(BubbleTextView icon) {
         mSrcIcon = icon;
         mScaledTouchSlop = ViewConfiguration.get(icon.getContext()).getScaledTouchSlop();
-        mTapTimeout = ViewConfiguration.getTapTimeout();
 
         mLongPressTimeout = CheckLongPressHelper.DEFAULT_LONG_PRESS_TIMEOUT;
 
@@ -113,10 +105,6 @@
     public void onViewDetachedFromWindow(View v) {
         mForwarding = false;
         mActivePointerId = MotionEvent.INVALID_POINTER_ID;
-
-        if (mDisallowIntercept != null) {
-            mSrcIcon.removeCallbacks(mDisallowIntercept);
-        }
     }
 
     /**
@@ -158,11 +146,6 @@
             case MotionEvent.ACTION_DOWN:
                 mActivePointerId = srcEvent.getPointerId(0);
 
-                if (mDisallowIntercept == null) {
-                    mDisallowIntercept = new DisallowIntercept();
-                }
-                src.postDelayed(mDisallowIntercept, mTapTimeout);
-
                 if (mTriggerLongPress == null) {
                     mTriggerLongPress = new TriggerLongPress();
                 }
@@ -195,10 +178,6 @@
         if (mTriggerLongPress != null) {
             mSrcIcon.removeCallbacks(mTriggerLongPress);
         }
-
-        if (mDisallowIntercept != null) {
-            mSrcIcon.removeCallbacks(mDisallowIntercept);
-        }
     }
 
     private void onLongPress() {
@@ -265,14 +244,6 @@
         return handled && keepForwarding;
     }
 
-    private class DisallowIntercept implements Runnable {
-        @Override
-        public void run() {
-            final ViewParent parent = mSrcIcon.getParent();
-            parent.requestDisallowInterceptTouchEvent(true);
-        }
-    }
-
     private class TriggerLongPress implements Runnable {
         @Override
         public void run() {
diff --git a/src/com/android/launcher3/util/ManagedProfileHeuristic.java b/src/com/android/launcher3/util/ManagedProfileHeuristic.java
index 7dbc0e7a..6661429 100644
--- a/src/com/android/launcher3/util/ManagedProfileHeuristic.java
+++ b/src/com/android/launcher3/util/ManagedProfileHeuristic.java
@@ -111,7 +111,7 @@
             for (int i = 0; i < count; i++) {
                 LauncherActivityInstallInfo info = apps.get(i);
 
-                ShortcutInfo si = ShortcutInfo.fromActivityInfo(info.info, mContext);
+                ShortcutInfo si = new ShortcutInfo(info.info, mContext);
                 ((info.installTime <= folderCreationTime) ? workFolderApps : homescreenApps).add(si);
             }
 
diff --git a/src/com/android/launcher3/util/TransformingTouchDelegate.java b/src/com/android/launcher3/util/TransformingTouchDelegate.java
new file mode 100644
index 0000000..da1de5e
--- /dev/null
+++ b/src/com/android/launcher3/util/TransformingTouchDelegate.java
@@ -0,0 +1,87 @@
+/*
+ * 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.util;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+
+/**
+ * This class differs from the framework {@link TouchDelegate} in that it transforms the
+ * coordinates of the motion event to the provided bounds.
+ *
+ * You can also modify the bounds post construction. Since the bounds are available during layout,
+ * this avoids new object creation during every layout.
+ */
+public class TransformingTouchDelegate extends TouchDelegate {
+    private static final Rect sTempRect = new Rect();
+
+    private final RectF mBounds;
+
+    private View mDelegateView;
+    private boolean mDelegateTargeted;
+
+    public TransformingTouchDelegate(View delegateView) {
+        super(sTempRect, delegateView);
+
+        mDelegateView = delegateView;
+        mBounds = new RectF();
+    }
+
+    public void setBounds(int left, int top, int right, int bottom) {
+        mBounds.set(left, top, right, bottom);
+    }
+
+    public void setDelegateView(View view) {
+        mDelegateView = view;
+    }
+
+    /**
+     * Will forward touch events to the delegate view if the event is within the bounds
+     * specified in the constructor.
+     *
+     * @param event The touch event to forward
+     * @return True if the event was forwarded to the delegate, false otherwise.
+     */
+    public boolean onTouchEvent(MotionEvent event) {
+        boolean sendToDelegate = false;
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                mDelegateTargeted = mBounds.contains(event.getX(), event.getY());
+                if (mDelegateTargeted) {
+                    sendToDelegate = true;
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                sendToDelegate = mDelegateTargeted;
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                sendToDelegate = mDelegateTargeted;
+                mDelegateTargeted = false;
+                break;
+        }
+        boolean handled = false;
+        if (sendToDelegate) {
+            event.offsetLocation(-mBounds.left, -mBounds.top);
+            handled = mDelegateView.dispatchTouchEvent(event);
+            event.offsetLocation(mBounds.left, mBounds.top);
+        }
+        return handled;
+    }
+}