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

Commit db3cdd4e authored by Mady Mellor's avatar Mady Mellor Committed by Android (Google) Code Review
Browse files

Merge changes from topic "mm_bubbleInfoFlags" into udc-qpr-dev

* changes:
  Show / hide the "update" dot on bubbles in bubble bar
  Handle any image / label changes for bubble updates in bubble bar
parents 64f3766b c299ad64
Loading
Loading
Loading
Loading
+41 −16
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_S

import android.annotation.BinderThread;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
@@ -243,17 +244,21 @@ public class BubbleBarController extends IBubblesListener.Stub {
            BUBBLE_STATE_EXECUTOR.execute(() -> {
                createAndAddOverflowIfNeeded();
                if (update.addedBubble != null) {
                    viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView);
                    viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
                            null /* existingBubble */);
                }
                if (update.updatedBubble != null) {
                    BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
                    viewUpdate.updatedBubble =
                            populateBubble(update.updatedBubble, mContext, mBarView);
                            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(update.currentBubbleList.get(i), mContext, mBarView);
                                populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
                                        null /* existingBubble */);
                        currentBubbles.add(b);
                    }
                    viewUpdate.currentBubbles = currentBubbles;
@@ -315,9 +320,11 @@ public class BubbleBarController extends IBubblesListener.Stub {
        mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());

        if (update.updatedBubble != null) {
            // TODO: (b/269670235) handle updates:
            //  (1) if content / icons change -- requires reload & add back in place
            //  (2) if showing update dot changes -- tell the view to hide / show the dot
            // Updates mean the dot state may have changed; any other changes were updated in
            // the populateBubble step.
            BubbleBarBubble bb = mBubbles.get(update.updatedBubble.getKey());
            // If we're not stashed, we're visible so animate
            bb.getView().updateDotVisibility(!mBubbleStashController.isStashed() /* animate */);
        }
        if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
            // Create the new list
