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
This commit is contained in:
Mady Mellor
2023-07-12 01:00:10 +00:00
committed by Android (Google) Code Review
4 changed files with 201 additions and 52 deletions
@@ -33,6 +33,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_S
import android.annotation.BinderThread; import android.annotation.BinderThread;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps; import android.content.pm.LauncherApps;
@@ -243,17 +244,21 @@ public class BubbleBarController extends IBubblesListener.Stub {
BUBBLE_STATE_EXECUTOR.execute(() -> { BUBBLE_STATE_EXECUTOR.execute(() -> {
createAndAddOverflowIfNeeded(); createAndAddOverflowIfNeeded();
if (update.addedBubble != null) { if (update.addedBubble != null) {
viewUpdate.addedBubble = populateBubble(update.addedBubble, mContext, mBarView); viewUpdate.addedBubble = populateBubble(mContext, update.addedBubble, mBarView,
null /* existingBubble */);
} }
if (update.updatedBubble != null) { if (update.updatedBubble != null) {
BubbleBarBubble existingBubble = mBubbles.get(update.updatedBubble.getKey());
viewUpdate.updatedBubble = viewUpdate.updatedBubble =
populateBubble(update.updatedBubble, mContext, mBarView); populateBubble(mContext, update.updatedBubble, mBarView,
existingBubble);
} }
if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) { if (update.currentBubbleList != null && !update.currentBubbleList.isEmpty()) {
List<BubbleBarBubble> currentBubbles = new ArrayList<>(); List<BubbleBarBubble> currentBubbles = new ArrayList<>();
for (int i = 0; i < update.currentBubbleList.size(); i++) { for (int i = 0; i < update.currentBubbleList.size(); i++) {
BubbleBarBubble b = BubbleBarBubble b =
populateBubble(update.currentBubbleList.get(i), mContext, mBarView); populateBubble(mContext, update.currentBubbleList.get(i), mBarView,
null /* existingBubble */);
currentBubbles.add(b); currentBubbles.add(b);
} }
viewUpdate.currentBubbles = currentBubbles; viewUpdate.currentBubbles = currentBubbles;
@@ -315,9 +320,11 @@ public class BubbleBarController extends IBubblesListener.Stub {
mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty()); mBubbleStashedHandleViewController.setHiddenForBubbles(mBubbles.isEmpty());
if (update.updatedBubble != null) { if (update.updatedBubble != null) {
// TODO: (b/269670235) handle updates: // Updates mean the dot state may have changed; any other changes were updated in
// (1) if content / icons change -- requires reload & add back in place // the populateBubble step.
// (2) if showing update dot changes -- tell the view to hide / show the dot 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()) { if (update.bubbleKeysInOrder != null && !update.bubbleKeysInOrder.isEmpty()) {
// Create the new list // Create the new list
@@ -362,7 +369,13 @@ public class BubbleBarController extends IBubblesListener.Stub {
if (getSelectedBubbleKey() != null) { if (getSelectedBubbleKey() != null) {
int[] bubbleBarCoords = mBarView.getLocationOnScreen(); int[] bubbleBarCoords = mBarView.getLocationOnScreen();
if (mSelectedBubble instanceof BubbleBarBubble) { 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(), mSystemUiProxy.showBubble(getSelectedBubbleKey(),
bubbleBarCoords[0], bubbleBarCoords[1]); bubbleBarCoords[0], bubbleBarCoords[1]);
@@ -407,7 +420,8 @@ public class BubbleBarController extends IBubblesListener.Stub {
// //
@Nullable @Nullable
private BubbleBarBubble populateBubble(BubbleInfo b, Context context, BubbleBarView bbv) { private BubbleBarBubble populateBubble(Context context, BubbleInfo b, BubbleBarView bbv,
@Nullable BubbleBarBubble existingBubble) {
String appName; String appName;
Bitmap badgeBitmap; Bitmap badgeBitmap;
Bitmap bubbleBitmap; Bitmap bubbleBitmap;
@@ -476,16 +490,27 @@ public class BubbleBarController extends IBubblesListener.Stub {
iconPath.transform(matrix); iconPath.transform(matrix);
dotPath = iconPath; dotPath = iconPath;
dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color,
Color.WHITE, WHITE_SCRIM_ALPHA); Color.WHITE, WHITE_SCRIM_ALPHA / 255f);
LayoutInflater inflater = LayoutInflater.from(context); if (existingBubble == null) {
BubbleView bubbleView = (BubbleView) inflater.inflate( LayoutInflater inflater = LayoutInflater.from(context);
R.layout.bubblebar_item_view, bbv, false /* attachToRoot */); BubbleView bubbleView = (BubbleView) inflater.inflate(
R.layout.bubblebar_item_view, bbv, false /* attachToRoot */);
BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView, BubbleBarBubble bubble = new BubbleBarBubble(b, bubbleView,
badgeBitmap, bubbleBitmap, dotColor, dotPath, appName); badgeBitmap, bubbleBitmap, dotColor, dotPath, appName);
bubbleView.setBubble(bubble); bubbleView.setBubble(bubble);
return 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) { private BubbleBarOverflow createOverflow(Context context) {
@@ -20,18 +20,18 @@ import android.graphics.Path
import com.android.wm.shell.common.bubbles.BubbleInfo import com.android.wm.shell.common.bubbles.BubbleInfo
/** An entity in the bubble bar. */ /** 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. */ /** Contains state info about a bubble in the bubble bar as well as presentation information. */
data class BubbleBarBubble( data class BubbleBarBubble(
val info: BubbleInfo, var info: BubbleInfo,
override val view: BubbleView, override var view: BubbleView,
val badge: Bitmap, var badge: Bitmap,
val icon: Bitmap, var icon: Bitmap,
val dotColor: Int, var dotColor: Int,
val dotPath: Path, var dotPath: Path,
val appName: String var appName: String
) : BubbleBarItem(info.key, view) ) : BubbleBarItem(info.key, view)
/** Represents the overflow bubble in the bubble bar. */ /** 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)
@@ -234,6 +234,7 @@ public class BubbleBarView extends FrameLayout {
final float collapsedWidth = collapsedWidth(); final float collapsedWidth = collapsedWidth();
int bubbleCount = getChildCount(); int bubbleCount = getChildCount();
final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f; final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
final boolean animate = getVisibility() == VISIBLE;
for (int i = 0; i < bubbleCount; i++) { for (int i = 0; i < bubbleCount; i++) {
BubbleView bv = (BubbleView) getChildAt(i); BubbleView bv = (BubbleView) getChildAt(i);
bv.setTranslationY(ty); bv.setTranslationY(ty);
@@ -251,16 +252,14 @@ public class BubbleBarView extends FrameLayout {
if (widthState == 1f) { if (widthState == 1f) {
bv.setZ(0); 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 { } else {
final float targetX = currentWidth - collapsedWidth + collapsedX; final float targetX = currentWidth - collapsedWidth + collapsedX;
bv.setTranslationX(widthState * (expandedX - targetX) + targetX); bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
bv.setZ((MAX_BUBBLES * mBubbleElevation) - i); bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
if (i > 0) { // If we're not the first bubble we're behind the stack
bv.hideBadge(); bv.setBehindStack(i > 0, animate);
} else {
bv.showBadge();
}
} }
} }
@@ -18,7 +18,9 @@ package com.android.launcher3.taskbar.bubbles;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Outline; import android.graphics.Outline;
import android.graphics.Rect;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -28,10 +30,13 @@ import android.widget.ImageView;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.launcher3.R; import com.android.launcher3.R;
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.IconNormalizer; 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/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 * 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 { 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; 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 mBubbleIcon;
private final ImageView mAppIcon; private final ImageView mAppIcon;
private final int mBubbleSize; 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 // TODO: (b/273310265) handle RTL
// Whether the bubbles are positioned on the left or right side of the screen
private boolean mOnLeft = false; private boolean mOnLeft = false;
private BubbleBarItem mBubble; private BubbleBarItem mBubble;
@@ -75,6 +108,8 @@ public class BubbleView extends ConstraintLayout {
mBubbleIcon = findViewById(R.id.icon_view); mBubbleIcon = findViewById(R.id.icon_view);
mAppIcon = findViewById(R.id.app_icon_view); mAppIcon = findViewById(R.id.app_icon_view);
mDrawParams = new DotRenderer.DrawParams();
setFocusable(true); setFocusable(true);
setClickable(true); setClickable(true);
setOutlineProvider(new ViewOutlineProvider() { setOutlineProvider(new ViewOutlineProvider() {
@@ -91,17 +126,43 @@ public class BubbleView extends ConstraintLayout {
outline.setOval(inset, inset, inset + normalizedSize, inset + normalizedSize); 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. */ /** Sets the bubble being rendered in this view. */
void setBubble(BubbleBarBubble bubble) { void setBubble(BubbleBarBubble bubble) {
mBubble = bubble; mBubble = bubble;
mBubbleIcon.setImageBitmap(bubble.getIcon()); mBubbleIcon.setImageBitmap(bubble.getIcon());
mAppIcon.setImageBitmap(bubble.getBadge()); 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) { void setOverflow(BubbleBarOverflow overflow, Bitmap bitmap) {
mBubble = overflow; mBubble = overflow;
mBubbleIcon.setImageBitmap(bitmap); mBubbleIcon.setImageBitmap(bitmap);
hideBadge(); mAppIcon.setVisibility(GONE); // Overflow doesn't show the app badge
} }
/** Returns the bubble being rendered in this view. */ /** Returns the bubble being rendered in this view. */
@@ -110,38 +171,102 @@ public class BubbleView extends ConstraintLayout {
return mBubble; return mBubble;
} }
/** Shows the app badge on this bubble. */ void updateDotVisibility(boolean animate) {
void showBadge() { final float targetScale = shouldDrawDot() ? 1f : 0f;
if (animate) {
animateDotScale();
} else {
mDotScale = targetScale;
mAnimatingToDotScale = targetScale;
invalidate();
}
}
void updateBadgeVisibility() {
if (mBubble instanceof BubbleBarOverflow) { if (mBubble instanceof BubbleBarOverflow) {
// The overflow bubble does not have a badge, so just bail. // The overflow bubble does not have a badge, so just bail.
return; return;
} }
BubbleBarBubble bubble = (BubbleBarBubble) mBubble; BubbleBarBubble bubble = (BubbleBarBubble) mBubble;
Bitmap appBadgeBitmap = bubble.getBadge(); Bitmap appBadgeBitmap = bubble.getBadge();
if (appBadgeBitmap == null) { int translationX = mOnLeft
mAppIcon.setVisibility(GONE); ? -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth())
: 0;
mAppIcon.setTranslationX(translationX);
mAppIcon.setVisibility(isBehindStack() ? GONE : VISIBLE);
}
/** 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 {
mSuppressionFlags.remove(SuppressionFlag.BEHIND_STACK);
}
updateDotVisibility(animate);
updateBadgeVisibility();
}
/** Whether this bubble is in the stack & not the first bubble. **/
boolean isBehindStack() {
return mSuppressionFlags.contains(SuppressionFlag.BEHIND_STACK);
}
/** 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; return;
} }
int translationX; mAnimatingToDotScale = toScale;
if (mOnLeft) {
translationX = -(bubble.getIcon().getWidth() - appBadgeBitmap.getWidth());
} else {
translationX = 0;
}
mAppIcon.setTranslationX(translationX); final boolean showDot = toScale > 0f;
mAppIcon.setVisibility(VISIBLE);
// 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();
} }
/** Hides the app badge on this bubble. */
void hideBadge() {
mAppIcon.setVisibility(GONE);
}
@Override @Override
public String toString() { public String toString() {
return "BubbleView{" + mBubble + "}"; String toString = mBubble != null ? mBubble.getKey() : "null";
return "BubbleView{" + toString + "}";
} }
} }