Add notification dots and popup menu notification support to the taskbar.

- Added support for notification dots
- Added support for notifications in popup menu
- Added support for dismissing notifications from the popup menu

Bug: 198438631
Test: long pressed launcher and taskbar icons, clicked notifications, clicked shortcuts
Change-Id: I7c981e60a82b4d6ce28332d804bbbfb5eb89c6a8
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index cc83431..55e9887 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -56,11 +56,14 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.LauncherSettings.Favorites;
 import com.android.launcher3.R;
+import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.folder.Folder;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.touch.ItemClickHandler;
 import com.android.launcher3.util.PackageManagerHelper;
 import com.android.launcher3.util.SettingsCache;
@@ -168,7 +171,7 @@
                 new TaskbarStashController(this),
                 new TaskbarEduController(this),
                 new TaskbarAutohideSuspendController(this),
-                new TaskbarPopupController());
+                new TaskbarPopupController(this));
     }
 
     public void init(TaskbarSharedState sharedState) {
@@ -316,6 +319,17 @@
         }
     }
 
+    @Override
+    public DotInfo getDotInfoForItem(ItemInfo info) {
+        return getPopupDataProvider().getDotInfoForItem(info);
+    }
+
+    @NonNull
+    @Override
+    public PopupDataProvider getPopupDataProvider() {
+        return mControllers.taskbarPopupController.getPopupDataProvider();
+    }
+
     /**
      * Sets a new data-source for this taskbar instance
      */
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index 56730db..a7c5d4d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -92,6 +92,7 @@
         stashedHandleViewController.init(this);
         taskbarStashController.init(this, sharedState);
         taskbarEduController.init(this);
