Inflate & make bitmaps for bubble views in background

* Simple AsyncTask to load bubble content
* BubbleController finds out about a notification being posted, if it
  is a bubble, BubbleController creates the bubble & tells it to "load"
  once it's done, BubbleData does its thing as per usual
* Anywhere we need to "reload" the bubbles (e.g. theme change) should
  use the async task
* Updates tests to work with these changes

Test: atest SystemUITests
Bug: 144719337
Change-Id: If55f27a517bff0c1f467722966a7b3b7075e9403
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index 6e03441..dc24996 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -17,18 +17,14 @@
 
 import android.annotation.Nullable;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Matrix;
 import android.graphics.Path;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.util.PathParser;
 import android.widget.ImageView;
 
-import com.android.internal.graphics.ColorUtils;
-import com.android.launcher3.icons.BitmapInfo;
 import com.android.launcher3.icons.DotRenderer;
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
@@ -43,9 +39,9 @@
 public class BadgedImageView extends ImageView {
 
     /** Same value as Launcher3 dot code */
-    private static final float WHITE_SCRIM_ALPHA = 0.54f;
+    public static final float WHITE_SCRIM_ALPHA = 0.54f;
     /** Same as value in Launcher3 IconShape */
-    private static final int DEFAULT_PATH_SIZE = 100;
+    public static final int DEFAULT_PATH_SIZE = 100;
 
     static final int DOT_STATE_DEFAULT = 0;
     static final int DOT_STATE_SUPPRESSED_FOR_FLYOUT = 1;
@@ -55,7 +51,6 @@
     private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT;
 
     private Bubble mBubble;
-    private BubbleIconFactory mBubbleIconFactory;
 
     private int mIconBitmapSize;
     private DotRenderer mDotRenderer;
@@ -91,6 +86,18 @@
         mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
     }
 
+    /**
+     * Updates the view with provided info.
+     */
+    public void update(Bubble bubble, Bitmap bubbleImage, int dotColor, Path dotPath) {
+        mBubble = bubble;
+        setImageBitmap(bubbleImage);
+        setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
+        mDotColor = dotColor;
+        drawDot(dotPath);
+        animateDot();
+    }
+
     @Override
     public void onDraw(Canvas canvas) {
         super.onDraw(canvas);
@@ -137,14 +144,6 @@
     }
 
     /**
-     * The colour to use for the dot.
-     */
-    void setDotColor(int color) {
-        mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */);
-        invalidate();
-    }
-
-    /**
      * @param iconPath The new icon path to use when calculating dot position.
      */
     void drawDot(Path iconPath) {
@@ -184,25 +183,6 @@
     }
 
     /**
-     * Populates this view with a bubble.
-     * <p>
-     * This should only be called when a new bubble is being set on the view, updates to the
-     * current bubble should use {@link #update(Bubble)}.
-     *
-     * @param bubble the bubble to display in this view.
-     */
-    public void setBubble(Bubble bubble) {
-        mBubble = bubble;
-    }
-
-    /**
-     * @param factory Factory for creating normalized bubble icons.
-     */
-    public void setBubbleIconFactory(BubbleIconFactory factory) {
-        mBubbleIconFactory = factory;
-    }
-
-    /**
      * The key for the {@link Bubble} associated with this view, if one exists.
      */
     @Nullable
@@ -210,15 +190,6 @@
         return (mBubble != null) ? mBubble.getKey() : null;
     }
 
-    /**
-     * Updates the UI based on the bubble, updates badge and animates messages as needed.
-     */
-    public void update(Bubble bubble) {
-        mBubble = bubble;
-        setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
-        updateViews();
-    }
-
     int getDotColor() {
         return mDotColor;
     }
@@ -274,34 +245,4 @@
                     }
                 }).start();
     }
