Add NotificationListener to launcher.

- NotificationListener extends NotificationListenerService, and is
  added to the manifest.
- Added PopupDataProvider, which contains logic for storing and
  interacting with data that goes into the long-press popup menu
  (shortcuts and notifications). A follow-up CL will rename
  DeepShortcutsContainer to a generic PopupContainerWithArrow.
- If Launcher has notification access, NotificationListener will
  get callbacks when notifications are posted and removed; upon
  receiving these callbacks, NotificationListener passes them to
  PopupDataProvider via a NotificationsChangedListener interface.
- Upon receiving the changed notifications, PopupDataProvider maps
  them to the corresponding package/user and tells launcher to
  update relevant icons on the workspace and all apps.

This is guarded by FeatureFlags.BADGE_ICONS.

Bug: 32410600
Change-Id: I59aeb31a7f92399c9c4b831ab551e51e13f44f5c
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 974b0df..b6e5bb0 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -76,6 +76,13 @@
             android:process=":wallpaper_chooser">
         </service>
 
+        <service android:name="com.android.launcher3.badging.NotificationListener"
+                 android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
+            <intent-filter>
+                <action android:name="android.service.notification.NotificationListenerService" />
+            </intent-filter>
+        </service>
+
         <meta-data android:name="android.nfc.disable_beam_default"
                        android:value="true" />
 
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index d9e9c7b..47a5b4f 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -39,14 +39,16 @@
 
 import com.android.launcher3.IconCache.IconLoadRequest;
 import com.android.launcher3.IconCache.ItemInfoUpdateReceiver;
-import com.android.launcher3.badge.BadgeRenderer;
 import com.android.launcher3.badge.BadgeInfo;
+import com.android.launcher3.badge.BadgeRenderer;
+import com.android.launcher3.badging.NotificationInfo;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.graphics.DrawableFactory;
 import com.android.launcher3.graphics.HolographicOutlineHelper;
 import com.android.launcher3.model.PackageItemInfo;
 
 import java.text.NumberFormat;