@@ -362,7 +369,13 @@ public class BubbleBarController extends IBubblesListener.Stub {
        if (getSelectedBubbleKey() != null) {
            int[] bubbleBarCoords = mBarView.getLocationOnScreen();
            if (mSelectedBubble instanceof BubbleBarBubble) {
                // TODO (b/269670235): hide the update dot on the view if needed.
                // Because we've visited this bubble, we should suppress the notification.
                // This is updated on WMShell side when we show the bubble, but that update isn't
                // passed to launcher, instead we apply it directly here.
                BubbleInfo info = ((BubbleBarBubble) mSelectedBubble).getInfo();
                info.setFlags(
                        info.getFlags() | Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION);
                mSelectedBubble.getView().updateDotVisibility(true /* animate */);
            }
            mSystemUiProxy.showBubble(getSelectedBubbleKey(),
                    bubbleBarCoords[0], bubbleBarCoords[1]);
@@ -407,7 +420,8 @@ public class BubbleBarController extends IBubblesListener.Stub {
    //

    @Nullable
    private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) {
    private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
            @Nullable BubbleBarBubble existingBubble) {
        String appName;
        Bitmap badgeBitmap;
        Bitmap bubbleBitmap;
@@ -476,8 +490,9 @@ public class BubbleBarController extends IBubblesListener.Stub {
        iconPath.transform(matrix);
        dotPath = iconPath;
        dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
                Color.WHITE, WHITE_SCRIM_ALPHA);
                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 */);
@@ -486,6 +501,16 @@ public class BubbleBarController extends IBubblesListener.Stub {
                    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) {
+9 −9
Original line number Diff line number Diff line
@@ -20,18 +20,18 @@ import android.graphics.Path
import com.android.wm.shell.common.bubbles.BubbleInfo

/** An entity in the bubble bar. */
sealed class BubbleBarItem(open val key: String, open val view: BubbleView)
sealed class BubbleBarItem(open var key: String, open var view: BubbleView)

/** Contains state info about a bubble in the bubble bar as well as presentation information. */
data class BubbleBarBubble(
    val info: BubbleInfo,
    override val view: BubbleView,
    val badge: Bitmap,
    val icon: Bitmap,
    val dotColor: Int,
    val dotPath: Path,
    val appName: String
    var info: BubbleInfo,
    override var view: BubbleView,
    var badge: Bitmap,
    var icon: Bitmap,
    var dotColor: Int,
    var dotPath: Path,
    var appName: String
) : BubbleBarItem(info.key, view)

/** Represents the overflow bubble in the bubble bar. */
data class BubbleBarOverflow(override val view: BubbleView) : BubbleBarItem("Overflow", view)
data class BubbleBarOverflow(override var view: BubbleView) : BubbleBarItem("Overflow", view)
+5 −6
Original line number Diff line number Diff line
@@ -234,6 +234,7 @@ public class BubbleBarView extends FrameLayout {
        final float collapsedWidth = collapsedWidth();
        int bubbleCount = getChildCount();
        final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
        final boolean animate = getVisibility() == VISIBLE;
        for (int i = 0; i < bubbleCount; i++) {
            BubbleView bv = (BubbleView) getChildAt(i);
            bv.setTranslationY(ty);
@@ -251,16 +252,14 @@ public class BubbleBarView extends FrameLayout {
                if (widthState == 1f) {
                    bv.setZ(0);
                }
                bv.showBadge();
                // When we're expanded, we're not stacked so we're not behind the stack
                bv.setBehindStack(false, animate);
            } else {
                final float targetX = currentWidth - collapsedWidth + collapsedX;
                bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
                bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
                if (i > 0) {
                    bv.hideBadge();
                } else {
                    bv.showBadge();
                }
                // If we're not the first bubble we're behind the stack
                bv.setBehindStack(i > 0, animate);
            }
        }

+145 −20
Original line number Diff line number Diff line
@@ -18,7 +18,9 @@ package com.android.launcher3.taskbar.bubbles;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,10 +30,13 @@ import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout;

import com.android.launcher3.R;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.animation.Interpolators;

import java.util.EnumSet;

// TODO: (b/276978250) This is will be similar to WMShell's BadgedImageView, it'd be nice to share.
// TODO: (b/269670235) currently this doesn't show the 'update dot'

/**
 * View that displays a bubble icon, along with an app badge on either the left or
@@ -39,14 +44,42 @@ import com.android.launcher3.icons.IconNormalizer;
 */
public class BubbleView extends ConstraintLayout {

    // TODO: (b/269670235) currently we don't render the 'update dot', this will be used for that.
    public static final int DEFAULT_PATH_SIZE = 100;

    /**
     * Flags that suppress the visibility of the 'new' dot or the app badge, for one reason or
     * another. If any of these flags are set, the dot will not be shown.
     * If {@link SuppressionFlag#BEHIND_STACK} then the app badge will not be shown.
     */
    enum SuppressionFlag {
        // TODO: (b/277815200) implement flyout
        // Suppressed because the flyout is visible - it will morph into the dot via animation.
        FLYOUT_VISIBLE,
        // Suppressed because this bubble is behind others in the collapsed stack.
        BEHIND_STACK,
    }

    private final EnumSet<SuppressionFlag> mSuppressionFlags =
            EnumSet.noneOf(SuppressionFlag.class);

    private final ImageView mBubbleIcon;
    private final ImageView mAppIcon;
    private final int mBubbleSize;

    private DotRenderer mDotRenderer;
    private DotRenderer.DrawParams mDrawParams;
    private int mDotColor;
    private Rect mTempBounds = new Rect();

    // Whether the dot is animating
    private boolean mDotIsAnimating;
    // What scale value the dot is animating to
    private float mAnimatingToDotScale;
    // The current scale value of the dot
    private float mDotScale;

    // TODO: (b/273310265) handle RTL
    // Whether the bubbles are positioned on the left or right side of the screen
    private boolean mOnLeft = false;

    private BubbleBarItem mBubble;
@@ -75,6 +108,8 @@ public class BubbleView extends ConstraintLayout {
        mBubbleIcon = findViewById(R.id.icon_view);
        mAppIcon = findViewById(R.id.app_icon_view);

        mDrawParams = new DotRenderer.DrawParams();

        setFocusable(true);
        setClickable(true);
        setOutlineProvider(new ViewOutlineProvider() {
@@ -91,17 +126,43 @@ public class BubbleView extends ConstraintLayout {
        outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize);
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        if (!shouldDrawDot()) {
            return;
        }

        getDrawingRect(mTempBounds);

        mDrawParams.dotColor = mDotColor;
        mDrawParams.iconBounds = mTempBounds;
        mDrawParams.leftAlign = mOnLeft;
        mDrawParams.scale = mDotScale;

        mDotRenderer.draw(canvas, mDrawParams);
    }

    /** Sets the bubble being rendered in this view. */
    void setBubble(BubbleBarBubble bubble) {
        mBubble = bubble;
        mBubbleIcon.setImageBitmap(bubble.getIcon());
        mAppIcon.setImageBitmap(bubble.getBadge());
        mDotColor = bubble.getDotColor();
        mDotRenderer = new DotRenderer(mBubbleSize, bubble.getDotPath(), DEFAULT_PATH_SIZE);
    }

    /**
     * Sets that this bubble represents the overflow. The overflow appears in the list of bubbles
     * but does not represent app content, instead it shows recent bubbles that couldn't fit into
     * the list of bubbles. It doesn't show an app icon because it is part of system UI / doesn't
     * come from an app.
     */
    void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
        mBubble = overflow;
        mBubbleIcon.setImageBitmap(bitmap);
        hideBadge();
        mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
    }

    /** Returns the bubble being rendered in this view. */
@@ -110,38 +171,102 @@ public class BubbleView extends ConstraintLayout {
        return mBubble;
    }

    /** Shows the app badge on this bubble. */
    void showBadge() {
    void updateDotVisibility(boolean animate) {
        final float targetScale = shouldDrawDot() ? 1f : 0f;
        if (animate) {
            animateDotScale();
        } else {
            mDotScale = targetScale;
            mAnimatingToDotScale = targetScale;
            invalidate();
        }
    }

    void updateBadgeVisibility() {
        if (mBubble instanceof BubbleBarOverflow) {
            // The overflow bubble does not have a badge, so just bail.
            return;
        }
        BubbleBarBubble bubble = (BubbleBarBubble) mBubble;

        Bitmap appBadgeBitmap = bubble.getBadge();
        if (appBadgeBitmap == null) {
            mAppIcon.setVisibility(GONE);
            return;
        int translationX = mOnLeft
                ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
                : 0;
        mAppIcon.setTranslationX(translationX);
        mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
    }

        int translationX;
        if (mOnLeft) {
            translationX = -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
    /** Sets whether this bubble is in the stack & not the first bubble. **/
    void setBehindStack(boolean behindStack, boolean animate) {
        if (behindStack) {
            mSuppressionFlags.add(SuppressionFlag.BEHIND_STACK);
        } else {
            translationX = 0;
            mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
        }
        updateDotVisibility(animate);
        updateBadgeVisibility();
    }

        mAppIcon.setTranslationX(translationX);
        mAppIcon.setVisibility(VISIBLE);
    /** Whether this bubble is in the stack & not the first bubble. **/
    boolean isBehindStack() {
        return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
    }

    /** Hides the app badge on this bubble. */
    void hideBadge() {
        mAppIcon.setVisibility(GONE);
    /** Whether the dot indicating unseen content in a bubble should be shown. */
    private boolean shouldDrawDot() {
        boolean bubbleHasUnseenContent = mBubble != null
                && mBubble instanceof BubbleBarBubble
                && mSuppressionFlags.isEmpty()
                && !((BubbleBarBubble) mBubble).getInfo().isNotificationSuppressed();

        // Always render the dot if it's animating, since it could be animating out. Otherwise, show
        // it if the bubble wants to show it, and we aren't suppressing it.
        return bubbleHasUnseenContent || mDotIsAnimating;
    }

    /** How big the dot should be, fraction from 0 to 1. */
    private void setDotScale(float fraction) {
        mDotScale = fraction;
        invalidate();
    }

    /**
     * Animates the dot to the given scale.
     */
    private void animateDotScale() {
        float toScale = shouldDrawDot() ? 1f : 0f;
        mDotIsAnimating = true;

        // Don't restart the animation if we're already animating to the given value.
        if (mAnimatingToDotScale == toScale || !shouldDrawDot()) {
            mDotIsAnimating = false;
            return;
        }

        mAnimatingToDotScale = toScale;

        final boolean showDot = toScale > 0f;

        // Do NOT wait until after animation ends to setShowDot
        // to avoid overriding more recent showDot states.
        clearAnimation();
        animate()
                .setDuration(200)
                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                .setUpdateListener((valueAnimator) -> {
                    float fraction = valueAnimator.getAnimatedFraction();
                    fraction = showDot ? fraction : 1f - fraction;
                    setDotScale(fraction);
                }).withEndAction(() -> {
                    setDotScale(showDot ? 1f : 0f);
                    mDotIsAnimating = false;
                }).start();
    }


    @Override
    public String toString() {
        return "BubbleView{" + mBubble + "}";
        String toString = mBubble != null ? mBubble.getKey() : "null";
        return "BubbleView{" + toString + "}";
    }
}