Add swipe-to-dismiss notifications in popup menu.

- Next secondary icon animates up to replace dismissed main notification
- Add padding around main notification so it always aligns with the
  straight edges of the view (not the rounded corners); looks more
  dismissable
- Notification view collapses as notifications are dismissed
- To mimic system notification behavior, we copy SwipeHelper,
  FlingAnimationUtils, and Interpolators. We also apply elevation
  to notifications and reveal a darker color beneath when dismissing.

Bug: 32410600
Change-Id: I9fbf10e73bb4996f17ef061c856efb013967d972
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ed8b531..2efe31f 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -44,6 +44,7 @@
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.HolographicOutlineHelper;
+import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.model.PackageItemInfo;
 
 import java.text.NumberFormat;
@@ -514,6 +515,11 @@
         }
     }
 
+    public IconPalette getIconPalette() {
+        return mIcon instanceof FastBitmapDrawable ? ((FastBitmapDrawable) mIcon).getIconPalette()
+                : null;
+    }
+
     private Theme getPreloaderTheme() {
         Object tag = getTag();
         int style = ((tag != null) && (tag instanceof ShortcutInfo) &&
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index 587d445..df19547 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -129,10 +129,7 @@
         mBadgeInfo = badgeInfo;
         mBadgeRenderer = badgeRenderer;
         if (wasBadged || isBadged) {
-            if (mBadgeInfo != null && mIconPalette == null) {
-                mIconPalette = IconPalette.fromDominantColor(Utilities
-                        .findDominantColorByHue(mBitmap, 20));
-            }
+            mIconPalette = getIconPalette();
             invalidateSelf();
         }
     }
@@ -161,6 +158,14 @@
         }
     }
 
+    public IconPalette getIconPalette() {
+        if (mIconPalette == null) {
+            mIconPalette = IconPalette.fromDominantColor(Utilities
+                    .findDominantColorByHue(mBitmap, 20));
+        }
+        return mIconPalette;
+    }
+
     private boolean hasBadge() {
         return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
     }
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index e2108a7..69b305f 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -85,7 +85,7 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DefaultAppSearchController;
 import com.android.launcher3.anim.AnimationLayerSet;
-import com.android.launcher3.badge.NotificationListener;
+import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java
index 01e73d4..9ea277c 100644
--- a/src/com/android/launcher3/LauncherAnimUtils.java
+++ b/src/com/android/launcher3/LauncherAnimUtils.java
@@ -23,7 +23,9 @@
 import android.animation.ValueAnimator;
 import android.util.Property;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.widget.ViewAnimator;
 
 import java.util.HashSet;
 import java.util.WeakHashMap;
@@ -127,4 +129,18 @@
         new FirstFrameAnimatorHelper(anim, view);
         return anim;
     }
+
+    public static ValueAnimator animateViewHeight(final View v, int fromHeight, int toHeight) {
+        ValueAnimator anim = ValueAnimator.ofInt(fromHeight, toHeight);
+        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                int val = (Integer) valueAnimator.getAnimatedValue();
+                ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
+                layoutParams.height = val;
+                v.setLayoutParams(layoutParams);
+            }
+        });
+        return anim;
+    }
 }
diff --git a/src/com/android/launcher3/badge/BadgeInfo.java b/src/com/android/launcher3/badge/BadgeInfo.java
index 4255c51..673c297 100644
--- a/src/com/android/launcher3/badge/BadgeInfo.java
+++ b/src/com/android/launcher3/badge/BadgeInfo.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher3.badge;
 
+import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.HashSet;
diff --git a/src/com/android/launcher3/graphics/IconPalette.java b/src/com/android/launcher3/graphics/IconPalette.java
index dcc5fcb..58ad449 100644
--- a/src/com/android/launcher3/graphics/IconPalette.java
+++ b/src/com/android/launcher3/graphics/IconPalette.java
@@ -26,16 +26,18 @@
 
     public int backgroundColor;
     public int textColor;
+    public int secondaryColor;
 
     public static IconPalette fromDominantColor(int dominantColor) {
         IconPalette palette = new IconPalette();
         palette.backgroundColor = getMutedColor(dominantColor);
         palette.textColor = getTextColorForBackground(palette.backgroundColor);
+        palette.secondaryColor = getLowContrastColor(palette.backgroundColor);
         return palette;
     }
 
     private static int getMutedColor(int color) {
-        int alpha = (int) (255 * 0.2f);
+        int alpha = (int) (255 * 0.15f);
         return ColorUtils.compositeColors(ColorUtils.setAlphaComponent(color, alpha), Color.WHITE);
     }
 