+import java.util.List;
 
 /**
  * TextView that draws a bubble behind the text. We cannot use a LineBackgroundSpan
@@ -168,6 +170,8 @@
         if (promiseStateChanged || info.isPromise()) {
             applyPromiseState(promiseStateChanged);
         }
+
+        applyBadgeState(info);
     }
 
     public void applyFromApplicationInfo(AppInfo info) {
@@ -178,6 +182,8 @@
 
         // Verify high res immediately
         verifyHighRes();
+
+        applyBadgeState(info);
     }
 
     public void applyFromPackageItemInfo(PackageItemInfo info) {
@@ -502,8 +508,9 @@
         }
     }
 
-    public void applyBadgeState(BadgeInfo badgeInfo) {
+    public void applyBadgeState(ItemInfo itemInfo) {
         if (mIcon instanceof FastBitmapDrawable) {
+            BadgeInfo badgeInfo = mLauncher.getPopupDataProvider().getBadgeInfoForItem(itemInfo);
             BadgeRenderer badgeRenderer = mLauncher.getDeviceProfile().mBadgeRenderer;
             ((FastBitmapDrawable) mIcon).applyIconBadge(badgeInfo, badgeRenderer);
         }
@@ -634,7 +641,8 @@
      * Returns true if the view can show custom shortcuts.
      */
     public boolean hasDeepShortcuts() {
-        return !mLauncher.getShortcutIdsForItem((ItemInfo) getTag()).isEmpty();
+        return !mLauncher.getPopupDataProvider().getShortcutIdsForItem((ItemInfo) getTag())
+                .isEmpty();
     }
 
     /**
diff --git a/src/com/android/launcher3/FastBitmapDrawable.java b/src/com/android/launcher3/FastBitmapDrawable.java
index b3e59f9..587d445 100644
--- a/src/com/android/launcher3/FastBitmapDrawable.java
+++ b/src/com/android/launcher3/FastBitmapDrawable.java
@@ -30,12 +30,13 @@
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
+import android.util.Log;
 import android.util.SparseArray;
 import android.view.animation.DecelerateInterpolator;
 
-import com.android.launcher3.graphics.IconPalette;
-import com.android.launcher3.badge.BadgeRenderer;
 import com.android.launcher3.badge.BadgeInfo;
+import com.android.launcher3.badge.BadgeRenderer;
+import com.android.launcher3.graphics.IconPalette;
 
 public class FastBitmapDrawable extends Drawable {
     private static final float DISABLED_DESATURATION = 1f;
@@ -123,13 +124,17 @@
     }
 
     public void applyIconBadge(BadgeInfo badgeInfo, BadgeRenderer badgeRenderer) {
+        boolean wasBadged = mBadgeInfo != null;
+        boolean isBadged = badgeInfo != null;
         mBadgeInfo = badgeInfo;
         mBadgeRenderer = badgeRenderer;
-        if (mIconPalette == null) {
-            mIconPalette = IconPalette.fromDominantColor(Utilities
-                    .findDominantColorByHue(mBitmap, 20));
+        if (wasBadged || isBadged) {
+            if (mBadgeInfo != null && mIconPalette == null) {
+                mIconPalette = IconPalette.fromDominantColor(Utilities
+                        .findDominantColorByHue(mBitmap, 20));
+            }
+            invalidateSelf();
         }
-        invalidateSelf();
     }
 
     @Override
@@ -157,7 +162,7 @@
     }
 
     private boolean hasBadge() {
-        return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != null;
+        return mBadgeInfo != null && mBadgeInfo.getNotificationCount() != 0;
     }
 
     @Override
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 9245f18..aa5b8c8 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -84,6 +84,8 @@
 import com.android.launcher3.allapps.AllAppsTransitionController;
 import com.android.launcher3.allapps.DefaultAppSearchController;
 import com.android.launcher3.anim.AnimationLayerSet;
+import com.android.launcher3.badging.NotificationListener;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
 import com.android.launcher3.compat.PinItemRequestCompat;
@@ -117,6 +119,7 @@
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.MultiHashMap;
 import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.PendingRequestArgs;
 import com.android.launcher3.util.TestingUtils;
 import com.android.launcher3.util.Thunk;
@@ -130,10 +133,10 @@
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Default launcher application.
@@ -260,8 +263,7 @@
     private boolean mHasFocus = false;
     private boolean mAttached = false;
 
-    /** Maps launcher activity components to their list of shortcut ids. */
-    private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
+    private PopupDataProvider mPopupDataProvider;
 
     private View.OnTouchListener mHapticFeedbackTouchListener;
 
@@ -394,6 +396,8 @@
         mExtractedColors = new ExtractedColors();
         loadExtractedColorsAndColorItems();
 
+        mPopupDataProvider = new PopupDataProvider(this);
+
         ((AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE))
                 .addAccessibilityStateChangeListener(this);
 
@@ -652,6 +656,10 @@
         return (int) info.id;
     }
 
+    public PopupDataProvider getPopupDataProvider() {
+        return mPopupDataProvider;
+    }
+
     /**
      * Returns whether we should delay spring loaded mode -- for shortcuts and widgets that have
      * a configuration step, this allows the proper animations to run after other transitions.
@@ -926,6 +934,8 @@
         if (Utilities.ATLEAST_NOUGAT_MR1) {
             mAppWidgetHost.stopListening();
         }
+
+        NotificationListener.removeNotificationsChangedListener();
     }
 
     @Override
@@ -940,6 +950,10 @@
         if (Utilities.ATLEAST_NOUGAT_MR1) {
             mAppWidgetHost.startListening();
         }
+
+        if (!isWorkspaceLoading()) {
+            NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
+        }
     }
 
     @Override
@@ -1564,6 +1578,19 @@
         }
     };
 
+    public void updateIconBadges(final Set<PackageUserKey> updatedBadges) {
+        Runnable r = new Runnable() {
+            @Override
+            public void run() {
+                mWorkspace.updateIconBadges(updatedBadges);
+                mAppsView.updateIconBadges(updatedBadges);
+            }
+        };
+        if (!waitUntilResume(r)) {
+            r.run();
+        }
+    }
+
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
@@ -3672,6 +3699,8 @@
 
         InstallShortcutReceiver.disableAndFlushInstallQueue(this);
 
+        NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
+
         if (mLauncherCallbacks != null) {
             mLauncherCallbacks.finishBindingItems(false);
         }
@@ -3741,21 +3770,7 @@
      */
     @Override
     public void bindDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