+        taskbarPopupController.init(this);
     }
 
     /**
@@ -107,5 +108,6 @@
         taskbarViewController.onDestroy();
         stashedHandleViewController.onDestroy();
         taskbarAutohideSuspendController.onDestroy();
+        taskbarPopupController.onDestroy();
     }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
index 952f597..06c75fc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java
@@ -19,15 +19,25 @@
 
 import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.R;
+import com.android.launcher3.dot.FolderDotInfo;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.model.data.FolderInfo;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.notification.NotificationListener;
 import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.popup.PopupLiveUpdateHandler;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
+import com.android.launcher3.util.PackageUserKey;
 import com.android.launcher3.views.ActivityContext;
 
 import java.util.HashMap;
 import java.util.Objects;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -39,11 +49,25 @@
     private static final SystemShortcut.Factory<TaskbarActivityContext>
             APP_INFO = SystemShortcut.AppInfo::new;
 
+    private final TaskbarActivityContext mContext;
     private final PopupDataProvider mPopupDataProvider;
 
-    public TaskbarPopupController() {
-        // TODO (b/198438631): add notifications dots change listener
-        mPopupDataProvider = new PopupDataProvider(packageUserKey -> {});
+    // Initialized in init.
+    private TaskbarControllers mControllers;
+
+    public TaskbarPopupController(TaskbarActivityContext context) {
+        mContext = context;
+        mPopupDataProvider = new PopupDataProvider(this::updateNotificationDots);
+    }
+
+    public void init(TaskbarControllers controllers) {
+        mControllers = controllers;
+
+        NotificationListener.addNotificationsChangedListener(mPopupDataProvider);
+    }
+
+    public void onDestroy() {
+        NotificationListener.removeNotificationsChangedListener(mPopupDataProvider);
     }
 
     @NonNull
@@ -55,6 +79,38 @@
         mPopupDataProvider.setDeepShortcutMap(deepShortcutMapCopy);
     }
 
+    private void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
+        final PackageUserKey packageUserKey = new PackageUserKey(null, null);
+        Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
+                || updatedDots.test(packageUserKey);
+
+        LauncherBindableItemsContainer.ItemOperator op = (info, v) -> {
+            if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView) {
+                if (matcher.test(info)) {
+                    ((BubbleTextView) v).applyDotState(info, true /* animate */);
+                }
+            } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
+                FolderInfo fi = (FolderInfo) info;
+                if (fi.contents.stream().anyMatch(matcher)) {
+                    FolderDotInfo folderDotInfo = new FolderDotInfo();
+                    for (WorkspaceItemInfo si : fi.contents) {
+                        folderDotInfo.addDotInfo(mPopupDataProvider.getDotInfoForItem(si));
+                    }
+                    ((FolderIcon) v).setDotInfo(folderDotInfo);
+                }
+            }
+
+            // process all the shortcuts
+            return false;
+        };
+
+        mControllers.taskbarViewController.mapOverItems(op);
+        Folder folder = Folder.getOpen(mContext);
+        if (folder != null) {
+            folder.iterateOverItems(op);
+        }
+    }
+
     /**
      * Shows the notifications and deep shortcuts associated with a Taskbar {@param icon}.
      * @return the container if shown or null.
@@ -74,6 +130,8 @@
         final PopupContainerWithArrow<TaskbarActivityContext> container =
                 (PopupContainerWithArrow) context.getLayoutInflater().inflate(
                         R.layout.popup_container, context.getDragLayer(), false);
+        container.addOnAttachStateChangeListener(
+                new PopupLiveUpdateHandler<>(mContext, container));
         // TODO (b/198438631): configure for taskbar/context
 
         container.populateAndShow(icon,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 59393d7..c8d9fca 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -36,6 +36,7 @@
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.uioverrides.ApiWrapper;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.views.ActivityContext;
 
 /**
@@ -330,4 +331,14 @@
         // Consider the overall visibility
         return getVisibility() == VISIBLE;
     }
+
+    protected void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
+        // map over all the shortcuts on the taskbar
+        for (int i = 0; i < getChildCount(); i++) {
+            View item = getChildAt(i);
+            if (op.evaluate((ItemInfo) item.getTag(), item)) {
+                return;
+            }
+        }
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index c47bde9..96eac51 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -35,6 +35,7 @@
 import com.android.launcher3.anim.PendingAnimation;
 import com.android.launcher3.folder.FolderIcon;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
 import com.android.launcher3.util.MultiValueAlpha;
 import com.android.quickstep.AnimatedFloat;
 
@@ -243,6 +244,10 @@
         mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY());
     }
 
+    public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) {
+        mTaskbarView.mapOverItems(op);
+    }
+
     /**
      * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
      * touch bounds.
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 8154168..0656125 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -669,6 +669,8 @@
         return !isWorkspaceLoading();
     }
 
+    @NonNull
+    @Override
     public PopupDataProvider getPopupDataProvider() {
         return mPopupDataProvider;
     }
@@ -948,7 +950,7 @@
         hideKeyboard();
         logStopAndResume(false /* isResume */);
         mAppWidgetHost.setActivityStarted(false);
-        NotificationListener.removeNotificationsChangedListener();
+        NotificationListener.removeNotificationsChangedListener(getPopupDataProvider());
     }
 
     @Override
@@ -977,7 +979,7 @@
         mModel.validateModelDataOnResume();
 
         // Set the notification listener and fetch updated notifications when we resume
-        NotificationListener.setNotificationsChangedListener(mPopupDataProvider);
+        NotificationListener.addNotificationsChangedListener(mPopupDataProvider);
 
         DiscoveryBounce.showForHomeIfNeeded(this);
         mAppWidgetHost.setActivityResumed(true);
diff --git a/src/com/android/launcher3/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
index d27d8c7..ebe45a5 100644
--- a/src/com/android/launcher3/notification/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -29,12 +29,13 @@
 import android.view.View;
 
 import com.android.launcher3.AbstractFloatingView;
-import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.dot.DotInfo;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * An object that contains relevant information from a {@link StatusBarNotification}. This should
@@ -99,21 +100,23 @@
         if (intent == null) {
             return;
         }