-
-    void updateViews() {
-        if (mBubble == null || mBubbleIconFactory == null) {
-            return;
-        }
-
-        Drawable bubbleDrawable = mBubbleIconFactory.getBubbleDrawable(mBubble, mContext);
-        BitmapInfo badgeBitmapInfo = mBubbleIconFactory.getBadgeBitmap(mBubble);
-        BitmapInfo bubbleBitmapInfo = mBubbleIconFactory.getBubbleBitmap(bubbleDrawable,
-                badgeBitmapInfo);
-        setImageBitmap(bubbleBitmapInfo.icon);
-
-        // Update badge.
-        mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
-        setDotColor(mDotColor);
-
-        // Update dot.
-        Path iconPath = PathParser.createPathFromPathData(
-                getResources().getString(com.android.internal.R.string.config_icon_mask));
-        Matrix matrix = new Matrix();
-        float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
-                null /* outBounds */, null /* path */, null /* outMaskShape */);
-        float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
-        matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
-                radius /* pivot y */);
-        iconPath.transform(matrix);
-        drawDot(iconPath);
-
-        animateDot();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 7934e10..77c8e0b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -16,6 +16,7 @@
 package com.android.systemui.bubbles;
 
 
+import static android.os.AsyncTask.Status.FINISHED;
 import static android.view.Display.INVALID_DISPLAY;
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
@@ -26,20 +27,17 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ShortcutInfo;
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Parcelable;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.LayoutInflater;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
@@ -59,19 +57,19 @@
     private NotificationEntry mEntry;
     private final String mKey;
     private final String mGroupId;
-    private String mAppName;
-    private Drawable mUserBadgedAppIcon;
-    private ShortcutInfo mShortcutInfo;
-
-    private boolean mInflated;
-    private BadgedImageView mIconView;
-    private BubbleExpandedView mExpandedView;
-    private BubbleIconFactory mBubbleIconFactory;
 
     private long mLastUpdated;
     private long mLastAccessed;
 
-    private boolean mIsUserCreated;
+    // Items that are typically loaded later
+    private String mAppName;
+    private ShortcutInfo mShortcutInfo;
+    private BadgedImageView mIconView;
+    private BubbleExpandedView mExpandedView;
+
+    private boolean mInflated;
+    private BubbleViewInfoTask mInflationTask;
+    private boolean mInflateSynchronously;
 
     /**
      * Whether this notification should be shown in the shade when it is also displayed as a bubble.
@@ -94,37 +92,11 @@
 
     /** Used in tests when no UI is required. */
     @VisibleForTesting(visibility = PRIVATE)
-    Bubble(Context context, NotificationEntry e) {
+    Bubble(NotificationEntry e) {
         mEntry = e;
         mKey = e.getKey();
         mLastUpdated = e.getSbn().getPostTime();
         mGroupId = groupId(e);
-
-        String shortcutId = e.getSbn().getNotification().getShortcutId();
-        if (BubbleExperimentConfig.useShortcutInfoToBubble(context)
-                && shortcutId != null) {
-            mShortcutInfo = BubbleExperimentConfig.getShortcutInfo(context,
-                    e.getSbn().getPackageName(),
-                    e.getSbn().getUser(), shortcutId);
-        }
-
-        PackageManager pm = context.getPackageManager();
-        ApplicationInfo info;
-        try {
-            info = pm.getApplicationInfo(
-                mEntry.getSbn().getPackageName(),
-                PackageManager.MATCH_UNINSTALLED_PACKAGES
-                    | PackageManager.MATCH_DISABLED_COMPONENTS
-                    | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
-                    | PackageManager.MATCH_DIRECT_BOOT_AWARE);
-            if (info != null) {
-                mAppName = String.valueOf(pm.getApplicationLabel(info));
-            }
-            Drawable appIcon = pm.getApplicationIcon(mEntry.getSbn().getPackageName());
-            mUserBadgedAppIcon = pm.getUserBadgedIcon(appIcon, mEntry.getSbn().getUser());
-        } catch (PackageManager.NameNotFoundException unused) {
-            mAppName = mEntry.getSbn().getPackageName();
-        }
     }
 
     public String getKey() {
@@ -143,41 +115,22 @@
         return mEntry.getSbn().getPackageName();
     }
 
+    @Nullable
     public String getAppName() {
         return mAppName;
     }
 
-    Drawable getUserBadgedAppIcon() {
-        return mUserBadgedAppIcon;
-    }
-
     @Nullable
     public ShortcutInfo getShortcutInfo() {
         return mShortcutInfo;
     }
 
-    /**
-     * Whether shortcut information should be used to populate the bubble.
-     * <p>
-     * To populate the activity use {@link LauncherApps#startShortcut(ShortcutInfo, Rect, Bundle)}.
-     * To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}.
-     */
-    public boolean usingShortcutInfo() {
-        return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent());
-    }
-
-    void setBubbleIconFactory(BubbleIconFactory factory) {
-        mBubbleIconFactory = factory;
-    }
-
-    boolean isInflated() {
-        return mInflated;
-    }
-
+    @Nullable
     BadgedImageView getIconView() {
         return mIconView;
     }
 