diff --git a/src/com/android/launcher3/notification/FlingAnimationUtils.java b/src/com/android/launcher3/notification/FlingAnimationUtils.java
new file mode 100644
index 0000000..3e954d0
--- /dev/null
+++ b/src/com/android/launcher3/notification/FlingAnimationUtils.java
@@ -0,0 +1,354 @@
+/*
+ * 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.notification;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Utility class to calculate general fling animation when the finger is released.
+ */
+public class FlingAnimationUtils {
+
+    private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
+    private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f;
+    private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
+    private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
+    private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
+    private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
+    private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
+
+    private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f;
+    private final float mSpeedUpFactor;
+    private final float mY2;
+
+    private float mMinVelocityPxPerSecond;
+    private float mMaxLengthSeconds;
+    private float mHighVelocityPxPerSecond;
+    private float mLinearOutSlowInX2;
+
+    private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
+    private PathInterpolator mInterpolator;
+    private float mCachedStartGradient = -1;
+    private float mCachedVelocityFactor = -1;
+
+    public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
+        this(ctx, maxLengthSeconds, 0.0f);
+    }
+
+    /**
+     * @param maxLengthSeconds the longest duration an animation can become in seconds
+     * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+     *                      the end of the animation. 0 means it's at the beginning and no
+     *                      acceleration will take place.
+     */
+    public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) {
+        this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
+    }
+
+    /**
+     * @param maxLengthSeconds the longest duration an animation can become in seconds
+     * @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
+     *                      the end of the animation. 0 means it's at the beginning and no
+     *                      acceleration will take place.
+     * @param x2 the x value to take for the second point of the bezier spline. If a value below 0
+     *           is provided, the value is automatically calculated.
+     * @param y2 the y value to take for the second point of the bezier spline
+     */
+    public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2,
+            float y2) {
+        mMaxLengthSeconds = maxLengthSeconds;
+        mSpeedUpFactor = speedUpFactor;
+        if (x2 < 0) {
+            mLinearOutSlowInX2 = interpolate(LINEAR_OUT_SLOW_IN_X2,
+                    LINEAR_OUT_SLOW_IN_X2_MAX,
+                    mSpeedUpFactor);
+        } else {
+            mLinearOutSlowInX2 = x2;
+        }
+        mY2 = y2;
+
+        mMinVelocityPxPerSecond
+                = MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+        mHighVelocityPxPerSecond
+                = HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+    }
+
+    private static float interpolate(float start, float end, float amount) {
+        return start * (1.0f - amount) + end * amount;
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     */
+    public void apply(Animator animator, float currValue, float endValue, float velocity) {
+        apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     */
+    public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+            float velocity) {
+        apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void apply(Animator animator, float currValue, float endValue, float velocity,
+            float maxDistance) {
+        AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void apply(ViewPropertyAnimator animator, float currValue, float endValue,
+            float velocity, float maxDistance) {
+        AnimatorProperties properties = getProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    private AnimatorProperties getProperties(float currValue,
+            float endValue, float velocity, float maxDistance) {
+        float maxLengthSeconds = (float) (mMaxLengthSeconds
+                * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
+        float diff = Math.abs(endValue - currValue);
+        float velAbs = Math.abs(velocity);
+        float velocityFactor = mSpeedUpFactor == 0.0f
+                ? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
+        float startGradient = interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
+                mY2 / mLinearOutSlowInX2, velocityFactor);
+        float durationSeconds = startGradient * diff / velAbs;
+        Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
+        if (durationSeconds <= maxLengthSeconds) {
+            mAnimatorProperties.interpolator = slowInInterpolator;
+        } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+            // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
+            durationSeconds = maxLengthSeconds;
+            VelocityInterpolator velocityInterpolator
+                    = new VelocityInterpolator(durationSeconds, velAbs, diff);
+            InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+                    velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
+            mAnimatorProperties.interpolator = superInterpolator;
+        } else {
+
+            // Just use a normal interpolator which doesn't take the velocity into account.
+            durationSeconds = maxLengthSeconds;
+            mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
+        }
+        mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+        return mAnimatorProperties;
+    }
+
+    private Interpolator getInterpolator(float startGradient, float velocityFactor) {
+        if (startGradient != mCachedStartGradient
+                || velocityFactor != mCachedVelocityFactor) {
+            float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
+            mInterpolator = new PathInterpolator(speedup,
+                    speedup * startGradient,
+                    mLinearOutSlowInX2, mY2);
+            mCachedStartGradient = startGradient;
+            mCachedVelocityFactor = velocityFactor;
+        }
+        return mInterpolator;
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion for the case when the animation is making something
+     * disappear.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void applyDismissing(Animator animator, float currValue, float endValue,
+            float velocity, float maxDistance) {
+        AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    /**
+     * Applies the interpolator and length to the animator, such that the fling animation is
+     * consistent with the finger motion for the case when the animation is making something
+     * disappear.
+     *
+     * @param animator the animator to apply
+     * @param currValue the current value
+     * @param endValue the end value of the animator
+     * @param velocity the current velocity of the motion
+     * @param maxDistance the maximum distance for this interaction; the maximum animation length
+     *                    gets multiplied by the ratio between the actual distance and this value
+     */
+    public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue,
+            float velocity, float maxDistance) {
+        AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity,
+                maxDistance);
+        animator.setDuration(properties.duration);
+        animator.setInterpolator(properties.interpolator);
+    }
+
+    private AnimatorProperties getDismissingProperties(float currValue, float endValue,
+            float velocity, float maxDistance) {
+        float maxLengthSeconds = (float) (mMaxLengthSeconds
+                * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
+        float diff = Math.abs(endValue - currValue);
+        float velAbs = Math.abs(velocity);
+        float y2 = calculateLinearOutFasterInY2(velAbs);
+
+        float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
+        Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
+        float durationSeconds = startGradient * diff / velAbs;
+        if (durationSeconds <= maxLengthSeconds) {
+            mAnimatorProperties.interpolator = mLinearOutFasterIn;
+        } else if (velAbs >= mMinVelocityPxPerSecond) {
+
+            // Cross fade between linear-out-faster-in and linear interpolator with current
+            // velocity.
+            durationSeconds = maxLengthSeconds;
+            VelocityInterpolator velocityInterpolator
+                    = new VelocityInterpolator(durationSeconds, velAbs, diff);
+            InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
+                    velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
+            mAnimatorProperties.interpolator = superInterpolator;
+        } else {
+
+            // Just use a normal interpolator which doesn't take the velocity into account.
+            durationSeconds = maxLengthSeconds;
+            mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+        }
+        mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+        return mAnimatorProperties;
+    }
+
+    /**
+     * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
+     * velocity. The faster the velocity, the more "linear" the interpolator gets.
+     *
+     * @param velocity the velocity of the gesture.
+     * @return the y2 control point for a cubic bezier path interpolator
+     */
+    private float calculateLinearOutFasterInY2(float velocity) {
+        float t = (velocity - mMinVelocityPxPerSecond)
+                / (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
+        t = Math.max(0, Math.min(1, t));
+        return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
+    }
+
+    /**
+     * @return the minimum velocity a gesture needs to have to be considered a fling
+     */
+    public float getMinVelocityPxPerSecond() {
+        return mMinVelocityPxPerSecond;
+    }
+
+    /**
+     * An interpolator which interpolates two interpolators with an interpolator.
+     */
+    private static final class InterpolatorInterpolator implements Interpolator {
+
+        private Interpolator mInterpolator1;
+        private Interpolator mInterpolator2;
+        private Interpolator mCrossfader;
+
+        InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
+                Interpolator crossfader) {
+            mInterpolator1 = interpolator1;
+            mInterpolator2 = interpolator2;
+            mCrossfader = crossfader;
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            float t = mCrossfader.getInterpolation(input);
+            return (1 - t) * mInterpolator1.getInterpolation(input)
+                    + t * mInterpolator2.getInterpolation(input);
+        }
+    }
+
+    /**
+     * An interpolator which interpolates with a fixed velocity.
+     */
+    private static final class VelocityInterpolator implements Interpolator {
+
+        private float mDurationSeconds;
+        private float mVelocity;
+        private float mDiff;
+
+        private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
+            mDurationSeconds = durationSeconds;
+            mVelocity = velocity;
+            mDiff = diff;
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            float time = input * mDurationSeconds;
+            return time * mVelocity / mDiff;
+        }
+    }
+
+    private static class AnimatorProperties {
+        Interpolator interpolator;
+        long duration;
+    }
+
+}
diff --git a/src/com/android/launcher3/notification/Interpolators.java b/src/com/android/launcher3/notification/Interpolators.java
new file mode 100644
index 0000000..664b313
--- /dev/null
+++ b/src/com/android/launcher3/notification/Interpolators.java
@@ -0,0 +1,35 @@
+/*
+ * 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.notification;
+
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Utility class to receive interpolators from
+ */
+public class Interpolators {
+    public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+    public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+    public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+
+    /**
+     * Interpolator to be used when animating a move based on a click. Pair with enough duration.
+     */
+    public static final Interpolator TOUCH_RESPONSE =
+            new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+}
diff --git a/src/com/android/launcher3/notification/NotificationFooterLayout.java b/src/com/android/launcher3/notification/NotificationFooterLayout.java
new file mode 100644
index 0000000..2965e4a
--- /dev/null
+++ b/src/com/android/launcher3/notification/NotificationFooterLayout.java
@@ -0,0 +1,244 @@
+/*
+ * 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.LauncherViewPropertyAnimator;
+import com.android.launcher3.R;
+import com.android.launcher3.graphics.IconPalette;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A {@link LinearLayout} that contains icons of notifications. If there is only one icon,
+ * we also supply the notification text/secondary text like we do for the main notification.
+ * If there are more than {@link #MAX_FOOTER_NOTIFICATIONS} icons, we add a "+x" overflow.
+ */
+public class NotificationFooterLayout extends LinearLayout {
+
+    public interface IconAnimationEndListener {
+        void onIconAnimationEnd(NotificationInfo animatedNotification);
+    }
+
+    private static final int MAX_FOOTER_NOTIFICATIONS = 5;
+
+    private static final Rect sTempRect = new Rect();
+
+    private final List<NotificationInfo> mNotifications = new ArrayList<>();
+    private final List<NotificationInfo> mOverflowNotifications = new ArrayList<>();
+    private final Map<View, NotificationInfo> mViewsToInfos = new HashMap<>();
+
+    LinearLayout.LayoutParams mIconLayoutParams;
+    private LinearLayout mIconRow;
+    private int mTextColor;
+
+    public NotificationFooterLayout(Context context) {
+        this(context, null, 0);
+    }
+
+    public NotificationFooterLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationFooterLayout(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        int size = getResources().getDimensionPixelSize(
+                R.dimen.notification_footer_icon_size);
+        int padding = getResources().getDimensionPixelSize(
+                R.dimen.deep_shortcut_drawable_padding);
+        mIconLayoutParams = new LayoutParams(size, size);
+        mIconLayoutParams.setMarginStart(padding);
+        mIconLayoutParams.gravity = Gravity.CENTER_VERTICAL;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mIconRow = (LinearLayout) findViewById(R.id.icon_row);
+    }
+
+    public void applyColors(IconPalette iconPalette) {
+        setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor));
+        findViewById(R.id.divider).setBackgroundColor(iconPalette.secondaryColor);
+        mTextColor = iconPalette.textColor;
+    }
+
+    /**
+     * Keep track of the NotificationInfo, and then update the UI when
+     * {@link #commitNotificationInfos()} is called.
+     */
+    public void addNotificationInfo(final NotificationInfo notificationInfo) {
+        if (mNotifications.size() < MAX_FOOTER_NOTIFICATIONS) {
+            mNotifications.add(notificationInfo);
+        } else {
+            mOverflowNotifications.add(notificationInfo);
+        }
+    }
+
+    /**
+     * Adds icons and potentially overflow text for all of the NotificationInfo's
+     * added using {@link #addNotificationInfo(NotificationInfo)}.
+     */
+    public void commitNotificationInfos() {
+        mIconRow.removeAllViews();
+        mViewsToInfos.clear();
+
+        for (int i = 0; i < mNotifications.size(); i++) {
+            NotificationInfo info = mNotifications.get(i);
+            addNotificationIconForInfo(info, false /* fromOverflow */);
+        }
+
+        if (!mOverflowNotifications.isEmpty()) {
+            TextView overflowText = new TextView(getContext());
+            overflowText.setTextColor(mTextColor);
+            updateOverflowText(overflowText);
+            mIconRow.addView(overflowText, mIconLayoutParams);
+        }
+    }
+
+    private void addNotificationIconForInfo(NotificationInfo info, boolean fromOverflow) {
+        View icon = new View(getContext());
+        icon.setBackground(info.iconDrawable);
+        icon.setOnClickListener(info);
+        int addIndex = mIconRow.getChildCount();
+        if (fromOverflow) {
+            // Add the notification before the overflow view.
+            addIndex--;
+            icon.setAlpha(0);
+            icon.animate().alpha(1);
+        }
+        mIconRow.addView(icon, addIndex, mIconLayoutParams);
+        mViewsToInfos.put(icon, info);
+    }
+
+    private void updateOverflowText(TextView overflowTextView) {
+        overflowTextView.setText(getResources().getString(R.string.deep_notifications_overflow,
+                mOverflowNotifications.size()));
+    }
+
+    public void animateFirstNotificationTo(Rect toBounds,
+            final IconAnimationEndListener callback) {
+        AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
+        final View firstNotification = mIconRow.getChildAt(0);
+
+        Rect fromBounds = sTempRect;
+        firstNotification.getGlobalVisibleRect(fromBounds);
+        float scale = (float) toBounds.height() / fromBounds.height();
+        Animator moveAndScaleIcon = new LauncherViewPropertyAnimator(firstNotification)
+                .translationY(toBounds.top - fromBounds.top
+                        + (fromBounds.height() * scale - fromBounds.height()) / 2)
+                .scaleX(scale).scaleY(scale);
+        moveAndScaleIcon.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                callback.onIconAnimationEnd(mViewsToInfos.get(firstNotification));
+            }
+        });
+        animation.play(moveAndScaleIcon);
+
+        // Shift all notifications (not the overflow) over to fill the gap.
+        int gapWidth = mIconLayoutParams.width + mIconLayoutParams.getMarginStart();
+        int numIcons = mIconRow.getChildCount()
+                - (mOverflowNotifications.isEmpty() ? 0 : 1);
+        for (int i = 1; i < numIcons; i++) {
+            final View child = mIconRow.getChildAt(i);
+            Animator shiftChild = new LauncherViewPropertyAnimator(child).translationX(-gapWidth);
+            shiftChild.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    // We have to set the translation X to 0 when the new main notification
+                    // is removed from the footer.
+                    // TODO: remove it here instead of expecting trimNotifications to do so.
+                    child.setTranslationX(0);
+                }
+            });
+            animation.play(shiftChild);
+        }
+        animation.start();
+    }
+
+    public void trimNotifications(Set<String> notifications) {
+        if (!isAttachedToWindow() || mIconRow.getChildCount() == 0) {
+            return;
+        }
+        Iterator<NotificationInfo> overflowIterator = mOverflowNotifications.iterator();
+        while (overflowIterator.hasNext()) {
+            if (!notifications.contains(overflowIterator.next().notificationKey)) {
+                overflowIterator.remove();
+            }
+        }
+        TextView overflowView = null;
+        for (int i = mIconRow.getChildCount() - 1; i >= 0; i--) {
+            View child = mIconRow.getChildAt(i);
+            if (child instanceof TextView) {
+                overflowView = (TextView) child;
+            } else {
+                NotificationInfo childInfo = mViewsToInfos.get(child);
+                if (!notifications.contains(childInfo.notificationKey)) {
+                    mIconRow.removeView(child);
+                    mNotifications.remove(childInfo);
+                    mViewsToInfos.remove(child);
+                    if (!mOverflowNotifications.isEmpty()) {
+                        NotificationInfo notification = mOverflowNotifications.remove(0);
+                        mNotifications.add(notification);
+                        addNotificationIconForInfo(notification, true /* fromOverflow */);
+                    }
+                }
+            }
+        }
+        if (overflowView != null) {
+            if (mOverflowNotifications.isEmpty()) {
+                mIconRow.removeView(overflowView);
+            } else {
+                updateOverflowText(overflowView);
+            }
+        }
+        if (mIconRow.getChildCount() == 0) {
+            // There are no more icons in the secondary view, so hide it.
+            PopupContainerWithArrow popup = PopupContainerWithArrow.getOpen(
+                    Launcher.getLauncher(getContext()));
+            int newHeight = getResources().getDimensionPixelSize(
+                    R.dimen.notification_footer_collapsed_height);
+            AnimatorSet collapseSecondary = LauncherAnimUtils.createAnimatorSet();
+            collapseSecondary.play(popup.animateTranslationYBy(getHeight() - newHeight,
+                    getResources().getInteger(R.integer.config_removeNotificationViewDuration)));
+            collapseSecondary.play(LauncherAnimUtils.animateViewHeight(
+                    this, getHeight(), newHeight));
+            collapseSecondary.start();
+        }
+    }
+}
diff --git a/src/com/android/launcher3/badge/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
similarity index 74%
rename from src/com/android/launcher3/badge/NotificationInfo.java
rename to src/com/android/launcher3/notification/NotificationInfo.java
index 51f6a4f..33ab966 100644
--- a/src/com/android/launcher3/badge/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.badge;
+package com.android.launcher3.notification;
 
 import android.app.Notification;
 import android.app.PendingIntent;
