Move bubble icon / content loading into its own controller class

I think this simplifies the BubbleBarController class and makes it
a bit easier to deal with the optional overflow (done in other CLs).

Flag: com.android.wm.shell.enable_bubble_bar
Test: manual - add some bubble to bubble bar, check that the overflow
               is there & can be opened
Bug: 334175587
Change-Id: Ie17fa0279a981a019d5d50b9f71dc87f49d98dee
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index e4f7262..2cb37ff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -108,6 +108,7 @@
 import com.android.launcher3.taskbar.bubbles.BubbleBarView;
 import com.android.launcher3.taskbar.bubbles.BubbleBarViewController;
 import com.android.launcher3.taskbar.bubbles.BubbleControllers;
+import com.android.launcher3.taskbar.bubbles.BubbleCreator;
 import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
 import com.android.launcher3.taskbar.bubbles.BubbleDragController;
 import com.android.launcher3.taskbar.bubbles.BubblePinController;
@@ -296,7 +297,8 @@
                     new BubbleBarPinController(this, mDragLayer,
                             () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
                     new BubblePinController(this, mDragLayer,
-                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize)
+                            () -> DisplayController.INSTANCE.get(this).getInfo().currentSize),
+                    new BubbleCreator(this)
             ));
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 58cd042..fbee080 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -15,13 +15,8 @@
  */
 package com.android.launcher3.taskbar.bubbles;
 
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
 import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
 
-import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
@@ -34,35 +29,12 @@
 import android.annotation.BinderThread;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
-import android.content.pm.ShortcutInfo;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Path;
 import android.graphics.Point;
-import android.graphics.drawable.AdaptiveIconDrawable;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
 import android.os.Bundle;
 import android.os.SystemProperties;
-import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Log;
-import android.util.PathParser;
-import android.view.LayoutInflater;
 
-import androidx.appcompat.content.res.AppCompatResources;
-
-import com.android.internal.graphics.ColorUtils;
-import com.android.launcher3.R;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.BubbleIconFactory;
-import com.android.launcher3.shortcuts.ShortcutRequest;
 import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController;
 import com.android.launcher3.util.Executors.SimpleThreadFactory;
 import com.android.quickstep.SystemUiProxy;
@@ -136,18 +108,16 @@
 
     private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
             new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
-    private final LauncherApps mLauncherApps;
-    private final BubbleIconFactory mIconFactory;
     private final SystemUiProxy mSystemUiProxy;
 
     private BubbleBarItem mSelectedBubble;
-    private BubbleBarOverflow mOverflowBubble;
 
     private ImeVisibilityChecker mImeVisibilityChecker;
     private BubbleBarViewController mBubbleBarViewController;
     private BubbleStashController mBubbleStashController;
     private Optional<BubbleStashedHandleViewController> mBubbleStashedHandleViewController;
     private BubblePinController mBubblePinController;
+    private BubbleCreator mBubbleCreator;
 
     // Cache last sent top coordinate to avoid sending duplicate updates to shell
     private int mLastSentBubbleBarTop;
@@ -198,13 +168,6 @@
         if (sBubbleBarEnabled) {
             mSystemUiProxy.setBubblesListener(this);
         }