-        mDeepShortcutMap = deepShortcutMapCopy;
-        if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
-    }
-
-    public List<String> getShortcutIdsForItem(ItemInfo info) {
-        if (!DeepShortcutManager.supportsShortcuts(info)) {
-            return Collections.EMPTY_LIST;
-        }
-        ComponentName component = info.getTargetComponent();
-        if (component == null) {
-            return Collections.EMPTY_LIST;
-        }
-
-        List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
-        return ids == null ? Collections.EMPTY_LIST : ids;
+        mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
     }
 
     /**
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 6eb87f2..e646dd9 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -78,6 +78,7 @@
 import com.android.launcher3.util.ItemInfoMatcher;
 import com.android.launcher3.util.LongArrayMap;
 import com.android.launcher3.util.MultiStateAlphaController;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.util.Thunk;
 import com.android.launcher3.util.VerticalFlingDetector;
 import com.android.launcher3.util.WallpaperOffsetInterpolator;
@@ -86,6 +87,7 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.Set;
 
 /**
  * The workspace is a wide area with a wallpaper and a finite number of pages.
@@ -3957,6 +3959,23 @@
         });
     }
 
+    public void updateIconBadges(final Set<PackageUserKey> updatedBadges) {
+        final PackageUserKey packageUserKey = new PackageUserKey(null, null);
+        mapOverItems(MAP_RECURSE, new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View v) {
+                if (info instanceof ShortcutInfo && v instanceof BubbleTextView) {
+                    packageUserKey.updateFromItemInfo(info);
+                    if (updatedBadges.contains(packageUserKey)) {
+                        ((BubbleTextView) v).applyBadgeState(info);
+                    }
+                }
+                // process all the shortcuts
+                return false;
+            }
+        });
+    }
+
     public void removeAbandonedPromise(String packageName, UserHandle user) {
         HashSet<String> packages = new HashSet<>(1);
         packages.add(packageName);
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index a2266fe..ec1fa34 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -53,9 +53,11 @@
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageUserKey;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 /**
  * The all apps view container.
@@ -472,4 +474,16 @@
             navBarBg.setVisibility(View.VISIBLE);
         }
     }
+
+    public void updateIconBadges(Set<PackageUserKey> updatedBadges) {
+        final PackageUserKey packageUserKey = new PackageUserKey(null, null);
+        for (AlphabeticalAppsList.AdapterItem app : mApps.getAdapterItems()) {
+            if (app.appInfo != null) {
+                packageUserKey.updateFromItemInfo(app.appInfo);
+                if (updatedBadges.contains(packageUserKey)) {
+                    mAdapter.notifyItemChanged(app.position);
+                }
+            }
+        }
+    }
 }
diff --git a/src/com/android/launcher3/badge/BadgeInfo.java b/src/com/android/launcher3/badge/BadgeInfo.java
index 0a9f87c..98d2277 100644
--- a/src/com/android/launcher3/badge/BadgeInfo.java
+++ b/src/com/android/launcher3/badge/BadgeInfo.java
@@ -16,18 +16,60 @@
 
 package com.android.launcher3.badge;
 
+import com.android.launcher3.util.PackageUserKey;
+
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Contains data to be used in an icon badge.
  */
 public class BadgeInfo {
 
-    private int mNotificationCount;
+    /** Used to link this BadgeInfo to icons on the workspace and all apps */
+    private PackageUserKey mPackageUserKey;
+    /**
+     * The keys of the notifications that this badge represents. These keys can later be
+     * used to retrieve {@link com.android.launcher3.badging.NotificationInfo}'s.
+     */
+    private Set<String> mNotificationKeys;
 
-    public void setNotificationCount(int count) {
-        mNotificationCount = count;
+    public BadgeInfo(PackageUserKey packageUserKey) {
+        mPackageUserKey = packageUserKey;
+        mNotificationKeys = new HashSet<>();
     }
 
-    public String getNotificationCount() {
-        return mNotificationCount == 0 ? null : String.valueOf(mNotificationCount);
+    /**
+     * Returns whether the notification was added (false if it already existed).
+     */
+    public boolean addNotificationKey(String notificationKey) {
+        return mNotificationKeys.add(notificationKey);
+    }
+
+    /**
+     * Returns whether the notification was removed (false if it didn't exist).
+     */
+    public boolean removeNotificationKey(String notificationKey) {
+        return mNotificationKeys.remove(notificationKey);
+    }
+
+    public Set<String> getNotificationKeys() {
+        return mNotificationKeys;
+    }
+
+    public int getNotificationCount() {
+        return mNotificationKeys.size();
+    }
+
+    /**
+     * Whether newBadge represents the same PackageUserKey as this badge, and icons with
+     * this badge should be invalidated. So, for instance, if a badge has 3 notifications
+     * and one of those notifications is updated, this method should return false because
+     * the badge still says "3" and the contents of those notifications are only retrieved
+     * upon long-click. This method always returns true when adding or removing notifications.
+     */
+    public boolean shouldBeInvalidated(BadgeInfo newBadge) {
+        return mPackageUserKey.equals(newBadge.mPackageUserKey)
+                && getNotificationCount() != newBadge.getNotificationCount();
     }
 }
diff --git a/src/com/android/launcher3/badge/BadgeRenderer.java b/src/com/android/launcher3/badge/BadgeRenderer.java
index 238b918..787ee72 100644
--- a/src/com/android/launcher3/badge/BadgeRenderer.java
+++ b/src/com/android/launcher3/badge/BadgeRenderer.java
@@ -61,7 +61,7 @@
         mBackgroundRect.set(iconBounds.right - size, iconBounds.top, iconBounds.right,
                 iconBounds.top + size);
         canvas.drawOval(mBackgroundRect, mBackgroundPaint);
-        String notificationCount = badgeInfo.getNotificationCount();
+        String notificationCount = String.valueOf(badgeInfo.getNotificationCount());
         canvas.drawText(notificationCount,
                 mBackgroundRect.centerX(),
                 mBackgroundRect.centerY() + mTextHeight / 2,
diff --git a/src/com/android/launcher3/badging/NotificationInfo.java b/src/com/android/launcher3/badging/NotificationInfo.java
new file mode 100644
index 0000000..2590add
--- /dev/null
+++ b/src/com/android/launcher3/badging/NotificationInfo.java
@@ -0,0 +1,82 @@
+/*
+ * 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.badging;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.service.notification.StatusBarNotification;
+import android.view.View;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.shortcuts.DeepShortcutsContainer;
+import com.android.launcher3.util.PackageUserKey;
+
+/**
+ * An object that contains relevant information from a {@link StatusBarNotification}. This should
+ * only be created when we need to show the notification contents on the UI; until then, a
+ * {@link com.android.launcher3.badge.BadgeInfo} with only the notification key should
+ * be passed around, and then this can be constructed using the StatusBarNotification from
+ * {@link NotificationListener#getNotificationsForKeys(String[])}.
+ */
+public class NotificationInfo implements View.OnClickListener {
+
+    public final PackageUserKey packageUserKey;
+    public final String notificationKey;
+    public final CharSequence title;
+    public final CharSequence text;
+    public final Drawable iconDrawable;
+    public final PendingIntent intent;
+    public final boolean autoCancel;
+
+    /**
+     * 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();
+        if (icon == null) {
+            icon = notification.getNotification().getSmallIcon();
+            iconDrawable = icon.loadDrawable(context);
+            iconDrawable.setTint(notification.getNotification().color);
+        } else {
+            iconDrawable = icon.loadDrawable(context);
+        }
+        intent = notification.getNotification().contentIntent;
+        autoCancel = (notification.getNotification().flags
+                & Notification.FLAG_AUTO_CANCEL) != 0;
+    }
+
+    @Override
+    public void onClick(View view) {
+        final Launcher launcher = Launcher.getLauncher(view.getContext());
+        try {
+            intent.send();
+        } catch (PendingIntent.CanceledException e) {
+            e.printStackTrace();
+        }
+        if (autoCancel) {
+            launcher.getPopupDataProvider().cancelNotification(notificationKey);
+        }
+        DeepShortcutsContainer.getOpen(launcher).close(true);
+    }
+}
diff --git a/src/com/android/launcher3/badging/NotificationListener.java b/src/com/android/launcher3/badging/NotificationListener.java
new file mode 100644
index 0000000..0a85d56
--- /dev/null
+++ b/src/com/android/launcher3/badging/NotificationListener.java
@@ -0,0 +1,213 @@
+/*
+ * 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.badging;
+
+import android.app.Notification;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+
+import com.android.launcher3.LauncherModel;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.util.PackageUserKey;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A {@link NotificationListenerService} that sends updates to its
+ * {@link NotificationsChangedListener} when notifications are posted or canceled,
+ * as well and when this service first connects. An instance of NotificationListener,
+ * and its methods for getting notifications, can be obtained via {@link #getInstance()}.
+ */
+public class NotificationListener extends NotificationListenerService {
+
+    private static final int MSG_NOTIFICATION_POSTED = 1;
+    private static final int MSG_NOTIFICATION_REMOVED = 2;
+    private static final int MSG_NOTIFICATION_FULL_REFRESH = 3;
+
+    private static NotificationListener sNotificationListenerInstance = null;
+    private static NotificationsChangedListener sNotificationsChangedListener;
+
+    private final Handler mWorkerHandler;
+    private final Handler mUiHandler;
+
+    private Handler.Callback mWorkerCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_NOTIFICATION_POSTED:
+                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
+                    break;
+                case MSG_NOTIFICATION_REMOVED:
+                    mUiHandler.obtainMessage(message.what, message.obj).sendToTarget();
+                    break;
+                case MSG_NOTIFICATION_FULL_REFRESH:
+                    final List<StatusBarNotification> activeNotifications
+                            = filterNotifications(getActiveNotifications());
+                    mUiHandler.obtainMessage(message.what, activeNotifications).sendToTarget();
+                    break;
+            }
+            return true;
+        }
+    };
+
+    private Handler.Callback mUiCallback = new Handler.Callback() {
+        @Override
+        public boolean handleMessage(Message message) {
+            switch (message.what) {
+                case MSG_NOTIFICATION_POSTED:
+                    if (sNotificationsChangedListener != null) {
+                        Pair<PackageUserKey, String> pair
+                                = (Pair<PackageUserKey, String>) message.obj;
+                        sNotificationsChangedListener.onNotificationPosted(pair.first, pair.second);
+                    }
+                    break;
+                case MSG_NOTIFICATION_REMOVED:
+                    if (sNotificationsChangedListener != null) {
+                        Pair<PackageUserKey, String> pair
+                                = (Pair<PackageUserKey, String>) message.obj;
+                        sNotificationsChangedListener.onNotificationRemoved(pair.first, pair.second);
+                    }
+                    break;
+                case MSG_NOTIFICATION_FULL_REFRESH:
+                    if (sNotificationsChangedListener != null) {
+                        sNotificationsChangedListener.onNotificationFullRefresh(
+                                (List<StatusBarNotification>) message.obj);
+                    }
+                    break;
+            }
+            return true;
+        }
+    };
+
+    public NotificationListener() {
+        super();
+        mWorkerHandler = new Handler(LauncherModel.getWorkerLooper(), mWorkerCallback);
+        mUiHandler = new Handler(Looper.getMainLooper(), mUiCallback);
+    }
+
+    public static @Nullable NotificationListener getInstance() {
+        return sNotificationListenerInstance;
+    }
+
+    public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
+        if (!FeatureFlags.BADGE_ICONS) {
+            return;
+        }
+        sNotificationsChangedListener = listener;
+
+        NotificationListener notificationListener = getInstance();
+        if (notificationListener != null) {
+            notificationListener.onNotificationFullRefresh();
+        }
+    }
+
+    public static void removeNotificationsChangedListener() {
+        sNotificationsChangedListener = null;
+    }
+
+    @Override
+    public void onListenerConnected() {
+        super.onListenerConnected();
+        sNotificationListenerInstance = this;
+        onNotificationFullRefresh();
+    }
+
+    private void onNotificationFullRefresh() {
+        mWorkerHandler.obtainMessage(MSG_NOTIFICATION_FULL_REFRESH).sendToTarget();
+    }
+
+    @Override
+    public void onListenerDisconnected() {
+        super.onListenerDisconnected();
+        sNotificationListenerInstance = null;
+    }
+
+    @Override
+    public void onNotificationPosted(final StatusBarNotification sbn) {
+        super.onNotificationPosted(sbn);
+        if (!shouldBeFilteredOut(sbn.getNotification())) {
+            Pair<PackageUserKey, String> packageUserKeyAndNotificationKey
+                    = new Pair<>(PackageUserKey.fromNotification(sbn), sbn.getKey());
+            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_POSTED, packageUserKeyAndNotificationKey)
+                    .sendToTarget();
+        }
+    }
+
+    @Override
+    public void onNotificationRemoved(final StatusBarNotification sbn) {
+        super.onNotificationRemoved(sbn);
+        if (!shouldBeFilteredOut(sbn.getNotification())) {
+            Pair<PackageUserKey, String> packageUserKeyAndNotificationKey
+                    = new Pair<>(PackageUserKey.fromNotification(sbn), sbn.getKey());
+            mWorkerHandler.obtainMessage(MSG_NOTIFICATION_REMOVED, packageUserKeyAndNotificationKey)
+                    .sendToTarget();
+        }
+    }
+
+    /** This makes a potentially expensive binder call and should be run on a background thread. */
+    public List<StatusBarNotification> getNotificationsForKeys(String[] keys) {
+        StatusBarNotification[] notifications = NotificationListener.this
+                .getActiveNotifications(keys);
+        return notifications == null ? Collections.EMPTY_LIST : Arrays.asList(notifications);
+    }
+
+    /**
+     * Filter out notifications that don't have an intent
+     * or are headers for grouped notifications.
+     *
+     * TODO: use the system concept of a badged notification instead
+     */
+    private List<StatusBarNotification> filterNotifications(
+            StatusBarNotification[] notifications) {
+        if (notifications == null) return null;
+        Set<Integer> removedNotifications = new HashSet<>();
+        for (int i = 0; i < notifications.length; i++) {
+            if (shouldBeFilteredOut(notifications[i].getNotification())) {
+                removedNotifications.add(i);
+            }
+        }
+        List<StatusBarNotification> filteredNotifications = new ArrayList<>(
+                notifications.length - removedNotifications.size());
+        for (int i = 0; i < notifications.length; i++) {
+            if (!removedNotifications.contains(i)) {
+                filteredNotifications.add(notifications[i]);
+            }
+        }
+        return filteredNotifications;
+    }
+
+    private boolean shouldBeFilteredOut(Notification notification) {
+        boolean isGroupHeader = (notification.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
+        return (notification.contentIntent == null || isGroupHeader);
+    }
+
+    public interface NotificationsChangedListener {
+        void onNotificationPosted(PackageUserKey postedPackageUserKey, String notificationKey);
+        void onNotificationRemoved(PackageUserKey removedPackageUserKey, String notificationKey);
+        void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications);
+    }
+}
diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java
new file mode 100644
index 0000000..4ed32b5
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupDataProvider.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.popup;
+
+import android.content.ComponentName;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.badge.BadgeInfo;
+import com.android.launcher3.badging.NotificationListener;
+import com.android.launcher3.shortcuts.DeepShortcutManager;
+import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.MultiHashMap;
+import com.android.launcher3.util.PackageUserKey;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides data for the popup menu that appears after long-clicking on apps.
+ */
+public class PopupDataProvider implements NotificationListener.NotificationsChangedListener {
+
+    private static final boolean LOGD = false;
+    private static final String TAG = "PopupDataProvider";
+
+    private final Launcher mLauncher;
+
+    /** Maps launcher activity components to their list of shortcut ids. */
+    private MultiHashMap<ComponentKey, String> mDeepShortcutMap = new MultiHashMap<>();
+    /** Maps packages to their BadgeInfo's . */
+    private Map<PackageUserKey, BadgeInfo> mPackageUserToBadgeInfos = new HashMap<>();
+
+    public PopupDataProvider(Launcher launcher) {
+        mLauncher = launcher;
+    }
+
+    @Override
+    public void onNotificationPosted(PackageUserKey postedPackageUserKey, String notificationKey) {
+        BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(postedPackageUserKey);
+        if (oldBadgeInfo == null) {
+            BadgeInfo newBadgeInfo = new BadgeInfo(postedPackageUserKey);
+            newBadgeInfo.addNotificationKey(notificationKey);
+            mPackageUserToBadgeInfos.put(postedPackageUserKey, newBadgeInfo);
+            mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
+        } else if (oldBadgeInfo.addNotificationKey(notificationKey)) {
+            mLauncher.updateIconBadges(Collections.singleton(postedPackageUserKey));
+        }
+    }
+
+    @Override
+    public void onNotificationRemoved(PackageUserKey removedPackageUserKey, String notificationKey) {
+        BadgeInfo oldBadgeInfo = mPackageUserToBadgeInfos.get(removedPackageUserKey);
+        if (oldBadgeInfo != null && oldBadgeInfo.removeNotificationKey(notificationKey)) {
+            if (oldBadgeInfo.getNotificationCount() == 0) {
+                mPackageUserToBadgeInfos.remove(removedPackageUserKey);
+            }
+            mLauncher.updateIconBadges(Collections.singleton(removedPackageUserKey));
+        }
+    }
+
+    @Override
+    public void onNotificationFullRefresh(List<StatusBarNotification> activeNotifications) {
+        if (activeNotifications == null) return;
+        // This will contain the PackageUserKeys which have updated badges.
+        HashMap<PackageUserKey, BadgeInfo> updatedBadges = new HashMap<>(mPackageUserToBadgeInfos);
+        mPackageUserToBadgeInfos.clear();
+        for (StatusBarNotification notification : activeNotifications) {
+            PackageUserKey packageUserKey = PackageUserKey.fromNotification(notification);
+            BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(packageUserKey);
+            if (badgeInfo == null) {
+                badgeInfo = new BadgeInfo(packageUserKey);
+                mPackageUserToBadgeInfos.put(packageUserKey, badgeInfo);
+            }
+            badgeInfo.addNotificationKey(notification.getKey());
+        }
+
+        // Add and remove from updatedBadges so it contains the PackageUserKeys of updated badges.
+        for (PackageUserKey packageUserKey : mPackageUserToBadgeInfos.keySet()) {
+            BadgeInfo prevBadge = updatedBadges.get(packageUserKey);
+            BadgeInfo newBadge = mPackageUserToBadgeInfos.get(packageUserKey);
+            if (prevBadge == null) {
+                updatedBadges.put(packageUserKey, newBadge);
+            } else {
+                if (!prevBadge.shouldBeInvalidated(newBadge)) {
+                    updatedBadges.remove(packageUserKey);
+                }
+            }
+        }
+
+        if (!updatedBadges.isEmpty()) {
+            mLauncher.updateIconBadges(updatedBadges.keySet());
+        }
+    }
+
+    public void setDeepShortcutMap(MultiHashMap<ComponentKey, String> deepShortcutMapCopy) {
+        mDeepShortcutMap = deepShortcutMapCopy;
+        if (LOGD) Log.d(TAG, "bindDeepShortcutMap: " + mDeepShortcutMap);
+    }
+
+    public List<String> getShortcutIdsForItem(ItemInfo info) {
+        if (!DeepShortcutManager.supportsShortcuts(info)) {
+            return Collections.EMPTY_LIST;
+        }
+        ComponentName component = info.getTargetComponent();
+        if (component == null) {
+            return Collections.EMPTY_LIST;
+        }
+
+        List<String> ids = mDeepShortcutMap.get(new ComponentKey(component, info.user));
+        return ids == null ? Collections.EMPTY_LIST : ids;
+    }
+
+    public BadgeInfo getBadgeInfoForItem(ItemInfo info) {
+        if (!DeepShortcutManager.supportsShortcuts(info)) {
+            return null;
+        }
+
+        return mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
+    }
+
+    public String[] getNotificationKeysForItem(ItemInfo info) {
+        BadgeInfo badgeInfo = mPackageUserToBadgeInfos.get(PackageUserKey.fromItemInfo(info));
+        Set<String> notificationKeys = badgeInfo.getNotificationKeys();
+        return notificationKeys.toArray(new String[notificationKeys.size()]);
+    }
+
+    /** This makes a potentially expensive binder call and should be run on a background thread. */
+    public List<StatusBarNotification> getStatusBarNotificationsForKeys(String[] notificationKeys) {
+        NotificationListener notificationListener = NotificationListener.getInstance();
+        return notificationListener == null ? Collections.EMPTY_LIST
+                : notificationListener.getNotificationsForKeys(notificationKeys);
+    }
+
+    public void cancelNotification(String notificationKey) {
+        NotificationListener notificationListener = NotificationListener.getInstance();
+        if (notificationListener == null) {
+            return;
+        }
+        notificationListener.cancelNotification(notificationKey);
+    }
+}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
index db2654c..5e12a57 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
@@ -718,7 +718,8 @@
             icon.clearFocus();
             return null;
         }