@@ -44,26 +44,28 @@
     public final Drawable iconDrawable;
     public final PendingIntent intent;
     public final boolean autoCancel;
+    public final boolean dismissable;
 
     /**
      * Extracts the data that we need from the StatusBarNotification.
      */
-    public NotificationInfo(Context context, StatusBarNotification notification) {
-        packageUserKey = PackageUserKey.fromNotification(notification);
-        notificationKey = notification.getKey();
-        title = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TITLE);
-        text = notification.getNotification().extras.getCharSequence(Notification.EXTRA_TEXT);
-        Icon icon = notification.getNotification().getLargeIcon();
+    public NotificationInfo(Context context, StatusBarNotification statusBarNotification) {
+        packageUserKey = PackageUserKey.fromNotification(statusBarNotification);
+        notificationKey = statusBarNotification.getKey();
+        Notification notification = statusBarNotification.getNotification();
+        title = notification.extras.getCharSequence(Notification.EXTRA_TITLE);
+        text = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
+        Icon icon = notification.getLargeIcon();
         if (icon == null) {
-            icon = notification.getNotification().getSmallIcon();
+            icon = notification.getSmallIcon();
             iconDrawable = icon.loadDrawable(context);
-            iconDrawable.setTint(notification.getNotification().color);
+            iconDrawable.setTint(statusBarNotification.getNotification().color);
         } else {
             iconDrawable = icon.loadDrawable(context);
         }
-        intent = notification.getNotification().contentIntent;
-        autoCancel = (notification.getNotification().flags
-                & Notification.FLAG_AUTO_CANCEL) != 0;
+        intent = notification.contentIntent;
+        autoCancel = (notification.flags & Notification.FLAG_AUTO_CANCEL) != 0;
+        dismissable = (notification.flags & Notification.FLAG_ONGOING_EVENT) == 0;
     }
 
     @Override
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
new file mode 100644
index 0000000..f388382
--- /dev/null
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -0,0 +1,173 @@
+/*
+ * 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.LinearInterpolator;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.graphics.IconPalette;
+import com.android.launcher3.popup.PopupItemView;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static com.android.launcher3.LauncherAnimUtils.animateViewHeight;
+
+/**
+ * A {@link FrameLayout} that contains a header, main view and a footer.
+ * The main view contains the icon and text (title + subtext) of the first notification.
+ * The footer contains: A list of just the icons of all the notifications past the first one.
+ * @see NotificationFooterLayout
+ */
+public class NotificationItemView extends PopupItemView {
+
+    private static final Rect sTempRect = new Rect();
+
+    private TextView mHeader;
+    private View mDivider;
+    private NotificationMainView mMainView;
+    private NotificationFooterLayout mFooter;
+    private SwipeHelper mSwipeHelper;
+    private boolean mAnimatingNextIcon;
+    private IconPalette mIconPalette;
+
+    public NotificationItemView(Context context) {
+        this(context, null, 0);
+    }
+
+    public NotificationItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mHeader = (TextView) findViewById(R.id.header);
+        mDivider = findViewById(R.id.divider);
+        mMainView = (NotificationMainView) findViewById(R.id.main_view);
+        mFooter = (NotificationFooterLayout) findViewById(R.id.footer);
+        mSwipeHelper = new SwipeHelper(SwipeHelper.X, mMainView, getContext());
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        getParent().requestDisallowInterceptTouchEvent(true);
+        return mSwipeHelper.onInterceptTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
+    }
+
+    @Override
+    protected ColorStateList getAttachedArrowColor() {
+        // This NotificationView itself has a different color that is only
+        // revealed when dismissing notifications.
+        return mFooter.getBackgroundTintList();
+    }
+
+    public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
+        if (notificationInfos.isEmpty()) {
+            return;
+        }
+
+        NotificationInfo mainNotification = notificationInfos.get(0);
+        mMainView.applyNotificationInfo(mainNotification, mIconView);
+
+        for (int i = 1; i < notificationInfos.size(); i++) {
+            mFooter.addNotificationInfo(notificationInfos.get(i));
+        }
+        mFooter.commitNotificationInfos();
+    }
+
+    public void applyColors(IconPalette iconPalette) {
+        mIconPalette = iconPalette;
+        setBackgroundTintList(ColorStateList.valueOf(iconPalette.secondaryColor));
+        mHeader.setBackgroundTintList(ColorStateList.valueOf(iconPalette.backgroundColor));
+        mHeader.setTextColor(ColorStateList.valueOf(iconPalette.textColor));
+        mDivider.setBackgroundColor(iconPalette.secondaryColor);
+        mMainView.setBackgroundColor(iconPalette.backgroundColor);
+        mFooter.applyColors(iconPalette);
+    }
+
+    public void trimNotifications(final Set<String> notificationKeys) {
+        boolean dismissedMainNotification = !notificationKeys.contains(
+                mMainView.getNotificationInfo().notificationKey);
+        if (dismissedMainNotification && !mAnimatingNextIcon) {
+            // Animate the next icon into place as the new main notification.
+            mAnimatingNextIcon = true;
+            mMainView.setVisibility(INVISIBLE);
+            mMainView.setTranslationX(0);
+            mIconView.getGlobalVisibleRect(sTempRect);
+            mFooter.animateFirstNotificationTo(sTempRect,
+                    new NotificationFooterLayout.IconAnimationEndListener() {
+                @Override
+                public void onIconAnimationEnd(NotificationInfo newMainNotification) {
+                    if (newMainNotification != null) {
+                        mMainView.applyNotificationInfo(newMainNotification, mIconView, mIconPalette);
+                        Set<String> footerNotificationKeys = new HashSet<>(notificationKeys);
+                        footerNotificationKeys.remove(newMainNotification.notificationKey);
+                        mFooter.trimNotifications(footerNotificationKeys);
+                        mMainView.setVisibility(VISIBLE);
+                    }
+                    mAnimatingNextIcon = false;
+                }
+            });
+        } else {
+            mFooter.trimNotifications(notificationKeys);
+        }
+    }
+
+    public Animator createRemovalAnimation(int fullDuration) {
+        AnimatorSet animation = new AnimatorSet();
+        int mainHeight = mMainView.getMeasuredHeight();
+        Animator removeMainView = animateViewHeight(mMainView, mainHeight, 0);
+        removeMainView.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                // Remove the remaining views but take on their color instead of the darker one.
+                setBackgroundTintList(mHeader.getBackgroundTintList());
+                removeAllViews();
+            }
+        });
+        Animator removeRest = LauncherAnimUtils.animateViewHeight(this, getHeight() - mainHeight, 0);
+        removeMainView.setDuration(fullDuration / 2);
+        removeRest.setDuration(fullDuration / 2);
+        removeMainView.setInterpolator(new LinearInterpolator());
+        removeRest.setInterpolator(new LinearInterpolator());
+        animation.playSequentially(removeMainView, removeRest);
+        return animation;
+    }
+}
diff --git a/src/com/android/launcher3/badge/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
similarity index 98%
rename from src/com/android/launcher3/badge/NotificationListener.java
rename to src/com/android/launcher3/notification/NotificationListener.java
index 1668a62..3f9a584 100644
--- a/src/com/android/launcher3/badge/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.launcher3.badge;
+package com.android.launcher3.notification;
 
 import android.app.Notification;
 import android.os.Handler;