-        mLauncherApps = context.getSystemService(LauncherApps.class);
-        mIconFactory = new BubbleIconFactory(context,
-                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
-                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
-                context.getResources().getColor(R.color.important_conversation),
-                context.getResources().getDimensionPixelSize(
-                        com.android.internal.R.dimen.importance_ring_stroke_width));
     }
 
     public void onDestroy() {
@@ -219,6 +182,7 @@
         mBubbleStashController = bubbleControllers.bubbleStashController;
         mBubbleStashedHandleViewController = bubbleControllers.bubbleStashedHandleViewController;
         mBubblePinController = bubbleControllers.bubblePinController;
+        mBubbleCreator = bubbleControllers.bubbleCreator;
 
         bubbleControllers.runAfterInit(() -> {
             mBubbleBarViewController.setHiddenForBubbles(
@@ -233,27 +197,6 @@
     }
 
     /**
-     * Creates and adds the overflow bubble to the bubble bar if it hasn't been created yet.
-     *
-     * <p>This should be called on the {@link #BUBBLE_STATE_EXECUTOR} executor to avoid inflating
-     * the overflow multiple times.
-     */
-    private void createAndAddOverflowIfNeeded() {
-        if (mOverflowBubble == null) {
-            BubbleBarOverflow overflow = createOverflow(mContext);
-            MAIN_EXECUTOR.execute(() -> {
-                // we're on the main executor now, so check that the overflow hasn't been created
-                // again to avoid races.
-                if (mOverflowBubble == null) {
-                    mBubbleBarViewController.addBubble(
-                            overflow, /* isExpanding= */ false, /* suppressAnimation= */ true);
-                    mOverflowBubble = overflow;
-                }
-            });
-        }
-    }
-
-    /**
      * Updates the bubble bar, handle bar, and stash controllers based on sysui state flags.
      */
     public void updateStateForSysuiFlags(@SystemUiStateFlags long flags) {
@@ -283,23 +226,25 @@
                 || !update.currentBubbleList.isEmpty()) {
             // We have bubbles to load
             BUBBLE_STATE_EXECUTOR.execute(() -> {
-                createAndAddOverflowIfNeeded();
                 if (update.addedBubble != null) {
-                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
+                    viewUpdate.addedBubble = mBubbleCreator.populateBubble(mContext,
+                            update.addedBubble,
+                            mBarView,
                             null /* existingBubble */);
                 }
                 if (update.updatedBubble != null) {
                     BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                     viewUpdate.updatedBubble =
-                            populateBubble(mContext, update.updatedBubble, mBarView,
+                            mBubbleCreator.populateBubble(mContext, update.updatedBubble,
+                                    mBarView,
                                     existingBubble);
                 }
                 if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
                     List<BubbleBarBubble> currentBubbles = new ArrayList<>();
                     for (int i = 0; i < update.currentBubbleList.size(); i++) {
-                        BubbleBarBubble b =
-                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
-                                        null /* existingBubble */);
+                        BubbleBarBubble b = mBubbleCreator.populateBubble(mContext,
+                                update.currentBubbleList.get(i), mBarView,
+                                null /* existingBubble */);
                         currentBubbles.add(b);
                     }
                     viewUpdate.currentBubbles = currentBubbles;
@@ -530,133 +475,6 @@
     // Loading data for the bubbles
     //
 
-    @Nullable
-    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
-            @Nullable BubbleBarBubble existingBubble) {
-        String appName;
-        Bitmap badgeBitmap;
-        Bitmap bubbleBitmap;
-        Path dotPath;
-        int dotColor;
-
-        boolean isImportantConvo = b.isImportantConversation();
-
-        ShortcutRequest.QueryResult result = new ShortcutRequest(context,
-                new UserHandle(b.getUserId()))
-                .forPackage(b.getPackageName(), b.getShortcutId())
-                .query(FLAG_MATCH_DYNAMIC
-                        | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
-                        | FLAG_MATCH_CACHED
-                        | FLAG_GET_PERSONS_DATA);
-
-        ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
-        if (shortcutInfo == null) {
-            Log.w(TAG, "No shortcutInfo found for bubble: " + b.getKey()
-                    + " with shortcutId: " + b.getShortcutId());
-        }
-
-        ApplicationInfo appInfo;
-        try {
-            appInfo = mLauncherApps.getApplicationInfo(
-                    b.getPackageName(),
-                    0,
-                    new UserHandle(b.getUserId()));
-        } catch (PackageManager.NameNotFoundException e) {
-            // If we can't find package... don't think we should show the bubble.
-            Log.w(TAG, "Unable to find packageName: " + b.getPackageName());
-            return null;
-        }
-        if (appInfo == null) {
-            Log.w(TAG, "Unable to find appInfo: " + b.getPackageName());
-            return null;
-        }
-        PackageManager pm = context.getPackageManager();
-        appName = String.valueOf(appInfo.loadLabel(pm));
-        Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
-        Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(b.getUserId()));
-
-        // Badged bubble image
-        Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
-                b.getIcon());
-        if (bubbleDrawable == null) {
-            // Default to app icon
-            bubbleDrawable = appIcon;
-        }
-
-        BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
-        badgeBitmap = badgeBitmapInfo.icon;
-
-        float[] bubbleBitmapScale = new float[1];
-        bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
-
-        // Dot color & placement
-        Path iconPath = PathParser.createPathFromPathData(
-                context.getResources().getString(
-                        com.android.internal.R.string.config_icon_mask));
-        Matrix matrix = new Matrix();
-        float scale = bubbleBitmapScale[0];
-        float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
-        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
-                radius /* pivot y */);
-        iconPath.transform(matrix);
-        dotPath = iconPath;
-        dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
-                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
-
-        if (existingBubble == null) {
-            LayoutInflater inflater = LayoutInflater.from(context);
-            BubbleView bubbleView = (BubbleView) inflater.inflate(
-                    R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
-
-            BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
-                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
-            bubbleView.setBubble(bubble);
-            return bubble;
-        } else {
-            // If we already have a bubble (so it already has an inflated view), update it.
-            existingBubble.setInfo(b);
-            existingBubble.setBadge(badgeBitmap);
-            existingBubble.setIcon(bubbleBitmap);
-            existingBubble.setDotColor(dotColor);
-            existingBubble.setDotPath(dotPath);
-            existingBubble.setAppName(appName);
-            return existingBubble;
-        }
-    }
-
-    private BubbleBarOverflow createOverflow(Context context) {
-        Bitmap bitmap = createOverflowBitmap(context);
-        LayoutInflater inflater = LayoutInflater.from(context);
-        BubbleView bubbleView = (BubbleView) inflater.inflate(
-                R.layout.bubble_bar_overflow_button, mBarView, false /* attachToRoot */);
-        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
-        bubbleView.setOverflow(overflow, bitmap);
-        return overflow;
-    }
-
-    private Bitmap createOverflowBitmap(Context context) {
-        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
-                R.drawable.bubble_ic_overflow_button);
-
-        final TypedArray ta = mContext.obtainStyledAttributes(
-                new int[]{
-                        R.attr.materialColorOnPrimaryFixed,
-                        R.attr.materialColorPrimaryFixed
-                });
-        int overflowIconColor = ta.getColor(0, Color.WHITE);
-        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
-        ta.recycle();
-
-        iconDrawable.setTint(overflowIconColor);
-
-        int inset = context.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
-        Drawable foreground = new InsetDrawable(iconDrawable, inset);
-        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
-                foreground);
-
-        return mIconFactory.createBadgedIconBitmap(drawable).icon;
-    }
-
     private void onBubbleBarBoundsChanged() {
         int newTop = mBarView.getRestingTopPositionOnScreen();
         if (newTop != mLastSentBubbleBarTop) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 916b7b1..590916e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -75,6 +75,7 @@
     private View.OnClickListener mBubbleClickListener;
     private View.OnClickListener mBubbleBarClickListener;
     private BubbleView.Controller mBubbleViewController;
+    private BubbleBarOverflow mOverflowBubble;
 
     // These are exposed to {@link BubbleStashController} to animate for stashing/un-stashing
     private final MultiValueAlpha mBubbleBarAlpha;
@@ -121,6 +122,8 @@
         mBubbleClickListener = v -> onBubbleClicked((BubbleView) v);
         mBubbleBarClickListener = v -> expandBubbleBar();
         mBubbleDragController.setupBubbleBarView(mBarView);
+        mOverflowBubble = bubbleControllers.bubbleCreator.createOverflow(mBarView);
+        addOverflow();
         mBarView.setOnClickListener(mBubbleBarClickListener);
         mBarView.addOnLayoutChangeListener(
                 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
@@ -492,6 +495,15 @@
     }
 
     /**
+     * Adds the overflow view to the bubble bar.
+     */
+    public void addOverflow() {
+        mBarView.addBubble(mOverflowBubble.getView());
+        mOverflowBubble.getView().setOnClickListener(mBubbleClickListener);
+        mOverflowBubble.getView().setController(mBubbleViewController);
+    }
+
+    /**
      * Adds the provided bubble to the bubble bar.
      */
     public void addBubble(BubbleBarItem b, boolean isExpanding, boolean suppressAnimation) {
@@ -501,10 +513,6 @@
             mBubbleDragController.setupBubbleView(b.getView());
             b.getView().setController(mBubbleViewController);
 
-            if (b instanceof BubbleBarOverflow) {
-                return;
-            }
-
             if (suppressAnimation || !(b instanceof BubbleBarBubble bubble)) {
                 // the bubble bar and handle are initialized as part of the first bubble animation.
                 // if the animation is suppressed, immediately stash or show the bubble bar to
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
index a5243fa..8478dc2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleControllers.java
@@ -33,6 +33,7 @@
     public final BubbleDismissController bubbleDismissController;
     public final BubbleBarPinController bubbleBarPinController;
     public final BubblePinController bubblePinController;
+    public final BubbleCreator bubbleCreator;
 
     private final RunnableList mPostInitRunnables = new RunnableList();
 
@@ -49,7 +50,8 @@
             BubbleDragController bubbleDragController,
             BubbleDismissController bubbleDismissController,
             BubbleBarPinController bubbleBarPinController,
-            BubblePinController bubblePinController) {
+            BubblePinController bubblePinController,
+            BubbleCreator bubbleCreator) {
         this.bubbleBarController = bubbleBarController;
         this.bubbleBarViewController = bubbleBarViewController;
         this.bubbleStashController = bubbleStashController;
@@ -58,6 +60,7 @@
         this.bubbleDismissController = bubbleDismissController;
         this.bubbleBarPinController = bubbleBarPinController;
         this.bubblePinController = bubblePinController;
+        this.bubbleCreator = bubbleCreator;
     }
 
     /**
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
new file mode 100644
index 0000000..8e9a2f6
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleCreator.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2024 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.taskbar.bubbles;
+
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_GET_PERSONS_DATA;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_CACHED;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC;
+import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED_BY_ANY_LAUNCHER;
+
+import static com.android.launcher3.icons.FastBitmapDrawable.WHITE_SCRIM_ALPHA;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.PathParser;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.appcompat.content.res.AppCompatResources;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.launcher3.shortcuts.ShortcutRequest;
+import com.android.wm.shell.common.bubbles.BubbleInfo;
+
+/**
+ * Loads the necessary info to populate / present a bubble (name, icon, shortcut).
+ */
+public class BubbleCreator {
+
+    private static final String TAG = BubbleCreator.class.getSimpleName();
+
+    private final Context mContext;
+    private final LauncherApps mLauncherApps;
+    private final BubbleIconFactory mIconFactory;
+
+    public BubbleCreator(Context context) {
+        mContext = context;
+        mLauncherApps = mContext.getSystemService(LauncherApps.class);
+        mIconFactory = new BubbleIconFactory(context,
+                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
+                context.getResources().getDimensionPixelSize(R.dimen.bubblebar_badge_size),
+                context.getResources().getColor(R.color.important_conversation),
+                context.getResources().getDimensionPixelSize(
+                        com.android.internal.R.dimen.importance_ring_stroke_width));
+    }
+
+    /**
+     * Creates a BubbleBarBubble object, including the view if needed, and populates it with
+     * the info needed for presentation.
+     *
+     * @param context the context to use for inflation.
+     * @param info the info to use to populate the bubble.
+     * @param barView the parent view for the bubble (bubble is not added to the view).
+     * @param existingBubble if a bubble exists already, this object gets updated with the new
+     *                       info & returned (& any existing views are reused instead of inflating
+     *                       new ones.
+     */
+    @Nullable
+    public BubbleBarBubble populateBubble(Context context, BubbleInfo info, ViewGroup barView,
+            @Nullable BubbleBarBubble existingBubble) {
+        String appName;
+        Bitmap badgeBitmap;
+        Bitmap bubbleBitmap;
+        Path dotPath;
+        int dotColor;
+
+        boolean isImportantConvo = info.isImportantConversation();
+
+        ShortcutRequest.QueryResult result = new ShortcutRequest(context,
+                new UserHandle(info.getUserId()))
+                .forPackage(info.getPackageName(), info.getShortcutId())
+                .query(FLAG_MATCH_DYNAMIC
+                        | FLAG_MATCH_PINNED_BY_ANY_LAUNCHER
+                        | FLAG_MATCH_CACHED
+                        | FLAG_GET_PERSONS_DATA);
+
+        ShortcutInfo shortcutInfo = result.size() > 0 ? result.get(0) : null;
+        if (shortcutInfo == null) {
+            Log.w(TAG, "No shortcutInfo found for bubble: " + info.getKey()
+                    + " with shortcutId: " + info.getShortcutId());
+        }
+
+        ApplicationInfo appInfo;
+        try {
+            appInfo = mLauncherApps.getApplicationInfo(
+                    info.getPackageName(),
+                    0,
+                    new UserHandle(info.getUserId()));
+        } catch (PackageManager.NameNotFoundException e) {
+            // If we can't find package... don't think we should show the bubble.
+            Log.w(TAG, "Unable to find packageName: " + info.getPackageName());
+            return null;
+        }
+        if (appInfo == null) {
+            Log.w(TAG, "Unable to find appInfo: " + info.getPackageName());
+            return null;
+        }
+        PackageManager pm = context.getPackageManager();
+        appName = String.valueOf(appInfo.loadLabel(pm));
+        Drawable appIcon = appInfo.loadUnbadgedIcon(pm);
+        Drawable badgedIcon = pm.getUserBadgedIcon(appIcon, new UserHandle(info.getUserId()));
+
+        // Badged bubble image
+        Drawable bubbleDrawable = mIconFactory.getBubbleDrawable(context, shortcutInfo,
+                info.getIcon());
+        if (bubbleDrawable == null) {
+            // Default to app icon
+            bubbleDrawable = appIcon;
+        }
+
+        BitmapInfo badgeBitmapInfo = mIconFactory.getBadgeBitmap(badgedIcon, isImportantConvo);
+        badgeBitmap = badgeBitmapInfo.icon;
+
+        float[] bubbleBitmapScale = new float[1];
+        bubbleBitmap = mIconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale);
+
+        // Dot color & placement
+        Path iconPath = PathParser.createPathFromPathData(
+                context.getResources().getString(
+                        com.android.internal.R.string.config_icon_mask));
+        Matrix matrix = new Matrix();
+        float scale = bubbleBitmapScale[0];
+        float radius = BubbleView.DEFAULT_PATH_SIZE / 2f;
+        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+                radius /* pivot y */);
+        iconPath.transform(matrix);
+        dotPath = iconPath;
+        dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
+                Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
+
+        if (existingBubble == null) {
+            LayoutInflater inflater = LayoutInflater.from(context);
+            BubbleView bubbleView = (BubbleView) inflater.inflate(
+                    R.layout.bubblebar_item_view, barView, false /* attachToRoot */);
+
+            BubbleBarBubble bubble = new BubbleBarBubble(info, bubbleView,
+                    badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
+            bubbleView.setBubble(bubble);
+            return bubble;
+        } else {
+            // If we already have a bubble (so it already has an inflated view), update it.
+            existingBubble.setInfo(info);
+            existingBubble.setBadge(badgeBitmap);
+            existingBubble.setIcon(bubbleBitmap);
+            existingBubble.setDotColor(dotColor);
+            existingBubble.setDotPath(dotPath);
+            existingBubble.setAppName(appName);
+            return existingBubble;
+        }
+    }
+
+    /**
+     * Creates the overflow view shown in the bubble bar.
+     *
+     * @param barView the parent view for the bubble (bubble is not added to the view).
+     */
+    public BubbleBarOverflow createOverflow(ViewGroup barView) {
+        Bitmap bitmap = createOverflowBitmap();
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        BubbleView bubbleView = (BubbleView) inflater.inflate(
+                R.layout.bubble_bar_overflow_button, barView, false /* attachToRoot */);
+        BubbleBarOverflow overflow = new BubbleBarOverflow(bubbleView);
+        bubbleView.setOverflow(overflow, bitmap);
+        return overflow;
+    }
+
+    private Bitmap createOverflowBitmap() {
+        Drawable iconDrawable = AppCompatResources.getDrawable(mContext,
+                R.drawable.bubble_ic_overflow_button);
+
+        final TypedArray ta = mContext.obtainStyledAttributes(
+                new int[]{
+                        R.attr.materialColorOnPrimaryFixed,
+                        R.attr.materialColorPrimaryFixed
+                });
+        int overflowIconColor = ta.getColor(0, Color.WHITE);
+        int overflowBackgroundColor = ta.getColor(1, Color.BLACK);
+        ta.recycle();
+
+        iconDrawable.setTint(overflowIconColor);
+
+        int inset = mContext.getResources().getDimensionPixelSize(R.dimen.bubblebar_overflow_inset);
+        Drawable foreground = new InsetDrawable(iconDrawable, inset);
+        Drawable drawable = new AdaptiveIconDrawable(new ColorDrawable(overflowBackgroundColor),
+                foreground);
+
+        return mIconFactory.createBadgedIconBitmap(drawable).icon;
+    }
+
+}