-        final Launcher launcher = Launcher.getLauncher(view.getContext());
+        final ActivityContext context = ActivityContext.lookupContext(view.getContext());
         Bundle activityOptions = ActivityOptions.makeClipRevealAnimation(
                 view, 0, 0, view.getWidth(), view.getHeight()).toBundle();
         try {
             intent.send(null, 0, null, null, null, null, activityOptions);
-            launcher.getStatsLogManager().logger().withItemInfo(mItemInfo)
+            context.getStatsLogManager().logger().withItemInfo(mItemInfo)
                     .log(LAUNCHER_NOTIFICATION_LAUNCH_TAP);
         } catch (PendingIntent.CanceledException e) {
             e.printStackTrace();
         }
         if (autoCancel) {
-            launcher.getPopupDataProvider().cancelNotification(notificationKey);
+            PopupDataProvider popupDataProvider = context.getPopupDataProvider();
+            if (popupDataProvider != null) {
+                popupDataProvider.cancelNotification(notificationKey);
+            }
         }
-        AbstractFloatingView.closeOpenContainer(launcher, AbstractFloatingView
-                .TYPE_ACTION_POPUP);
+        AbstractFloatingView.closeOpenContainer(context, AbstractFloatingView.TYPE_ACTION_POPUP);
     }
 
     public Drawable getIconForBackground(Context context, int background) {
diff --git a/src/com/android/launcher3/notification/NotificationListener.java b/src/com/android/launcher3/notification/NotificationListener.java
index e58f5fa..bbeb886 100644
--- a/src/com/android/launcher3/notification/NotificationListener.java
+++ b/src/com/android/launcher3/notification/NotificationListener.java
@@ -30,6 +30,7 @@
 import android.service.notification.NotificationListenerService;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 
@@ -66,7 +67,8 @@
     private static final int MSG_RANKING_UPDATE = 5;
 
     private static NotificationListener sNotificationListenerInstance = null;
-    private static NotificationsChangedListener sNotificationsChangedListener;
+    private static final ArraySet<NotificationsChangedListener> sNotificationsChangedListeners =
+            new ArraySet<>();
     private static boolean sIsConnected;
 
     private final Handler mWorkerHandler;
@@ -94,8 +96,11 @@
         return sIsConnected ? sNotificationListenerInstance : null;
     }
 