@@ -24,6 +24,7 @@
 import android.service.notification.StatusBarNotification;
 import android.support.annotation.Nullable;
 import android.support.v4.util.Pair;
+import android.util.Log;
 
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.config.FeatureFlags;
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
new file mode 100644
index 0000000..2997d40
--- /dev/null
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -0,0 +1,156 @@
+/*
+ * 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.LauncherViewPropertyAnimator;
+import com.android.launcher3.R;
+import com.android.launcher3.graphics.IconPalette;
+
+/**
+ * A {@link LinearLayout} that contains a single notification, e.g. icon + title + text.
+ */
+public class NotificationMainView extends LinearLayout implements SwipeHelper.Callback {
+
+    private NotificationInfo mNotificationInfo;
+    private TextView mTitleView;
+    private TextView mTextView;
+
+    public NotificationMainView(Context context) {
+        this(context, null, 0);
+    }
+
+    public NotificationMainView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NotificationMainView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mTitleView = (TextView) findViewById(R.id.title);
+        mTextView = (TextView) findViewById(R.id.text);
+    }
+
+    public void applyNotificationInfo(NotificationInfo mainNotification, View iconView) {
+        applyNotificationInfo(mainNotification, iconView, null);
+    }
+
+    /**
+     * @param iconPalette if not null, indicates that the new info should be animated in,
+     *                    and that part of this animation includes animating the background
+     *                    from iconPalette.secondaryColor to iconPalette.backgroundColor.
+     */
+    public void applyNotificationInfo(NotificationInfo mainNotification, View iconView,
+            @Nullable IconPalette iconPalette) {
+        boolean animate = iconPalette != null;
+        if (animate) {
+            mTitleView.setAlpha(0);
+            mTextView.setAlpha(0);
+            setBackgroundColor(iconPalette.secondaryColor);
+        }
+        mNotificationInfo = mainNotification;
+        mTitleView.setText(mNotificationInfo.title);
+        mTextView.setText(mNotificationInfo.text);
+        iconView.setBackground(mNotificationInfo.iconDrawable);
+        setOnClickListener(mNotificationInfo);
+        setTranslationX(0);
+        if (animate) {
+            AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
+            Animator textFade = new LauncherViewPropertyAnimator(mTextView).alpha(1);
+            Animator titleFade = new LauncherViewPropertyAnimator(mTitleView).alpha(1);
+            ValueAnimator colorChange = ValueAnimator.ofArgb(iconPalette.secondaryColor,
+                    iconPalette.backgroundColor);
+            colorChange.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                @Override
+                public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                    setBackgroundColor((Integer) valueAnimator.getAnimatedValue());
+                }
+            });
+            animation.playTogether(textFade, titleFade, colorChange);
+            animation.setDuration(150);
+            animation.start();
+        }
+    }
+
+    public NotificationInfo getNotificationInfo() {
+        return mNotificationInfo;
+    }
+
+
+    // SwipeHelper.Callback's
+
+    @Override
+    public View getChildAtPosition(MotionEvent ev) {
+        return this;
+    }
+
+    @Override
+    public boolean canChildBeDismissed(View v) {
+        return mNotificationInfo.dismissable;
+    }
+
+    @Override
+    public boolean isAntiFalsingNeeded() {
+        return false;
+    }
+
+    @Override
+    public void onBeginDrag(View v) {
+    }
+
+    @Override
+    public void onChildDismissed(View v) {
+        Launcher.getLauncher(getContext()).getPopupDataProvider().cancelNotification(
+                mNotificationInfo.notificationKey);
+    }
+
+    @Override
+    public void onDragCancelled(View v) {
+    }
+
+    @Override
+    public void onChildSnappedBack(View animView, float targetLeft) {
+    }
+
+    @Override
+    public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
+        // Don't fade out.
+        return true;
+    }
+
+    @Override
+    public float getFalsingThresholdFactor() {
+        return 1;
+    }
+}
diff --git a/src/com/android/launcher3/notification/SwipeHelper.java b/src/com/android/launcher3/notification/SwipeHelper.java
new file mode 100644
index 0000000..3678174
--- /dev/null
+++ b/src/com/android/launcher3/notification/SwipeHelper.java
@@ -0,0 +1,687 @@
+/*
+ * 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.notification;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.launcher3.R;
+
+import java.util.HashMap;
+
+public class SwipeHelper {
+    static final String TAG = "SwipeHelper";
+    private static final boolean DEBUG = false;
+    private static final boolean DEBUG_INVALIDATE = false;
+    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
+    private static final boolean CONSTRAIN_SWIPE = true;
+    private static final boolean FADE_OUT_DURING_SWIPE = true;
+    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
+
+    public static final int X = 0;
+    public static final int Y = 1;
+
+    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
+    private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
+    private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
+    private int MAX_DISMISS_VELOCITY = 4000; // dp/sec
+    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
+
+    static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width
+                                                // beyond which swipe progress->0
+    private float mMinSwipeProgress = 0f;
+    private float mMaxSwipeProgress = 1f;
+
+    private FlingAnimationUtils mFlingAnimationUtils;
+    private float mPagingTouchSlop;
+    private Callback mCallback;
+    private Handler mHandler;
+    private int mSwipeDirection;
+    private VelocityTracker mVelocityTracker;
+
+    private float mInitialTouchPos;
+    private float mPerpendicularInitialTouchPos;
+    private boolean mDragging;
+    private boolean mSnappingChild;
+    private View mCurrView;
+    private boolean mCanCurrViewBeDimissed;
+    private float mDensityScale;
+    private float mTranslation = 0;
+
+    private boolean mLongPressSent;
+    private LongPressListener mLongPressListener;
+    private Runnable mWatchLongPress;
+    private long mLongPressTimeout;
+
+    final private int[] mTmpPos = new int[2];
+    private int mFalsingThreshold;
+    private boolean mTouchAboveFalsingThreshold;
+    private boolean mDisableHwLayers;
+
+    private HashMap<View, Animator> mDismissPendingMap = new HashMap<>();
+
+    public SwipeHelper(int swipeDirection, Callback callback, Context context) {
+        mCallback = callback;
+        mHandler = new Handler();
+        mSwipeDirection = swipeDirection;
+        mVelocityTracker = VelocityTracker.obtain();
+        mDensityScale =  context.getResources().getDisplayMetrics().density;
+        mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
+
+        mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
+        mFalsingThreshold = context.getResources().getDimensionPixelSize(
+                R.dimen.swipe_helper_falsing_threshold);
+        mFlingAnimationUtils = new FlingAnimationUtils(context, getMaxEscapeAnimDuration() / 1000f);
+    }
+
+    public void setLongPressListener(LongPressListener listener) {
+        mLongPressListener = listener;
+    }
+
+    public void setDensityScale(float densityScale) {
+        mDensityScale = densityScale;
+    }
+
+    public void setPagingTouchSlop(float pagingTouchSlop) {
+        mPagingTouchSlop = pagingTouchSlop;
+    }
+
+    public void setDisableHardwareLayers(boolean disableHwLayers) {
+        mDisableHwLayers = disableHwLayers;
+    }
+
+    private float getPos(MotionEvent ev) {
+        return mSwipeDirection == X ? ev.getX() : ev.getY();
+    }
+
+    private float getPerpendicularPos(MotionEvent ev) {
+        return mSwipeDirection == X ? ev.getY() : ev.getX();
+    }
+
+    protected float getTranslation(View v) {
+        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
+    }
+
+    private float getVelocity(VelocityTracker vt) {
+        return mSwipeDirection == X ? vt.getXVelocity() :
+                vt.getYVelocity();
+    }
+
+    protected ObjectAnimator createTranslationAnimation(View v, float newPos) {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
+                mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos);
+        return anim;
+    }
+
+    private float getPerpendicularVelocity(VelocityTracker vt) {
+        return mSwipeDirection == X ? vt.getYVelocity() :
+                vt.getXVelocity();
+    }
+
+    protected Animator getViewTranslationAnimator(View v, float target,
+            AnimatorUpdateListener listener) {
+        ObjectAnimator anim = createTranslationAnimation(v, target);
+        if (listener != null) {
+            anim.addUpdateListener(listener);
+        }
+        return anim;
+    }
+
+    protected void setTranslation(View v, float translate) {
+        if (v == null) {
+            return;
+        }
+        if (mSwipeDirection == X) {
+            v.setTranslationX(translate);
+        } else {
+            v.setTranslationY(translate);
+        }
+    }
+
+    protected float getSize(View v) {
+        return mSwipeDirection == X ? v.getMeasuredWidth() :
+                v.getMeasuredHeight();
+    }
+
+    public void setMinSwipeProgress(float minSwipeProgress) {
+        mMinSwipeProgress = minSwipeProgress;
+    }
+
+    public void setMaxSwipeProgress(float maxSwipeProgress) {
+        mMaxSwipeProgress = maxSwipeProgress;
+    }
+
+    private float getSwipeProgressForOffset(View view, float translation) {
+        float viewSize = getSize(view);
+        float result = Math.abs(translation / viewSize);
+        return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress);
+    }
+
+    private float getSwipeAlpha(float progress) {
+        return Math.min(0, Math.max(1, progress / SWIPE_PROGRESS_FADE_END));
+    }
+
+    private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
+        updateSwipeProgressFromOffset(animView, dismissable, getTranslation(animView));
+    }
+
+    private void updateSwipeProgressFromOffset(View animView, boolean dismissable,
+            float translation) {
+        float swipeProgress = getSwipeProgressForOffset(animView, translation);
+        if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
+            if (FADE_OUT_DURING_SWIPE && dismissable) {
+                float alpha = swipeProgress;
+                if (!mDisableHwLayers) {
+                    if (alpha != 0f && alpha != 1f) {
+                        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                    } else {
+                        animView.setLayerType(View.LAYER_TYPE_NONE, null);
+                    }
+                }
+                animView.setAlpha(getSwipeAlpha(swipeProgress));
+            }
+        }
+        invalidateGlobalRegion(animView);
+    }
+
+    // invalidate the view's own bounds all the way up the view hierarchy
+    public static void invalidateGlobalRegion(View view) {
+        invalidateGlobalRegion(
+                view,
+                new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
+    }
+
+    // invalidate a rectangle relative to the view's coordinate system all the way up the view
+    // hierarchy
+    public static void invalidateGlobalRegion(View view, RectF childBounds) {
+        //childBounds.offset(view.getTranslationX(), view.getTranslationY());
+        if (DEBUG_INVALIDATE)
+            Log.v(TAG, "-------------");
+        while (view.getParent() != null && view.getParent() instanceof View) {
+            view = (View) view.getParent();
+            view.getMatrix().mapRect(childBounds);
+            view.invalidate((int) Math.floor(childBounds.left),
+                    (int) Math.floor(childBounds.top),
+                    (int) Math.ceil(childBounds.right),
+                    (int) Math.ceil(childBounds.bottom));
+            if (DEBUG_INVALIDATE) {
+                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
+                        + "," + (int) Math.floor(childBounds.top)
+                        + "," + (int) Math.ceil(childBounds.right)
+                        + "," + (int) Math.ceil(childBounds.bottom));
+            }
+        }
+    }
+
+    public void removeLongPressCallback() {
+        if (mWatchLongPress != null) {
+            mHandler.removeCallbacks(mWatchLongPress);
+            mWatchLongPress = null;
+        }
+    }
+
+    public boolean onInterceptTouchEvent(final MotionEvent ev) {
+        final int action = ev.getAction();
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mTouchAboveFalsingThreshold = false;
+                mDragging = false;
+                mSnappingChild = false;
+                mLongPressSent = false;
+                mVelocityTracker.clear();
+                mCurrView = mCallback.getChildAtPosition(ev);
+
+                if (mCurrView != null) {
+                    onDownUpdate(mCurrView);
+                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
+                    mVelocityTracker.addMovement(ev);
+                    mInitialTouchPos = getPos(ev);
+                    mPerpendicularInitialTouchPos = getPerpendicularPos(ev);
+                    mTranslation = getTranslation(mCurrView);
+                    if (mLongPressListener != null) {
+                        if (mWatchLongPress == null) {
+                            mWatchLongPress = new Runnable() {
+                                @Override
+                                public void run() {
+                                    if (mCurrView != null && !mLongPressSent) {
+                                        mLongPressSent = true;
+                                        mCurrView.sendAccessibilityEvent(
+                                                AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
+                                        mCurrView.getLocationOnScreen(mTmpPos);
+                                        final int x = (int) ev.getRawX() - mTmpPos[0];
+                                        final int y = (int) ev.getRawY() - mTmpPos[1];
+                                        mLongPressListener.onLongPress(mCurrView, x, y);
+                                    }
+                                }
+                            };
+                        }
+                        mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
+                    }
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                if (mCurrView != null && !mLongPressSent) {
+                    mVelocityTracker.addMovement(ev);
+                    float pos = getPos(ev);
+                    float perpendicularPos = getPerpendicularPos(ev);
+                    float delta = pos - mInitialTouchPos;
+                    float deltaPerpendicular = perpendicularPos - mPerpendicularInitialTouchPos;
+                    if (Math.abs(delta) > mPagingTouchSlop
+                            && Math.abs(delta) > Math.abs(deltaPerpendicular)) {
+                        mCallback.onBeginDrag(mCurrView);
+                        mDragging = true;
+                        mInitialTouchPos = getPos(ev);
+                        mTranslation = getTranslation(mCurrView);
+                        removeLongPressCallback();
+                    }
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                final boolean captured = (mDragging || mLongPressSent);
+                mDragging = false;
+                mCurrView = null;
+                mLongPressSent = false;
+                removeLongPressCallback();
+                if (captured) return true;
+                break;
+        }
+        return mDragging || mLongPressSent;
+    }
+
+    /**
+     * @param view The view to be dismissed
+     * @param velocity The desired pixels/second speed at which the view should move
+     * @param useAccelerateInterpolator Should an accelerating Interpolator be used
+     */
+    public void dismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
+        dismissChild(view, velocity, null /* endAction */, 0 /* delay */,
+                useAccelerateInterpolator, 0 /* fixedDuration */, false /* isDismissAll */);
+    }
+
+    /**
+     * @param animView The view to be dismissed
+     * @param velocity The desired pixels/second speed at which the view should move
+     * @param endAction The action to perform at the end
+     * @param delay The delay after which we should start
+     * @param useAccelerateInterpolator Should an accelerating Interpolator be used
+     * @param fixedDuration If not 0, this exact duration will be taken
+     */
+    public void dismissChild(final View animView, float velocity, final Runnable endAction,
+            long delay, boolean useAccelerateInterpolator, long fixedDuration,
+            boolean isDismissAll) {
+        final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
+        float newPos;
+        boolean isLayoutRtl = animView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+
+        // if we use the Menu to dismiss an item in landscape, animate up
+        boolean animateUpForMenu = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll)
+                && mSwipeDirection == Y;
+        // if the language is rtl we prefer swiping to the left
+        boolean animateLeftForRtl = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll)
+                && isLayoutRtl;
+        boolean animateLeft = velocity < 0
+                || (velocity == 0 && getTranslation(animView) < 0 && !isDismissAll);
+
+        if (animateLeft || animateLeftForRtl || animateUpForMenu) {
+            newPos = -getSize(animView);
+        } else {
+            newPos = getSize(animView);
+        }
+        long duration;
+        if (fixedDuration == 0) {
+            duration = MAX_ESCAPE_ANIMATION_DURATION;
+            if (velocity != 0) {
+                duration = Math.min(duration,
+                        (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
+                                .abs(velocity))
+                );
+            } else {
+                duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
+            }
+        } else {
+            duration = fixedDuration;
+        }
+
+        if (!mDisableHwLayers) {
+            animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+        }
+        AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
+            public void onAnimationUpdate(ValueAnimator animation) {
+                onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed);
+            }
+        };
+
+        Animator anim = getViewTranslationAnimator(animView, newPos, updateListener);
+        if (anim == null) {
+            return;
+        }
+        if (useAccelerateInterpolator) {
+            anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+            anim.setDuration(duration);
+        } else {
+            mFlingAnimationUtils.applyDismissing(anim, getTranslation(animView),
+                    newPos, velocity, getSize(animView));
+        }
+        if (delay > 0) {
+            anim.setStartDelay(delay);
+        }
+        anim.addListener(new AnimatorListenerAdapter() {
+            private boolean mCancelled;
+
+            public void onAnimationCancel(Animator animation) {
+                mCancelled = true;
+            }
+
+            public void onAnimationEnd(Animator animation) {
+                updateSwipeProgressFromOffset(animView, canBeDismissed);
+                mDismissPendingMap.remove(animView);
+                if (!mCancelled) {
+                    mCallback.onChildDismissed(animView);
+                }
+                if (endAction != null) {
+                    endAction.run();
+                }
+                if (!mDisableHwLayers) {
+                    animView.setLayerType(View.LAYER_TYPE_NONE, null);
+                }
+            }
+        });
+
+        prepareDismissAnimation(animView, anim);
+        mDismissPendingMap.put(animView, anim);
+        anim.start();
+    }
+
+    /**
+     * Called to update the dismiss animation.
+     */
+    protected void prepareDismissAnimation(View view, Animator anim) {
+        // Do nothing
+    }
+
+    public void snapChild(final View animView, final float targetLeft, float velocity) {
+        final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
+        AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
+            public void onAnimationUpdate(ValueAnimator animation) {
+                onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed);
+            }
+        };
+
+        Animator anim = getViewTranslationAnimator(animView, targetLeft, updateListener);
+        if (anim == null) {
+            return;
+        }
+        int duration = SNAP_ANIM_LEN;
+        anim.setDuration(duration);
+        anim.addListener(new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator animator) {
+                mSnappingChild = false;
+                updateSwipeProgressFromOffset(animView, canBeDismissed);
+                mCallback.onChildSnappedBack(animView, targetLeft);
+            }
+        });
+        prepareSnapBackAnimation(animView, anim);
+        mSnappingChild = true;
+        anim.start();
+    }
+
+    /**
+     * Called to update the snap back animation.
+     */
+    protected void prepareSnapBackAnimation(View view, Animator anim) {
+        // Do nothing
+    }
+
+    /**
+     * Called when there's a down event.
+     */
+    public void onDownUpdate(View currView) {
+        // Do nothing
+    }
+
+    /**
+     * Called on a move event.
+     */
+    protected void onMoveUpdate(View view, float totalTranslation, float delta) {
+        // Do nothing
+    }
+
+    /**
+     * Called in {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} when the current
+     * view is being animated to dismiss or snap.
+     */
+    public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) {
+        updateSwipeProgressFromOffset(animView, canBeDismissed, value);
+    }
+
+    private void snapChildInstantly(final View view) {
+        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
+        setTranslation(view, 0);
+        updateSwipeProgressFromOffset(view, canAnimViewBeDismissed);
+    }
+
+    /**
+     * Called when a view is updated to be non-dismissable, if the view was being dismissed before
+     * the update this will handle snapping it back into place.
+     *
+     * @param view the view to snap if necessary.
+     * @param animate whether to animate the snap or not.
+     * @param targetLeft the target to snap to.
+     */
+    public void snapChildIfNeeded(final View view, boolean animate, float targetLeft) {
+        if ((mDragging && mCurrView == view) || mSnappingChild) {
+            return;
+        }
+        boolean needToSnap = false;
+        Animator dismissPendingAnim = mDismissPendingMap.get(view);
+        if (dismissPendingAnim != null) {
+            needToSnap = true;
+            dismissPendingAnim.cancel();
+        } else if (getTranslation(view) != 0) {
+            needToSnap = true;
+        }
+        if (needToSnap) {
+            if (animate) {
+                snapChild(view, targetLeft, 0.0f /* velocity */);
+            } else {
+                snapChildInstantly(view);
+            }
+        }
+    }
+
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mLongPressSent) {
+            return true;
+        }
+
+        if (!mDragging) {
+            if (mCallback.getChildAtPosition(ev) != null) {
+
+                // We are dragging directly over a card, make sure that we also catch the gesture
+                // even if nobody else wants the touch event.
+                onInterceptTouchEvent(ev);
+                 return true;
+            } else {
+
+                // We are not doing anything, make sure the long press callback
+                // is not still ticking like a bomb waiting to go off.
+                removeLongPressCallback();
+                return false;
+            }
+        }
+
+        mVelocityTracker.addMovement(ev);
+        final int action = ev.getAction();
+        switch (action) {
+            case MotionEvent.ACTION_OUTSIDE:
+            case MotionEvent.ACTION_MOVE:
+                if (mCurrView != null) {
+                    float delta = getPos(ev) - mInitialTouchPos;
+                    float absDelta = Math.abs(delta);
+                    if (absDelta >= getFalsingThreshold()) {
+                        mTouchAboveFalsingThreshold = true;
+                    }
+                    // don't let items that can't be dismissed be dragged more than
+                    // maxScrollDistance
+                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
+                        float size = getSize(mCurrView);
+                        float maxScrollDistance = 0.25f * size;
+                        if (absDelta >= size) {
+                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
+                        } else {
+                            delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
+                        }
+                    }
+
+                    setTranslation(mCurrView, mTranslation + delta);
+                    updateSwipeProgressFromOffset(mCurrView, mCanCurrViewBeDimissed);
+                    onMoveUpdate(mCurrView, mTranslation + delta, delta);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                if (mCurrView == null) {
+                    break;
+                }
+                mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity());
+                float velocity = getVelocity(mVelocityTracker);
+
+                if (!handleUpEvent(ev, mCurrView, velocity, getTranslation(mCurrView))) {
+                    if (isDismissGesture(ev)) {
+                        // flingadingy
+                        dismissChild(mCurrView, velocity,
+                                !swipedFastEnough() /* useAccelerateInterpolator */);
+                    } else {
+                        // snappity
+                        mCallback.onDragCancelled(mCurrView);
+                        snapChild(mCurrView, 0 /* leftTarget */, velocity);
+                    }
+                    mCurrView = null;
+                }
+                mDragging = false;
+                break;
+        }
+        return true;
+    }
+
+    private int getFalsingThreshold() {
+        float factor = mCallback.getFalsingThresholdFactor();
+        return (int) (mFalsingThreshold * factor);
+    }
+
+    private float getMaxVelocity() {
+        return MAX_DISMISS_VELOCITY * mDensityScale;
+    }
+
+    protected float getEscapeVelocity() {
+        return getUnscaledEscapeVelocity() * mDensityScale;
+    }
+
+    protected float getUnscaledEscapeVelocity() {
+        return SWIPE_ESCAPE_VELOCITY;
+    }
+
+    protected long getMaxEscapeAnimDuration() {
+        return MAX_ESCAPE_ANIMATION_DURATION;
+    }
+
+    protected boolean swipedFarEnough() {
+        float translation = getTranslation(mCurrView);
+        return DISMISS_IF_SWIPED_FAR_ENOUGH && Math.abs(translation) > 0.4 * getSize(mCurrView);
+    }
+
+    protected boolean isDismissGesture(MotionEvent ev) {
+        boolean falsingDetected = mCallback.isAntiFalsingNeeded() && !mTouchAboveFalsingThreshold;
+        return !falsingDetected && (swipedFastEnough() || swipedFarEnough())
+                && ev.getActionMasked() == MotionEvent.ACTION_UP
+                && mCallback.canChildBeDismissed(mCurrView);
+    }
+
+    protected boolean swipedFastEnough() {
+        float velocity = getVelocity(mVelocityTracker);
+        float translation = getTranslation(mCurrView);
+        boolean ret = (Math.abs(velocity) > getEscapeVelocity())
+                && (velocity > 0) == (translation > 0);
+        return ret;
+    }
+
+    protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
+            float translation) {
+        return false;
+    }
+
+    public interface Callback {
+        View getChildAtPosition(MotionEvent ev);
+
+        boolean canChildBeDismissed(View v);
+
+        boolean isAntiFalsingNeeded();
+
+        void onBeginDrag(View v);
+
+        void onChildDismissed(View v);
+
+        void onDragCancelled(View v);
+
+        /**
+         * Called when the child is snapped to a position.
+         *
+         * @param animView the view that was snapped.
+         * @param targetLeft the left position the view was snapped to.
+         */
+        void onChildSnappedBack(View animView, float targetLeft);
+
+        /**
+         * Updates the swipe progress on a child.
+         *
+         * @return if true, prevents the default alpha fading.
+         */
+        boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
+
+        /**
+         * @return The factor the falsing threshold should be multiplied with
+         */
+        float getFalsingThresholdFactor();
+    }
+
+    /**
+     * Equivalent to View.OnLongClickListener with coordinates
+     */
+    public interface LongPressListener {
+        /**
+         * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
+         * @return whether the longpress was handled
+         */
+        boolean onLongPress(View v, int x, int y);
+    }
+}
\ 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 95d51dc..c69cf6d 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -20,10 +20,10 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Point;
@@ -33,6 +33,7 @@
 import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