+    @Nullable
     BubbleExpandedView getExpandedView() {
         return mExpandedView;
     }
@@ -188,20 +141,62 @@
         }
     }
 
-    void inflate(LayoutInflater inflater, BubbleStackView stackView) {
-        if (mInflated) {
-            return;
+    /**
+     * Sets whether to perform inflation on the same thread as the caller. This method should only
+     * be used in tests, not in production.
+     */
+    @VisibleForTesting
+    void setInflateSynchronously(boolean inflateSynchronously) {
+        mInflateSynchronously = inflateSynchronously;
+    }
+
+    /**
+     * Starts a task to inflate & load any necessary information to display a bubble.
+     *
+     * @param callback the callback to notify one the bubble is ready to be displayed.
+     * @param context the context for the bubble.
+     * @param stackView the stackView the bubble is eventually added to.
+     * @param iconFactory the iconfactory use to create badged images for the bubble.
+     */
+    void inflate(BubbleViewInfoTask.Callback callback,
+            Context context,
+            BubbleStackView stackView,
+            BubbleIconFactory iconFactory) {
+        if (isBubbleLoading()) {
+            mInflationTask.cancel(true /* mayInterruptIfRunning */);
         }
-        mIconView = (BadgedImageView) inflater.inflate(
-                R.layout.bubble_view, stackView, false /* attachToRoot */);
-        mIconView.setBubbleIconFactory(mBubbleIconFactory);
-        mIconView.setBubble(this);
+        mInflationTask = new BubbleViewInfoTask(this,
+                context,
+                stackView,
+                iconFactory,
+                callback);
+        if (mInflateSynchronously) {
+            mInflationTask.onPostExecute(mInflationTask.doInBackground());
+        } else {
+            mInflationTask.execute();
+        }
+    }
 
-        mExpandedView = (BubbleExpandedView) inflater.inflate(
-                R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
-        mExpandedView.setBubble(this, stackView);
+    private boolean isBubbleLoading() {
+        return mInflationTask != null && mInflationTask.getStatus() != FINISHED;
+    }
 
-        mInflated = true;
+    boolean isInflated() {
+        return mInflated;
+    }
+
+    void setViewInfo(BubbleViewInfoTask.BubbleViewInfo info) {
+        if (!isInflated()) {
+            mIconView = info.imageView;
+            mExpandedView = info.expandedView;
+            mInflated = true;
+        }
+
+        mShortcutInfo = info.shortcutInfo;
+        mAppName = info.appName;
+
+        mExpandedView.update(this);
+        mIconView.update(this, info.badgedBubbleImage, info.dotColor, info.dotPath);
     }
 
     /**
@@ -218,13 +213,12 @@
         }
     }
 
-    void updateEntry(NotificationEntry entry) {
+    /**
+     * Sets the entry associated with this bubble.
+     */
+    void setEntry(NotificationEntry entry) {
         mEntry = entry;
         mLastUpdated = entry.getSbn().getPostTime();
-        if (mInflated) {
-            mIconView.update(this);
-            mExpandedView.update(this);
-        }
     }
 
     /**
@@ -242,13 +236,6 @@
     }
 
     /**
-     * @return the timestamp in milliseconds when this bubble was last displayed in expanded state
-     */
-    long getLastAccessTime() {
-        return mLastAccessed;
-    }
-
-    /**
      * @return the display id of the virtual display on which bubble contents is drawn.
      */
     int getDisplayId() {
@@ -352,6 +339,16 @@
         }
     }
 
+    /**
+     * Whether shortcut information should be used to populate the bubble.
+     * <p>
+     * To populate the activity use {@link LauncherApps#startShortcut(ShortcutInfo, Rect, Bundle)}.
+     * To populate the icon use {@link LauncherApps#getShortcutIconDrawable(ShortcutInfo, int)}.
+     */
+    boolean usingShortcutInfo() {
+        return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent());
+    }
+
     @Nullable
     PendingIntent getBubbleIntent() {
         Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 85a3959..ef8041b 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -184,6 +184,8 @@
     /** Last known orientation, used to detect orientation changes in {@link #onConfigChanged}. */
     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
 
+    private boolean mInflateSynchronously;
+
     /**
      * Listener to be notified when some states of the bubbles change.
      */
@@ -357,6 +359,15 @@
     }
 
     /**
+     * Sets whether to perform inflation on the same thread as the caller. This method should only
+     * be used in tests, not in production.
+     */
+    @VisibleForTesting
+    void setInflateSynchronously(boolean inflateSynchronously) {
+        mInflateSynchronously = inflateSynchronously;
+    }
+
+    /**
      * BubbleStackView is lazily created by this method the first time a Bubble is added. This
      * method initializes the stack view and adds it to the StatusBar just above the scrim.
      */
@@ -426,15 +437,14 @@
     }
 
     private void updateForThemeChanges() {
-        mBubbleIconFactory = new BubbleIconFactory(mContext);
-        for (Bubble b: mBubbleData.getBubbles()) {
-            b.getIconView().setBubbleIconFactory(mBubbleIconFactory);
-            b.getIconView().updateViews();
-            b.getExpandedView().applyThemeAttrs();
-        }
         if (mStackView != null) {
             mStackView.onThemeChanged();
         }
+        mBubbleIconFactory = new BubbleIconFactory(mContext);
+        for (Bubble b: mBubbleData.getBubbles()) {
+            // Reload each bubble
+            b.inflate(null /* callback */, mContext, mStackView, mBubbleIconFactory);
+        }
     }
 
     @Override