-    public static void setNotificationsChangedListener(NotificationsChangedListener listener) {
-        sNotificationsChangedListener = listener;
+    public static void addNotificationsChangedListener(NotificationsChangedListener listener) {
+        if (listener == null) {
+            return;
+        }
+        sNotificationsChangedListeners.add(listener);
 
         NotificationListener notificationListener = getInstanceIfConnected();
         if (notificationListener != null) {
@@ -108,8 +113,10 @@
         }
     }
 
-    public static void removeNotificationsChangedListener() {
-        sNotificationsChangedListener = null;
+    public static void removeNotificationsChangedListener(NotificationsChangedListener listener) {
+        if (listener != null) {
+            sNotificationsChangedListeners.remove(listener);
+        }
     }
 
     private boolean handleWorkerMessage(Message message) {
@@ -180,23 +187,27 @@
     private boolean handleUiMessage(Message message) {
         switch (message.what) {
             case MSG_NOTIFICATION_POSTED:
-                if (sNotificationsChangedListener != null) {
+                if (sNotificationsChangedListeners.size() > 0) {
                     Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
-                    sNotificationsChangedListener.onNotificationPosted(
-                            msg.first, msg.second);
+                    for (NotificationsChangedListener listener : sNotificationsChangedListeners) {
+                        listener.onNotificationPosted(msg.first, msg.second);
+                    }
                 }
                 break;
             case MSG_NOTIFICATION_REMOVED:
-                if (sNotificationsChangedListener != null) {
+                if (sNotificationsChangedListeners.size() > 0) {
                     Pair<PackageUserKey, NotificationKeyData> msg = (Pair) message.obj;
-                    sNotificationsChangedListener.onNotificationRemoved(
-                            msg.first, msg.second);
+                    for (NotificationsChangedListener listener : sNotificationsChangedListeners) {
+                        listener.onNotificationRemoved(msg.first, msg.second);
+                    }
                 }
                 break;
             case MSG_NOTIFICATION_FULL_REFRESH:
-                if (sNotificationsChangedListener != null) {
-                    sNotificationsChangedListener.onNotificationFullRefresh(
-                            (List<StatusBarNotification>) message.obj);
+                if (sNotificationsChangedListeners.size() > 0) {
+                    for (NotificationsChangedListener listener : sNotificationsChangedListeners) {
+                        listener.onNotificationFullRefresh(
+                                (List<StatusBarNotification>) message.obj);
+                    }
                 }
                 break;
         }
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index f9ff8a6..16a4057 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -38,11 +38,12 @@
 
 import androidx.annotation.Nullable;
 
-import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
 
 /**
  * A {@link android.widget.FrameLayout} that contains a single notification,
@@ -320,9 +321,12 @@
     }
 
     public void onChildDismissed() {
-        Launcher launcher = Launcher.getLauncher(getContext());
-        launcher.getPopupDataProvider().cancelNotification(
-                mNotificationInfo.notificationKey);
-        launcher.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
+        ActivityContext activityContext = ActivityContext.lookupContext(getContext());
+        PopupDataProvider popupDataProvider = activityContext.getPopupDataProvider();
+        if (popupDataProvider == null) {
+            return;
+        }
+        popupDataProvider.cancelNotification(mNotificationInfo.notificationKey);
+        activityContext.getStatsLogManager().logger().log(LAUNCHER_NOTIFICATION_DISMISSED);
     }
 }
diff --git a/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java
new file mode 100644
index 0000000..731f439
--- /dev/null
+++ b/src/com/android/launcher3/popup/LauncherPopupLiveUpdateHandler.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2021 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.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.model.data.ItemInfo;
+
+/**
+ * Utility class to handle updates while the popup is visible on the Launcher
+ */
+public class LauncherPopupLiveUpdateHandler extends PopupLiveUpdateHandler<Launcher> {
+
+    public LauncherPopupLiveUpdateHandler(
+            Launcher launcher, PopupContainerWithArrow<Launcher> popupContainerWithArrow) {
+        super(launcher, popupContainerWithArrow);
+    }
+
+    private View getWidgetsView(ViewGroup container) {
+        for (int i = container.getChildCount() - 1; i >= 0; --i) {
+            View systemShortcutView = container.getChildAt(i);
+            if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
+                return systemShortcutView;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void onWidgetsBound() {
+        ItemInfo itemInfo = (ItemInfo) mPopupContainerWithArrow.getOriginalIcon().getTag();
+        SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mContext, itemInfo);
+        View widgetsView = getWidgetsView(mPopupContainerWithArrow);
+        if (widgetsView == null && mPopupContainerWithArrow.getWidgetContainer() != null) {
+            widgetsView = getWidgetsView(mPopupContainerWithArrow.getWidgetContainer());
+        }
+
+        if (widgetInfo != null && widgetsView == null) {
+            // We didn't have any widgets cached but now there are some, so enable the shortcut.
+            if (mPopupContainerWithArrow.getSystemShortcutContainer()
+                    != mPopupContainerWithArrow) {
+                if (mPopupContainerWithArrow.getWidgetContainer() == null) {
+                    mPopupContainerWithArrow.setWidgetContainer(
+                            mPopupContainerWithArrow.inflateAndAdd(
+                                    R.layout.widget_shortcut_container,
+                                    mPopupContainerWithArrow));
+                }
+                mPopupContainerWithArrow.initializeSystemShortcut(
+                        R.layout.system_shortcut,
+                        mPopupContainerWithArrow.getWidgetContainer(),
+                        widgetInfo);
+            } else {
+                // If using the expanded system shortcut (as opposed to just the icon), we need
+                // to reopen the container to ensure measurements etc. all work out. While this
+                // could be quite janky, in practice the user would typically see a small
+                // flicker as the animation restarts partway through, and this is a very rare
+                // edge case anyway.
+                mPopupContainerWithArrow.close(false);
+                PopupContainerWithArrow.showForIcon(mPopupContainerWithArrow.getOriginalIcon());
+            }
+        } else if (widgetInfo == null && widgetsView != null) {
+            // No widgets exist, but we previously added the shortcut so remove it.
+            if (mPopupContainerWithArrow.getSystemShortcutContainer()
+                    != mPopupContainerWithArrow
+                    && mPopupContainerWithArrow.getWidgetContainer() != null) {
+                mPopupContainerWithArrow.getWidgetContainer().removeView(widgetsView);
+            } else {
+                mPopupContainerWithArrow.close(false);
+                PopupContainerWithArrow.showForIcon(mPopupContainerWithArrow.getOriginalIcon());
+            }
+        }
+    }
+}
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 278f57d..aa8c70b 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -60,7 +60,6 @@
 import com.android.launcher3.notification.NotificationContainer;
 import com.android.launcher3.notification.NotificationInfo;
 import com.android.launcher3.notification.NotificationKeyData;
-import com.android.launcher3.popup.PopupDataProvider.PopupDataChangeListener;
 import com.android.launcher3.shortcuts.DeepShortcutView;
 import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
 import com.android.launcher3.touch.ItemLongClickListener;
@@ -72,9 +71,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -225,7 +222,8 @@
     }
 
     private void configureForLauncher(Launcher launcher) {
-        addOnAttachStateChangeListener(new LiveUpdateHandler(launcher));
+        addOnAttachStateChangeListener(new LauncherPopupLiveUpdateHandler(
+                launcher, (PopupContainerWithArrow<Launcher>) this));
         mPopupItemDragHandler = new LauncherPopupItemDragHandler(launcher, this);
         mAccessibilityDelegate = new ShortcutMenuAccessibilityDelegate(launcher);
         launcher.getDragController().addDragListener(this);
@@ -329,6 +327,26 @@
                 this, mShortcuts, notificationKeys));
     }
 
+    protected NotificationContainer getNotificationContainer() {
+        return mNotificationContainer;
+    }
+
+    protected BubbleTextView getOriginalIcon() {
+        return mOriginalIcon;
+    }
+
+    protected ViewGroup getSystemShortcutContainer() {
+        return mSystemShortcutContainer;
+    }
+
+    protected ViewGroup getWidgetContainer() {
+        return mWidgetContainer;
+    }
+
+    protected void setWidgetContainer(ViewGroup widgetContainer) {
+        mWidgetContainer = widgetContainer;
+    }
+
     private String getTitleForAccessibility() {
         return getContext().getString(mNumNotifications == 0 ?
                 R.string.action_deep_shortcut :
@@ -352,7 +370,7 @@
         }
     }
 
-    private void updateHiddenShortcuts() {
+    protected void updateHiddenShortcuts() {
         int allowedCount = mNotificationContainer != null
                 ? MAX_SHORTCUTS_IF_NOTIFICATIONS : MAX_SHORTCUTS;
 
@@ -363,7 +381,7 @@
         }
     }
 
-    private void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) {
+    protected void initializeSystemShortcut(int resId, ViewGroup container, SystemShortcut info) {
         View view = inflateAndAdd(
                 resId, container, getInsertIndexForSystemShortcut(container, info));
         if (view instanceof DeepShortcutView) {
@@ -436,7 +454,7 @@
         };
     }
 
-    private void updateNotificationHeader() {
+    protected void updateNotificationHeader() {
         ItemInfoWithIcon itemInfo = (ItemInfoWithIcon) mOriginalIcon.getTag();
         DotInfo dotInfo = mActivityContext.getDotInfoForItem(itemInfo);
         if (mNotificationContainer != null && dotInfo != null) {
@@ -495,118 +513,6 @@
     }
 
     /**
-     * Utility class to handle updates while the popup is visible (like widgets and
-     * notification changes)
-     */
-    private class LiveUpdateHandler implements
-            PopupDataChangeListener, View.OnAttachStateChangeListener {
-
-        private final Launcher mLauncher;
-
-        LiveUpdateHandler(Launcher launcher) {
-            mLauncher = launcher;
-        }
-
-        @Override
-        public void onViewAttachedToWindow(View view) {
-            mLauncher.getPopupDataProvider().setChangeListener(this);
-        }
-
-        @Override
-        public void onViewDetachedFromWindow(View view) {
-            mLauncher.getPopupDataProvider().setChangeListener(null);
-        }
-
-        private View getWidgetsView(ViewGroup container) {
-            for (int i = container.getChildCount() - 1; i >= 0; --i) {
-                View systemShortcutView = container.getChildAt(i);
-                if (systemShortcutView.getTag() instanceof SystemShortcut.Widgets) {
-                    return systemShortcutView;
-                }
-            }
-            return null;
-        }
-
-        @Override
-        public void onWidgetsBound() {
-            ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
-            SystemShortcut widgetInfo = SystemShortcut.WIDGETS.getShortcut(mLauncher, itemInfo);
-            View widgetsView = getWidgetsView(PopupContainerWithArrow.this);
-            if (widgetsView == null && mWidgetContainer != null) {
-                widgetsView = getWidgetsView(mWidgetContainer);
-            }
-
-            if (widgetInfo != null && widgetsView == null) {
-                // We didn't have any widgets cached but now there are some, so enable the shortcut.
-                if (mSystemShortcutContainer != PopupContainerWithArrow.this) {
-                    if (mWidgetContainer == null) {
-                        mWidgetContainer = inflateAndAdd(R.layout.widget_shortcut_container,
-                                PopupContainerWithArrow.this);
-                    }
-                    initializeSystemShortcut(R.layout.system_shortcut, mWidgetContainer,
-                            widgetInfo);
-                } else {
-                    // If using the expanded system shortcut (as opposed to just the icon), we need
-                    // to reopen the container to ensure measurements etc. all work out. While this
-                    // could be quite janky, in practice the user would typically see a small
-                    // flicker as the animation restarts partway through, and this is a very rare
-                    // edge case anyway.
-                    close(false);
-                    PopupContainerWithArrow.showForIcon(mOriginalIcon);
-                }
-            } else if (widgetInfo == null && widgetsView != null) {
-                // No widgets exist, but we previously added the shortcut so remove it.
-                if (mSystemShortcutContainer
-                        != PopupContainerWithArrow.this
-                        && mWidgetContainer != null) {
-                    mWidgetContainer.removeView(widgetsView);
-                } else {
-                    close(false);
-                    PopupContainerWithArrow.showForIcon(mOriginalIcon);
-                }
-            }
-        }
-
-        /**
-         * Updates the notification header if the original icon's dot updated.
-         */
-        @Override
-        public void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) {
-            ItemInfo itemInfo = (ItemInfo) mOriginalIcon.getTag();
-            PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
-            if (updatedDots.test(packageUser)) {
-                updateNotificationHeader();
-            }
-        }
-
-
-        @Override
-        public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
-            if (mNotificationContainer == null) {
-                return;
-            }
-            ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
-            DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo));
-            if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) {
-                // No more notifications, remove the notification views and expand all shortcuts.
-                mNotificationContainer.setVisibility(GONE);
-                updateHiddenShortcuts();
-                assignMarginsAndBackgrounds(PopupContainerWithArrow.this);
-                updateArrowColor();
-            } else {
-                mNotificationContainer.trimNotifications(
-                        NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
-            }
-        }
-
-        @Override
-        public void onSystemShortcutsUpdated() {
-            close(true);
-            PopupContainerWithArrow.showForIcon(mOriginalIcon);
-        }
-    }
-
-    /**
      * Dismisses the popup if it is no longer valid
      */
     public static void dismissInvalidPopup(BaseDraggingActivity activity) {
diff --git a/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
new file mode 100644
index 0000000..194c22f
--- /dev/null
+++ b/src/com/android/launcher3/popup/PopupLiveUpdateHandler.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.popup;
+
+import static android.view.View.GONE;
+
+import android.content.Context;
+import android.view.View;
+
+import com.android.launcher3.dot.DotInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.notification.NotificationContainer;
+import com.android.launcher3.notification.NotificationKeyData;
+import com.android.launcher3.util.PackageUserKey;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * Utility class to handle updates while the popup is visible (like widgets and
+ * notification changes)
+ *
+ * @param <T> The activity on which the popup shows
+ */
+public class PopupLiveUpdateHandler<T extends Context & ActivityContext> implements
+        PopupDataProvider.PopupDataChangeListener, View.OnAttachStateChangeListener {
+
+    protected final T mContext;
+    protected final PopupContainerWithArrow<T> mPopupContainerWithArrow;
+
+    public PopupLiveUpdateHandler(
+            T context, PopupContainerWithArrow<T> popupContainerWithArrow) {
+        mContext = context;
+        mPopupContainerWithArrow = popupContainerWithArrow;
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View view) {
+        PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+
+        if (popupDataProvider != null) {
+            popupDataProvider.setChangeListener(this);
+        }
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View view) {
+        PopupDataProvider popupDataProvider = mContext.getPopupDataProvider();
+
+        if (popupDataProvider != null) {
+            popupDataProvider.setChangeListener(null);
+        }
+    }
+
+    /**
+     * Updates the notification header if the original icon's dot updated.
+     */
+    @Override
+    public void onNotificationDotsUpdated(Predicate<PackageUserKey> updatedDots) {
+        ItemInfo itemInfo = (ItemInfo) mPopupContainerWithArrow.getOriginalIcon().getTag();
+        PackageUserKey packageUser = PackageUserKey.fromItemInfo(itemInfo);
+        if (updatedDots.test(packageUser)) {
+            mPopupContainerWithArrow.updateNotificationHeader();
+        }
+    }
+
+
+    @Override
+    public void trimNotifications(Map<PackageUserKey, DotInfo> updatedDots) {
+        NotificationContainer notificationContainer =
+                mPopupContainerWithArrow.getNotificationContainer();
+        if (notificationContainer == null) {
+            return;
+        }
+        ItemInfo originalInfo = (ItemInfo) mPopupContainerWithArrow.getOriginalIcon().getTag();
+        DotInfo dotInfo = updatedDots.get(PackageUserKey.fromItemInfo(originalInfo));
+        if (dotInfo == null || dotInfo.getNotificationKeys().size() == 0) {
+            // No more notifications, remove the notification views and expand all shortcuts.
+            notificationContainer.setVisibility(GONE);
+            mPopupContainerWithArrow.updateHiddenShortcuts();
+            mPopupContainerWithArrow.assignMarginsAndBackgrounds(mPopupContainerWithArrow);
+            mPopupContainerWithArrow.updateArrowColor();
+        } else {
+            notificationContainer.trimNotifications(
+                    NotificationKeyData.extractKeysOnly(dotInfo.getNotificationKeys()));
+        }
+    }
+
+    @Override
+    public void onSystemShortcutsUpdated() {
+        mPopupContainerWithArrow.close(true);
+        PopupContainerWithArrow.showForIcon(mPopupContainerWithArrow.getOriginalIcon());
+    }
+}
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
index 1820933..0d8602f 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDragLayer.java
@@ -33,6 +33,7 @@
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ShortcutUtil;
 import com.android.launcher3.util.TouchController;
 import com.android.launcher3.views.BaseDragLayer;
@@ -177,12 +178,16 @@
         if (!ShortcutUtil.supportsShortcuts(item)) {
             return false;
         }
+        PopupDataProvider popupDataProvider = mActivity.getPopupDataProvider();
+        if (popupDataProvider == null) {
+            return false;
+        }
         final PopupContainerWithArrow container =
                 (PopupContainerWithArrow) mActivity.getLayoutInflater().inflate(
                         R.layout.popup_container, mActivity.getDragLayer(), false);
 
         container.populateAndShow((BubbleTextView) v,
-                mActivity.getPopupDataProvider().getShortcutCountForItem(item),
+                popupDataProvider.getShortcutCountForItem(item),
                 Collections.emptyList(),
                 Arrays.asList(mPinnedAppsAdapter.getSystemShortcut(item),
                         APP_INFO.getShortcut(mActivity, item)));
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index a2e4ad6..e09eff6 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -31,6 +31,7 @@
 import com.android.launcher3.logger.LauncherAtom;
 import com.android.launcher3.logging.StatsLogManager;
 import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.popup.PopupDataProvider;
 import com.android.launcher3.util.ViewCache;
 
 /**
@@ -166,4 +167,9 @@
             // No op.
         };
     }
+
+    @Nullable
+    default PopupDataProvider getPopupDataProvider() {
+        return null;
+    }
 }