+import android.support.annotation.Nullable;
 import android.util.AttributeSet;
 import android.view.Gravity;
 import android.view.LayoutInflater;
@@ -65,15 +66,18 @@
 import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.TriangleShape;
+import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
-import static com.android.launcher3.userevent.nano.LauncherLogProto.*;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.ItemType;
+import static com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 
 /**
  * A container for shortcuts to deep links within apps.
@@ -137,19 +141,22 @@
         }
         ItemInfo itemInfo = (ItemInfo) icon.getTag();
         List<String> shortcutIds = launcher.getPopupDataProvider().getShortcutIdsForItem(itemInfo);
-        if (shortcutIds.size() > 0) {
+        String[] notificationKeys = launcher.getPopupDataProvider()
+                .getNotificationKeysForItem(itemInfo);
+        if (shortcutIds.size() > 0 || notificationKeys.length > 0) {
             final PopupContainerWithArrow container =
                     (PopupContainerWithArrow) launcher.getLayoutInflater().inflate(
                             R.layout.popup_container, launcher.getDragLayer(), false);
             container.setVisibility(View.INVISIBLE);
             launcher.getDragLayer().addView(container);
-            container.populateAndShow(icon, shortcutIds);
+            container.populateAndShow(icon, shortcutIds, notificationKeys);
             return container;
         }
         return null;
     }
 
-    public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds) {
+    public void populateAndShow(final BubbleTextView originalIcon, final List<String> shortcutIds,
+            final String[] notificationKeys) {
         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);
@@ -159,8 +166,9 @@
                 R.dimen.deep_shortcuts_arrow_vertical_offset);
 
         // Add dummy views first, and populate with real info when ready.
-        PopupPopulator.Item[] itemsToPopulate = PopupPopulator.getItemsToPopulate(shortcutIds);
-        addDummyViews(originalIcon, itemsToPopulate);
+        PopupPopulator.Item[] itemsToPopulate = PopupPopulator
+                .getItemsToPopulate(shortcutIds, notificationKeys);
+        addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
 
         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
         orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
@@ -169,13 +177,14 @@
         if (reverseOrder) {
             removeAllViews();
             itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
-            addDummyViews(originalIcon, itemsToPopulate);
+            addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
 
             measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
             orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
         }
 
         List<DeepShortcutView> shortcutViews = new ArrayList<>();
+        NotificationItemView notificationView = null;
         for (int i = 0; i < getChildCount(); i++) {
             View item = getChildAt(i);
             switch (itemsToPopulate[i]) {
@@ -186,6 +195,11 @@
                         shortcutViews.add((DeepShortcutView) item);
                     }
                     break;
+                case NOTIFICATION:
+                    notificationView = (NotificationItemView) item;
+                    IconPalette iconPalette = originalIcon.getIconPalette();
+                    notificationView.applyColors(iconPalette);
+                    break;
             }
         }
 
@@ -193,6 +207,8 @@
         mArrow = addArrowView(arrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
         mArrow.setPivotX(arrowWidth / 2);
         mArrow.setPivotY(mIsAboveIcon ? 0 : arrowHeight);
+        PopupItemView firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0);
+        mArrow.setBackgroundTintList(firstItem.getAttachedArrowColor());
 
         animateOpen();
 
@@ -204,16 +220,24 @@
         final Looper workerLooper = LauncherModel.getWorkerLooper();
         new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
                 mLauncher, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()),
-                this, shortcutIds, shortcutViews));
+                this, shortcutIds, shortcutViews, notificationKeys, notificationView));
     }
 
-    private void addDummyViews(BubbleTextView originalIcon, PopupPopulator.Item[] itemsToPopulate) {
-        final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+    private void addDummyViews(BubbleTextView originalIcon,
+            PopupPopulator.Item[] itemsToPopulate, boolean secondaryNotificationViewHasIcons) {
+        final Resources res = getResources();
+        final int spacing = res.getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
         final LayoutInflater inflater = mLauncher.getLayoutInflater();
         int numItems = itemsToPopulate.length;
         for (int i = 0; i < numItems; i++) {
             final PopupItemView item = (PopupItemView) inflater.inflate(
                     itemsToPopulate[i].layoutId, this, false);
+            if (itemsToPopulate[i] == PopupPopulator.Item.NOTIFICATION) {
+                int secondaryHeight = secondaryNotificationViewHasIcons ?
+                        res.getDimensionPixelSize(R.dimen.notification_footer_height) :
+                        res.getDimensionPixelSize(R.dimen.notification_footer_collapsed_height);
+                item.findViewById(R.id.footer).getLayoutParams().height = secondaryHeight;
+            }
             if (i < numItems - 1) {
                 ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
             }
@@ -550,6 +574,78 @@
         return false;
     }
 
+    public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
+        final NotificationItemView notificationView = (NotificationItemView) findViewById(R.id.notification_view);
+        if (notificationView == null) {
+            return;
+        }
+        ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
+        BadgeInfo badgeInfo = updatedBadges.get(PackageUserKey.fromItemInfo(originalInfo));
+        if (badgeInfo == null || badgeInfo.getNotificationCount() == 0) {
+            AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
+            final int duration = getResources().getInteger(
+                    R.integer.config_removeNotificationViewDuration);
+            final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+            removeNotification.play(animateTranslationYBy(notificationView.getHeight() + spacing,
+                    duration));
+            Animator reduceHeight = notificationView.createRemovalAnimation(duration);
+            final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
+                    : notificationView;
+            if (removeMarginView != null) {
+                ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
+                removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+                    @Override
+                    public void onAnimationUpdate(ValueAnimator valueAnimator) {
+                        ((MarginLayoutParams) removeMarginView.getLayoutParams()).bottomMargin
+                                = (int) (spacing * (float) valueAnimator.getAnimatedValue());
+                    }
+                });
+                removeNotification.play(removeMargin);
+            }
+            removeNotification.play(reduceHeight);
+            Animator fade = new LauncherViewPropertyAnimator(notificationView).alpha(0)
+                    .setDuration(duration);
+            fade.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    removeView(notificationView);
+                    if (getItemCount() == 0) {
+                        close(false);
+                        return;
+                    }
+                    View firstItem = getItemViewAt(mIsAboveIcon ? getItemCount() - 1 : 0);
+                    mArrow.setBackgroundTintList(firstItem.getBackgroundTintList());
+                }
+            });
+            removeNotification.play(fade);
+            final long arrowScaleDuration = getResources().getInteger(
+                    R.integer.config_deepShortcutArrowOpenDuration);
+            Animator hideArrow = new LauncherViewPropertyAnimator(mArrow)
+                    .scaleX(0).scaleY(0).setDuration(arrowScaleDuration);
+            hideArrow.setStartDelay(0);
+            Animator showArrow = new LauncherViewPropertyAnimator(mArrow)
+                    .scaleX(1).scaleY(1).setDuration(arrowScaleDuration);
+            showArrow.setStartDelay((long) (duration - arrowScaleDuration * 1.5));
+            removeNotification.playSequentially(hideArrow, showArrow);
+            removeNotification.start();
+            return;
+        }
+        notificationView.trimNotifications(badgeInfo.getNotificationKeys());
+    }
+
+    /**
+     * Animates the translationY of this container if it is open above the icon.
+     * If it is below the icon, the container already shifts up when the height
+     * of a child (e.g. NotificationView) changes, so the translation isn't necessary.
+     */
+    public @Nullable Animator animateTranslationYBy(int translationY, int duration) {
+        if (mIsAboveIcon) {
+            return new LauncherViewPropertyAnimator(this)
+                    .translationY(getTranslationY() + translationY).setDuration(duration);
+        }
+        return null;
+    }
+
     @Override
     public boolean supportsAppInfoDropTarget() {
         return true;
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
index b671c36..f6fdb76 100644
--- a/src/com/android/launcher3/popup/PopupDataProvider.java
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -23,7 +23,7 @@
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.badge.BadgeInfo;
-import com.android.launcher3.badge.NotificationListener;
+import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.MultiHashMap;
@@ -75,6 +75,11 @@
                 mPackageUserToBadgeInfos.remove(removedPackageUserKey);
             }
             mLauncher.updateIconBadges(Collections.singleton(removedPackageUserKey));