-        List<String> ids = launcher.getShortcutIdsForItem((ItemInfo) icon.getTag());
+        List<String> ids = launcher.getPopupDataProvider().getShortcutIdsForItem(
+                (ItemInfo) icon.getTag());
         if (!ids.isEmpty()) {
             final DeepShortcutsContainer container =
                     (DeepShortcutsContainer) launcher.getLayoutInflater().inflate(
diff --git a/src/com/android/launcher3/util/PackageUserKey.java b/src/com/android/launcher3/util/PackageUserKey.java
new file mode 100644
index 0000000..d08b0e9
--- /dev/null
+++ b/src/com/android/launcher3/util/PackageUserKey.java
@@ -0,0 +1,51 @@
+package com.android.launcher3.util;
+
+import android.os.UserHandle;
+import android.service.notification.StatusBarNotification;
+
+import com.android.launcher3.ItemInfo;
+
+import java.util.Arrays;
+
+/** Creates a hash key based on package name and user. */
+public class PackageUserKey {
+
+    private String mPackageName;
+    private UserHandle mUser;
+    private int mHashCode;
+
+    public static PackageUserKey fromItemInfo(ItemInfo info) {
+        return new PackageUserKey(info.getTargetComponent().getPackageName(), info.user);
+    }
+
+    public static PackageUserKey fromNotification(StatusBarNotification notification) {
+        return new PackageUserKey(notification.getPackageName(), notification.getUser());
+    }
+
+    public PackageUserKey(String packageName, UserHandle user) {
+        update(packageName, user);
+    }
+
+    private void update(String packageName, UserHandle user) {
+        mPackageName = packageName;
+        mUser = user;
+        mHashCode = Arrays.hashCode(new Object[] {packageName, user});
+    }
+
+    /** This should only be called to avoid new object creations in a loop. */
+    public void updateFromItemInfo(ItemInfo info) {
+        update(info.getTargetComponent().getPackageName(), info.user);
+    }
+
+    @Override
+    public int hashCode() {
+        return mHashCode;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof PackageUserKey)) return false;
+        PackageUserKey otherKey = (PackageUserKey) obj;
+        return mPackageName.equals(otherKey.mPackageName) && mUser.equals(otherKey.mUser);
+    }
+}
diff --git a/src_config/com/android/launcher3/config/FeatureFlags.java b/src_config/com/android/launcher3/config/FeatureFlags.java
index 4cad836..ffb86e4 100644
--- a/src_config/com/android/launcher3/config/FeatureFlags.java
+++ b/src_config/com/android/launcher3/config/FeatureFlags.java
@@ -39,4 +39,6 @@
     public static final boolean LIGHT_STATUS_BAR = false;
     // When enabled allows to use any point on the fast scrollbar to start dragging.
     public static final boolean LAUNCHER3_DIRECT_SCROLL = true;
+    // When enabled icons are badged with the number of notifications associated with that app.
+    public static final boolean BADGE_ICONS = true;
 }