@@ -568,11 +578,19 @@
     }
 
     void updateBubble(NotificationEntry notif, boolean suppressFlyout, boolean showInShade) {
+        if (mStackView == null) {
+            // Lazy init stack view when a bubble is created
+            ensureStackViewCreated();
+        }
         // If this is an interruptive notif, mark that it's interrupted
         if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) {
             notif.setInterruption();
         }
-        mBubbleData.notificationEntryUpdated(notif, suppressFlyout, showInShade);
+        Bubble bubble = mBubbleData.getOrCreateBubble(notif);
+        bubble.setInflateSynchronously(mInflateSynchronously);
+        bubble.inflate(
+                b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade),
+                mContext, mStackView, mBubbleIconFactory);
     }
 
     /**
@@ -789,16 +807,6 @@
 
         @Override
         public void applyUpdate(BubbleData.Update update) {
-            if (mStackView == null && update.addedBubble != null) {
-                // Lazy init stack view when the first bubble is added.
-                ensureStackViewCreated();
-            }
-
-            // If not yet initialized, ignore all other changes.
-            if (mStackView == null) {
-                return;
-            }
-
             if (update.addedBubble != null) {
                 mStackView.addBubble(update.addedBubble);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index b7df5ba..97224f1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -33,6 +33,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
 import com.android.systemui.bubbles.BubbleController.DismissReason;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
@@ -48,7 +49,6 @@
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
-import com.android.systemui.R;
 
 /**
  * Keeps track of active bubbles.
@@ -180,28 +180,44 @@
         dispatchPendingChanges();
     }
 
-    void notificationEntryUpdated(NotificationEntry entry, boolean suppressFlyout,
-            boolean showInShade) {
+    /**
+     * Constructs a new bubble or returns an existing one. Does not add new bubbles to
+     * bubble data, must go through {@link #notificationEntryUpdated(Bubble, boolean, boolean)}
+     * for that.
+     */
+    Bubble getOrCreateBubble(NotificationEntry entry) {
+        Bubble bubble = getBubbleWithKey(entry.getKey());
+        if (bubble == null) {
+            bubble = new Bubble(entry);
+        } else {
+            bubble.setEntry(entry);
+        }
+        return bubble;
+    }
+
+    /**
+     * When this method is called it is expected that all info in the bubble has completed loading.
+     * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context,
+     * BubbleStackView, BubbleIconFactory).
+     */
+    void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) {
         if (DEBUG_BUBBLE_DATA) {
-            Log.d(TAG, "notificationEntryUpdated: " + entry);
+            Log.d(TAG, "notificationEntryUpdated: " + bubble);
         }
 
-        Bubble bubble = getBubbleWithKey(entry.getKey());
-        suppressFlyout |= !shouldShowFlyout(entry);
+        Bubble prevBubble = getBubbleWithKey(bubble.getKey());
+        suppressFlyout |= !shouldShowFlyout(bubble.getEntry());
 
-        if (bubble == null) {
+        if (prevBubble == null) {
             // Create a new bubble
-            bubble = new Bubble(mContext, entry);
             bubble.setSuppressFlyout(suppressFlyout);
             doAdd(bubble);
             trim();
         } else {
             // Updates an existing bubble
-            bubble.updateEntry(entry);
             bubble.setSuppressFlyout(suppressFlyout);
             doUpdate(bubble);
         }
-
         if (bubble.shouldAutoExpand()) {
             setSelectedBubbleInternal(bubble);
             if (!mExpanded) {
@@ -214,6 +230,7 @@
         bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade);
         bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
         dispatchPendingChanges();
+
     }
 
     public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
index 093fd0d..b32dbb7 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
@@ -65,9 +65,9 @@
      * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This
      * will include the workprofile indicator on the badge if appropriate.
      */
-    BitmapInfo getBadgeBitmap(Bubble b) {
+    BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon) {
         Bitmap userBadgedBitmap = createIconBitmap(
-                b.getUserBadgedAppIcon(), 1f, getBadgeSize());
+                userBadgedAppIcon, 1f, getBadgeSize());
 
         Canvas c = new Canvas();
         ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize());
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
new file mode 100644
index 0000000..41f5028
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2019 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.systemui.bubbles;
+
+import static com.android.systemui.bubbles.BadgedImageView.DEFAULT_PATH_SIZE;
+import static com.android.systemui.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA;
+import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
+import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.service.notification.StatusBarNotification;
+import android.util.Log;
+import android.util.PathParser;
+import android.view.LayoutInflater;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.graphics.ColorUtils;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.systemui.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Simple task to inflate views & load necessary info to display a bubble.
+ */
+public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> {
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES;
+
+
+    /**
+     * Callback to find out when the bubble has been inflated & necessary data loaded.
+     */
+    public interface Callback {
+        /**
+         * Called when data has been loaded for the bubble.
+         */
+        void onBubbleViewsReady(Bubble bubble);
+    }
+
+    private Bubble mBubble;
+    private WeakReference<Context> mContext;
+    private WeakReference<BubbleStackView> mStackView;
+    private BubbleIconFactory mIconFactory;
+    private Callback mCallback;
+
+    /**
+     * Creates a task to load information for the provided {@link Bubble}. Once all info
+     * is loaded, {@link Callback} is notified.
+     */
+    BubbleViewInfoTask(Bubble b,
+            Context context,
+            BubbleStackView stackView,
+            BubbleIconFactory factory,
+            Callback c) {
+        mBubble = b;
+        mContext = new WeakReference<>(context);
+        mStackView = new WeakReference<>(stackView);
+        mIconFactory = factory;
+        mCallback = c;
+    }
+
+    @Override
+    protected BubbleViewInfo doInBackground(Void... voids) {
+        return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble);
+    }
+
+    @Override
+    protected void onPostExecute(BubbleViewInfo viewInfo) {
+        if (viewInfo != null) {
+            mBubble.setViewInfo(viewInfo);
+            if (mCallback != null && !isCancelled()) {
+                mCallback.onBubbleViewsReady(mBubble);
+            }
+        }
+    }
+
+    static class BubbleViewInfo {
+        BadgedImageView imageView;
+        BubbleExpandedView expandedView;
+        ShortcutInfo shortcutInfo;
+        String appName;
+        Bitmap badgedBubbleImage;
+        int dotColor;
+        Path dotPath;
+
+        @Nullable
+        static BubbleViewInfo populate(Context c, BubbleStackView stackView,
+                BubbleIconFactory iconFactory, Bubble b) {
+            BubbleViewInfo info = new BubbleViewInfo();
+
+            // View inflation: only should do this once per bubble
+            if (!b.isInflated()) {
+                LayoutInflater inflater = LayoutInflater.from(c);
+                info.imageView = (BadgedImageView) inflater.inflate(
+                        R.layout.bubble_view, stackView, false /* attachToRoot */);
+
+                info.expandedView = (BubbleExpandedView) inflater.inflate(
+                        R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
+                info.expandedView.setStackView(stackView);
+            }
+
+            StatusBarNotification sbn = b.getEntry().getSbn();
+            String packageName = sbn.getPackageName();
+
+            // Shortcut info for this bubble
+            String shortcutId = sbn.getNotification().getShortcutId();
+            if (BubbleExperimentConfig.useShortcutInfoToBubble(c)
+                    && shortcutId != null) {
+                info.shortcutInfo = BubbleExperimentConfig.getShortcutInfo(c,
+                        packageName,
+                        sbn.getUser(), shortcutId);
+            }
+
+            // App name & app icon
+            PackageManager pm = c.getPackageManager();
+            ApplicationInfo appInfo;
+            Drawable badgedIcon;
+            try {
+                appInfo = pm.getApplicationInfo(
+                        packageName,
+                        PackageManager.MATCH_UNINSTALLED_PACKAGES
+                                | PackageManager.MATCH_DISABLED_COMPONENTS
+                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+                                | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+                if (appInfo != null) {
+                    info.appName = String.valueOf(pm.getApplicationLabel(appInfo));
+                }
+                Drawable appIcon = pm.getApplicationIcon(packageName);
+                badgedIcon = pm.getUserBadgedIcon(appIcon, sbn.getUser());
+            } catch (PackageManager.NameNotFoundException exception) {
+                // If we can't find package... don't think we should show the bubble.
+                Log.w(TAG, "Unable to find package: " + packageName);
+                return null;
+            }
+
+            // Badged bubble image
+            Drawable bubbleDrawable = iconFactory.getBubbleDrawable(b, c);
+            BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon);
+            info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable,
+                    badgeBitmapInfo).icon;
+
+            // Dot color & placement
+            Path iconPath = PathParser.createPathFromPathData(
+                    c.getResources().getString(com.android.internal.R.string.config_icon_mask));
+            Matrix matrix = new Matrix();
+            float scale = iconFactory.getNormalizer().getScale(bubbleDrawable,
+                    null /* outBounds */, null /* path */, null /* outMaskShape */);
+            float radius = DEFAULT_PATH_SIZE / 2f;
+            matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+                    radius /* pivot y */);
+            iconPath.transform(matrix);
+            info.dotPath = iconPath;
+            info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
+                    Color.WHITE, WHITE_SCRIM_ALPHA);
+            return info;
+        }
+    }
+}