+
+            PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
+            if (openContainer != null) {
+                openContainer.trimNotifications(mPackageUserToBadgeInfos);
+            }
         }
     }
 
@@ -110,6 +115,11 @@
         if (!updatedBadges.isEmpty()) {
             mLauncher.updateIconBadges(updatedBadges.keySet());
         }
+
+        PopupContainerWithArrow openContainer = PopupContainerWithArrow.getOpen(mLauncher);
+        if (openContainer != null) {
+            openContainer.trimNotifications(updatedBadges);
+        }
     }
 
     public void setDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
@@ -140,6 +150,7 @@
 
     public String[] getNotificationKeysForItem(ItemInfo info) {
         BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
+        if (badgeInfo == null) { return new String[0]; }
         Set<String> notificationKeys = badgeInfo.getNotificationKeys();
         return notificationKeys.toArray(new String[notificationKeys.size()]);
     }
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
index 25d496a..6af6e7d 100644
--- a/src/com/android/launcher3/popup/PopupItemView.java
+++ b/src/com/android/launcher3/popup/PopupItemView.java
@@ -20,6 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.AttributeSet;
@@ -72,6 +73,10 @@
         mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
     }
 
+    protected ColorStateList getAttachedArrowColor() {
+        return getBackgroundTintList();
+    }
+
     public boolean willDrawIcon() {
         return true;
     }
