Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3df7ab00 authored by Mady Mellor's avatar Mady Mellor
Browse files

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
parent 247ca2c4
Loading
Loading
Loading
Loading
+15 −74
Original line number Diff line number Diff line
@@ -17,18 +17,14 @@ package com.android.systemui.bubbles;

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 @@ import com.android.systemui.R;
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 @@ public class BadgedImageView extends ImageView {
    private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT;

    private Bubble mBubble;
    private BubbleIconFactory mBubbleIconFactory;

    private int mIconBitmapSize;
    private DotRenderer mDotRenderer;
@@ -91,6 +86,18 @@ public class BadgedImageView extends ImageView {
        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);
@@ -136,14 +143,6 @@ public class BadgedImageView extends ImageView {
        invalidate();
    }

    /**
     * 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.
     */
@@ -183,25 +182,6 @@ public class BadgedImageView extends ImageView {
        return new float[]{dotCenterX, dotCenterY};
    }

    /**
     * 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.
     */
@@ -210,15 +190,6 @@ public class BadgedImageView extends ImageView {
        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 @@ public class BadgedImageView extends ImageView {
                    }
                }).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();
    }
}
+80 −83
Original line number Diff line number Diff line
@@ -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.Notification;
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 @@ class Bubble {
    private NotificationEntry mEntry;
    private final String mKey;
    private final String mGroupId;

    private long mLastUpdated;
    private long mLastAccessed;

    // Items that are typically loaded later
    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;
    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 @@ class Bubble {

    /** 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 @@ class Bubble {
        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,22 +141,64 @@ class Bubble {
        }
    }

    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 */);
        }
        mInflationTask = new BubbleViewInfoTask(this,
                context,
                stackView,
                iconFactory,
                callback);
        if (mInflateSynchronously) {
            mInflationTask.onPostExecute(mInflationTask.doInBackground());
        } else {
            mInflationTask.execute();
        }
    }

    private boolean isBubbleLoading() {
        return mInflationTask != null && mInflationTask.getStatus() != FINISHED;
    }
        mIconView = (BadgedImageView) inflater.inflate(
                R.layout.bubble_view, stackView, false /* attachToRoot */);
        mIconView.setBubbleIconFactory(mBubbleIconFactory);
        mIconView.setBubble(this);

        mExpandedView = (BubbleExpandedView) inflater.inflate(
                R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
        mExpandedView.setBubble(this, stackView);
    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);
    }

    /**
     * Set visibility of bubble in the expanded state.
     *
@@ -218,13 +213,12 @@ class Bubble {
        }
    }

    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);
        }
    }

    /**
@@ -241,13 +235,6 @@ class Bubble {
        return mLastUpdated;
    }

    /**
     * @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.
     */
@@ -352,6 +339,16 @@ class Bubble {
        }
    }

    /**
     * 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();
+25 −17
Original line number Diff line number Diff line
@@ -184,6 +184,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
    /** 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.
     */
@@ -356,6 +358,15 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
        mBubbleIconFactory = new BubbleIconFactory(context);
    }

    /**
     * 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
    }

    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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi
    }

    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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi

        @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);
            }
+27 −10
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import android.util.Pair;
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 java.util.Objects;

import javax.inject.Inject;
import javax.inject.Singleton;
import com.android.systemui.R;

/**
 * Keeps track of active bubbles.
@@ -180,28 +180,44 @@ public class BubbleData {
        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 @@ public class BubbleData {
        bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade);
        bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
        dispatchPendingChanges();

    }

    public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) {
+2 −2
Original line number Diff line number Diff line
@@ -65,9 +65,9 @@ public class BubbleIconFactory extends BaseIconFactory {
     * 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());
Loading