@@ -158,7 +163,8 @@
 
         public ZoomRevealOutlineProvider(int x, int y, Rect pillRect,
                 View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) {
-            super(x, y, pillRect);
+            super(x, y, pillRect, zoomView.getResources().getDimensionPixelSize(
+                    R.dimen.bg_pill_radius));
             mTranslateView = translateView;
             mZoomView = zoomView;
             mFullHeight = pillRect.height();
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index b5a59b0..f990fa2 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -19,12 +19,15 @@
 import android.content.ComponentName;
 import android.os.Handler;
 import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
 import android.support.annotation.VisibleForTesting;
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.notification.NotificationInfo;
+import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.graphics.LauncherIcons;
 import com.android.launcher3.shortcuts.DeepShortcutManager;
 import com.android.launcher3.shortcuts.DeepShortcutView;
@@ -45,7 +48,8 @@
     @VisibleForTesting static final int NUM_DYNAMIC = 2;
 
     public enum Item {
-        SHORTCUT(R.layout.deep_shortcut);
+        SHORTCUT(R.layout.deep_shortcut),
+        NOTIFICATION(R.layout.notification);
 
         public final int layoutId;
 
@@ -54,12 +58,18 @@
         }
     }
 
-    public static Item[] getItemsToPopulate(List<String> shortcutIds) {
-        int numItems = Math.min(MAX_ITEMS, shortcutIds.size());
+    public static Item[] getItemsToPopulate(List<String> shortcutIds, String[] notificationKeys) {
+        boolean hasNotifications = notificationKeys.length > 0;
+        int numNotificationItems = hasNotifications ? 1 : 0;
+        int numItems = Math.min(MAX_ITEMS, shortcutIds.size() + numNotificationItems);
         Item[] items = new Item[numItems];
         for (int i = 0; i < numItems; i++) {
             items[i] = Item.SHORTCUT;
         }
+        if (hasNotifications) {
+            // The notification layout is always first.
+            items[0] = Item.NOTIFICATION;
+        }
         return items;
     }
 
@@ -134,12 +144,24 @@
 
     public static Runnable createUpdateRunnable(final Launcher launcher, ItemInfo originalInfo,
             final Handler uiHandler, final PopupContainerWithArrow container,
-            final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews) {
+            final List<String> shortcutIds, final List<DeepShortcutView> shortcutViews,
+            final String[] notificationKeys, final NotificationItemView notificationView) {
         final ComponentName activity = originalInfo.getTargetComponent();
         final UserHandle user = originalInfo.user;
         return new Runnable() {
             @Override
             public void run() {
+                if (notificationView != null) {
+                    List<StatusBarNotification> notifications = launcher.getPopupDataProvider()
+                            .getStatusBarNotificationsForKeys(notificationKeys);
+                    List<NotificationInfo> infos = new ArrayList<>(notifications.size());
+                    for (int i = 0; i < notifications.size(); i++) {
+                        StatusBarNotification notification = notifications.get(i);
+                        infos.add(new NotificationInfo(launcher, notification));
+                    }
+                    uiHandler.post(new UpdateNotificationChild(notificationView, infos));
+                }
+
                 final List<ShortcutInfoCompat> shortcuts = PopupPopulator.sortAndFilterShortcuts(
                         DeepShortcutManager.getInstance(launcher).queryForShortcutsContainer(
                                 activity, shortcutIds, user));
@@ -176,4 +198,21 @@
             mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer);
         }
     }
+
+    /** Updates the child of this container at the given index based on the given shortcut info. */
+    private static class UpdateNotificationChild implements Runnable {
+        private NotificationItemView mNotificationView;
+        private List<NotificationInfo> mNotificationInfos;
+
+        public UpdateNotificationChild(NotificationItemView notificationView,
+                List<NotificationInfo> notificationInfos) {
+            mNotificationView = notificationView;
+            mNotificationInfos = notificationInfos;
+        }
+
+        @Override
+        public void run() {
+            mNotificationView.applyNotificationInfos(mNotificationInfos);
+        }
+    }
 }
diff --git a/src/com/android/launcher3/util/PillRevealOutlineProvider.java b/src/com/android/launcher3/util/PillRevealOutlineProvider.java
index 1a3b486..a57d69f 100644
--- a/src/com/android/launcher3/util/PillRevealOutlineProvider.java
+++ b/src/com/android/launcher3/util/PillRevealOutlineProvider.java
@@ -28,6 +28,7 @@
 
     private int mCenterX;
     private int mCenterY;
+    private float mFinalRadius;
     protected Rect mPillRect;
 
     /**
@@ -36,10 +37,14 @@
      * @param pillRect round rect that represents the final pill shape
      */
     public PillRevealOutlineProvider(int x, int y, Rect pillRect) {
+        this(x, y, pillRect, pillRect.height() / 2f);
+    }
+
+    public PillRevealOutlineProvider(int x, int y, Rect pillRect, float radius) {
         mCenterX = x;
         mCenterY = y;
         mPillRect = pillRect;
-        mOutlineRadius = pillRect.height() / 2f;
+        mOutlineRadius = mFinalRadius = radius;
     }
 
     @Override
@@ -58,6 +63,6 @@
         mOutline.top = Math.max(mPillRect.top, mCenterY - currentSize);
         mOutline.right = Math.min(mPillRect.right, mCenterX + currentSize);
         mOutline.bottom = Math.min(mPillRect.bottom, mCenterY + currentSize);
-        mOutlineRadius = mOutline.height() / 2;
+        mOutlineRadius = Math.min(mFinalRadius, mOutline.height() / 2);
     }
 }