diff --git a/packages/SystemUI/res/layout/bubble_flyout.xml b/packages/SystemUI/res/layout/bubble_flyout.xml
index 5f773f462debe..7ab0a0c09f9b0 100644
--- a/packages/SystemUI/res/layout/bubble_flyout.xml
+++ b/packages/SystemUI/res/layout/bubble_flyout.xml
@@ -15,25 +15,50 @@
-->
-
-
+
+
+ android:orientation="vertical">
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 26af4ec31e471..65ca9f2132cd2 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1097,13 +1097,15 @@
4dp
- 16dp
+ 12dp
- 8dp
+ 10dp
6dp
8dp
+
+ 6dp
16dp
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index b89305e863497..2d9775deb4b55 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -32,20 +32,17 @@ 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 com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.R;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.List;
import java.util.Objects;
/**
@@ -85,6 +82,18 @@ class Bubble {
/** Whether flyout text should be suppressed, regardless of any other flags or state. */
private boolean mSuppressFlyout;
+ /**
+ * Presentational info about the flyout.
+ */
+ public static class FlyoutMessage {
+ @Nullable public Drawable senderAvatar;
+ @Nullable public CharSequence senderName;
+ @Nullable public CharSequence message;
+ @Nullable public boolean isGroupChat;
+ }
+
+ private FlyoutMessage mFlyoutMessage;
+
public static String groupId(NotificationEntry entry) {
UserHandle user = entry.getSbn().getUser();
return user.getIdentifier() + "|" + entry.getSbn().getPackageName();
@@ -194,6 +203,7 @@ class Bubble {
mShortcutInfo = info.shortcutInfo;
mAppName = info.appName;
+ mFlyoutMessage = info.flyoutMessage;
mExpandedView.update(this);
mIconView.update(this, info.badgedBubbleImage, info.dotColor, info.dotPath);
@@ -307,6 +317,10 @@ class Bubble {
mSuppressFlyout = suppressFlyout;
}
+ FlyoutMessage getFlyoutMessage() {
+ return mFlyoutMessage;
+ }
+
/**
* Returns whether the notification for this bubble is a foreground service. It shows that this
* is an ongoing bubble.
@@ -368,72 +382,6 @@ class Bubble {
return intent;
}
- /**
- * Returns our best guess for the most relevant text summary of the latest update to this
- * notification, based on its type. Returns null if there should not be an update message.
- */
- CharSequence getUpdateMessage(Context context) {
- final Notification underlyingNotif = mEntry.getSbn().getNotification();
- final Class extends Notification.Style> style = underlyingNotif.getNotificationStyle();
-
- try {
- if (Notification.BigTextStyle.class.equals(style)) {
- // Return the big text, it is big so probably important. If it's not there use the
- // normal text.
- CharSequence bigText =
- underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
- return !TextUtils.isEmpty(bigText)
- ? bigText
- : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
- } else if (Notification.MessagingStyle.class.equals(style)) {
- final List messages =
- Notification.MessagingStyle.Message.getMessagesFromBundleArray(
- (Parcelable[]) underlyingNotif.extras.get(
- Notification.EXTRA_MESSAGES));
-
- final Notification.MessagingStyle.Message latestMessage =
- Notification.MessagingStyle.findLatestIncomingMessage(messages);
-
- if (latestMessage != null) {
- final CharSequence personName = latestMessage.getSenderPerson() != null
- ? latestMessage.getSenderPerson().getName()
- : null;
-
- // Prepend the sender name if available since group chats also use messaging
- // style.
- if (!TextUtils.isEmpty(personName)) {
- return context.getResources().getString(
- R.string.notification_summary_message_format,
- personName,
- latestMessage.getText());
- } else {
- return latestMessage.getText();
- }
- }
- } else if (Notification.InboxStyle.class.equals(style)) {
- CharSequence[] lines =
- underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
-
- // Return the last line since it should be the most recent.
- if (lines != null && lines.length > 0) {
- return lines[lines.length - 1];
- }
- } else if (Notification.MediaStyle.class.equals(style)) {
- // Return nothing, media updates aren't typically useful as a text update.
- return null;
- } else {
- // Default to text extra.
- return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
- }
- } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
- // No use crashing, we'll just return null and the caller will assume there's no update
- // message.
- e.printStackTrace();
- }
-
- return null;
- }
-
private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) {
PackageManager pm = context.getPackageManager();
Resources r;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
index 78e98eb72fa3f..4194352f93bdd 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
@@ -32,11 +32,13 @@ import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.ShapeDrawable;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
+import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -65,7 +67,9 @@ public class BubbleFlyoutView extends FrameLayout {
private final float mCornerRadius;
private final ViewGroup mFlyoutTextContainer;
- private final TextView mFlyoutText;
+ private final ImageView mSenderAvatar;
+ private final TextView mSenderText;
+ private final TextView mMessageText;
/** Values related to the 'new' dot which we use to figure out where to collapse the flyout. */
private final float mNewDotRadius;
@@ -142,7 +146,9 @@ public class BubbleFlyoutView extends FrameLayout {
LayoutInflater.from(context).inflate(R.layout.bubble_flyout, this, true);
mFlyoutTextContainer = findViewById(R.id.bubble_flyout_text_container);
- mFlyoutText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text);
+ mSenderText = findViewById(R.id.bubble_flyout_name);
+ mSenderAvatar = findViewById(R.id.bubble_flyout_avatar);
+ mMessageText = mFlyoutTextContainer.findViewById(R.id.bubble_flyout_text);
final Resources res = getResources();
mFlyoutPadding = res.getDimensionPixelSize(R.dimen.bubble_flyout_padding_x);
@@ -204,9 +210,34 @@ public class BubbleFlyoutView extends FrameLayout {
/** Configures the flyout, collapsed into to dot form. */
void setupFlyoutStartingAsDot(
- CharSequence updateMessage, PointF stackPos, float parentWidth,
- boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete,
- @Nullable Runnable onHide, float[] dotCenter, boolean hideDot) {
+ Bubble.FlyoutMessage flyoutMessage,
+ PointF stackPos,
+ float parentWidth,
+ boolean arrowPointingLeft,
+ int dotColor,
+ @Nullable Runnable onLayoutComplete,
+ @Nullable Runnable onHide,
+ float[] dotCenter,
+ boolean hideDot) {
+
+ if (flyoutMessage.senderAvatar != null && flyoutMessage.isGroupChat) {
+ mSenderAvatar.setVisibility(VISIBLE);
+ mSenderAvatar.setImageDrawable(flyoutMessage.senderAvatar);
+ } else {
+ mSenderAvatar.setVisibility(GONE);
+ mSenderAvatar.setTranslationX(0);
+ mMessageText.setTranslationX(0);
+ mSenderText.setTranslationX(0);
+ }
+
+ // Name visibility
+ if (!TextUtils.isEmpty(flyoutMessage.senderName)) {
+ mSenderText.setText(flyoutMessage.senderName);
+ mSenderText.setVisibility(VISIBLE);
+ } else {
+ mSenderText.setVisibility(GONE);
+ }
+
mArrowPointingLeft = arrowPointingLeft;
mDotColor = dotColor;
mOnHide = onHide;
@@ -217,15 +248,15 @@ public class BubbleFlyoutView extends FrameLayout {
// Set the flyout TextView's max width in terms of percent, and then subtract out the
// padding so that the entire flyout view will be the desired width (rather than the
// TextView being the desired width + extra padding).
- mFlyoutText.setMaxWidth(
+ mMessageText.setMaxWidth(
(int) (parentWidth * FLYOUT_MAX_WIDTH_PERCENT) - mFlyoutPadding * 2);
- mFlyoutText.setText(updateMessage);
+ mMessageText.setText(flyoutMessage.message);
// Wait for the TextView to lay out so we know its line count.
post(() -> {
float restingTranslationY;
// Multi line flyouts get top-aligned to the bubble.
- if (mFlyoutText.getLineCount() > 1) {
+ if (mMessageText.getLineCount() > 1) {
restingTranslationY = stackPos.y + mBubbleIconTopPadding;
} else {
// Single line flyouts are vertically centered with respect to the bubble.
@@ -289,11 +320,20 @@ public class BubbleFlyoutView extends FrameLayout {
mPercentStillFlyout = (1f - mPercentTransitionedToDot);
// Move and fade out the text.
- mFlyoutText.setTranslationX(
- (mArrowPointingLeft ? -getWidth() : getWidth()) * mPercentTransitionedToDot);
- mFlyoutText.setAlpha(clampPercentage(
+ final float translationX = mPercentTransitionedToDot
+ * (mArrowPointingLeft ? -getWidth() : getWidth());
+ final float alpha = clampPercentage(
(mPercentStillFlyout - (1f - BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS))
- / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS));
+ / BubbleStackView.FLYOUT_DRAG_PERCENT_DISMISS);
+
+ mMessageText.setTranslationX(translationX);
+ mMessageText.setAlpha(alpha);
+
+ mSenderText.setTranslationX(translationX);
+ mSenderText.setAlpha(alpha);
+
+ mSenderAvatar.setTranslationX(translationX);
+ mSenderAvatar.setAlpha(alpha);
// Reduce the elevation towards that of the topmost bubble.
setTranslationZ(
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 02314bfe72516..54a42a6ec2125 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -1377,9 +1377,10 @@ public class BubbleStackView extends FrameLayout {
*/
@VisibleForTesting
void animateInFlyoutForBubble(Bubble bubble) {
- final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
+ Bubble.FlyoutMessage flyoutMessage = bubble.getFlyoutMessage();
final BadgedImageView bubbleView = bubble.getIconView();
- if (updateMessage == null
+ if (flyoutMessage == null
+ || flyoutMessage.message == null
|| !bubble.showFlyout()
|| isExpanded()
|| mIsExpansionAnimating
@@ -1432,8 +1433,8 @@ public class BubbleStackView extends FrameLayout {
};
mFlyout.postDelayed(mAnimateInFlyout, 200);
};
- mFlyout.setupFlyoutStartingAsDot(
- updateMessage, mStackAnimationController.getStackPosition(), getWidth(),
+ mFlyout.setupFlyoutStartingAsDot(flyoutMessage,
+ mStackAnimationController.getStackPosition(), getWidth(),
mStackAnimationController.isStackOnLeftSide(),
bubble.getIconView().getDotColor() /* dotColor */,
expandFlyoutAfterDelay /* onLayoutComplete */,
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
index 8924cd6a8f3e2..e7055847f0346 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java
@@ -21,6 +21,9 @@ 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.annotation.NonNull;
+import android.app.Notification;
+import android.app.Person;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -31,7 +34,9 @@ import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
+import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
import android.util.Log;
import android.util.PathParser;
import android.view.LayoutInflater;
@@ -41,8 +46,10 @@ import androidx.annotation.Nullable;
import com.android.internal.graphics.ColorUtils;
import com.android.launcher3.icons.BitmapInfo;
import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.lang.ref.WeakReference;
+import java.util.List;
/**
* Simple task to inflate views & load necessary info to display a bubble.
@@ -98,6 +105,9 @@ public class BubbleViewInfoTask extends AsyncTask style = underlyingNotif.getNotificationStyle();
+
+ Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage();
+ bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean(
+ Notification.EXTRA_IS_GROUP_CONVERSATION);
+ try {
+ if (Notification.BigTextStyle.class.equals(style)) {
+ // Return the big text, it is big so probably important. If it's not there use the
+ // normal text.
+ CharSequence bigText =
+ underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
+ bubbleMessage.message = !TextUtils.isEmpty(bigText)
+ ? bigText
+ : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
+ return bubbleMessage;
+ } else if (Notification.MessagingStyle.class.equals(style)) {
+ final List messages =
+ Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+ (Parcelable[]) underlyingNotif.extras.get(
+ Notification.EXTRA_MESSAGES));
+
+ final Notification.MessagingStyle.Message latestMessage =
+ Notification.MessagingStyle.findLatestIncomingMessage(messages);
+ if (latestMessage != null) {
+ bubbleMessage.message = latestMessage.getText();
+ Person sender = latestMessage.getSenderPerson();
+ bubbleMessage.senderName = sender != null
+ ? sender.getName()
+ : null;
+ bubbleMessage.senderAvatar = sender != null
+ ? sender.getIcon().loadDrawable(context)
+ : null;
+ return bubbleMessage;
+ }
+ } else if (Notification.InboxStyle.class.equals(style)) {
+ CharSequence[] lines =
+ underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
+
+ // Return the last line since it should be the most recent.
+ if (lines != null && lines.length > 0) {
+ bubbleMessage.message = lines[lines.length - 1];
+ return bubbleMessage;
+ }
+ } else if (Notification.MediaStyle.class.equals(style)) {
+ // Return nothing, media updates aren't typically useful as a text update.
+ return bubbleMessage;
+ } else {
+ // Default to text extra.
+ bubbleMessage.message =
+ underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
+ return bubbleMessage;
+ }
+ } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
+ // No use crashing, we'll just return null and the caller will assume there's no update
+ // message.
+ e.printStackTrace();
+ }
+
+ return bubbleMessage;
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
index 376ecf7815ac5..fd6e2ee66107d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
@@ -45,14 +45,22 @@ import org.mockito.MockitoAnnotations;
public class BubbleFlyoutViewTest extends SysuiTestCase {
private BubbleFlyoutView mFlyout;
private TextView mFlyoutText;
+ private TextView mSenderName;
private float[] mDotCenter = new float[2];
+ private Bubble.FlyoutMessage mFlyoutMessage;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+
+ mFlyoutMessage = new Bubble.FlyoutMessage();
+ mFlyoutMessage.senderName = "Josh";
+ mFlyoutMessage.message = "Hello";
+
mFlyout = new BubbleFlyoutView(getContext());
mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
+ mSenderName = mFlyout.findViewById(R.id.bubble_flyout_name);
mDotCenter[0] = 30;
mDotCenter[1] = 30;
}
@@ -60,19 +68,21 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {
@Test
public void testShowFlyout_isVisible() {
mFlyout.setupFlyoutStartingAsDot(
- "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
+ mFlyoutMessage,
+ new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
false);
mFlyout.setVisibility(View.VISIBLE);
assertEquals("Hello", mFlyoutText.getText());
+ assertEquals("Josh", mSenderName.getText());
assertEquals(View.VISIBLE, mFlyout.getVisibility());
}
@Test
public void testFlyoutHide_runsCallback() {
Runnable after = Mockito.mock(Runnable.class);
- mFlyout.setupFlyoutStartingAsDot(
- "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
+ mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
+ new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
false);
mFlyout.hideFlyout();
@@ -81,8 +91,8 @@ public class BubbleFlyoutViewTest extends SysuiTestCase {
@Test
public void testSetCollapsePercent() {
- mFlyout.setupFlyoutStartingAsDot(
- "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
+ mFlyout.setupFlyoutStartingAsDot(mFlyoutMessage,
+ new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
false);
mFlyout.setVisibility(View.VISIBLE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
index 3c42fd1cb48d4..02f721ce58a72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleTest.java
@@ -45,7 +45,6 @@ public class BubbleTest extends SysuiTestCase {
private Notification mNotif;
private NotificationEntry mEntry;
- private Bubble mBubble;
private Bundle mExtras;
@Before
@@ -58,7 +57,6 @@ public class BubbleTest extends SysuiTestCase {
mEntry = new NotificationEntryBuilder()
.setNotification(mNotif)
.build();
- mBubble = new Bubble(mEntry);
}
@Test
@@ -66,7 +64,8 @@ public class BubbleTest extends SysuiTestCase {
final String msg = "Hello there!";
doReturn(Notification.Style.class).when(mNotif).getNotificationStyle();
mExtras.putCharSequence(Notification.EXTRA_TEXT, msg);
- assertEquals(msg, mBubble.getUpdateMessage(mContext));
+ assertEquals(msg, BubbleViewInfoTask.extractFlyoutMessage(mContext,
+ mEntry).message);
}
@Test
@@ -77,7 +76,8 @@ public class BubbleTest extends SysuiTestCase {
mExtras.putCharSequence(Notification.EXTRA_BIG_TEXT, msg);
// Should be big text, not the small text.
- assertEquals(msg, mBubble.getUpdateMessage(mContext));
+ assertEquals(msg, BubbleViewInfoTask.extractFlyoutMessage(mContext,
+ mEntry).message);
}
@Test
@@ -85,7 +85,8 @@ public class BubbleTest extends SysuiTestCase {
doReturn(Notification.MediaStyle.class).when(mNotif).getNotificationStyle();
// Media notifs don't get update messages.
- assertNull(mBubble.getUpdateMessage(mContext));
+ assertNull(BubbleViewInfoTask.extractFlyoutMessage(mContext,
+ mEntry).message);
}
@Test
@@ -100,7 +101,8 @@ public class BubbleTest extends SysuiTestCase {
"Really? I prefer them that way."});
// Should be the last one only.
- assertEquals("Really? I prefer them that way.", mBubble.getUpdateMessage(mContext));
+ assertEquals("Really? I prefer them that way.",
+ BubbleViewInfoTask.extractFlyoutMessage(mContext, mEntry).message);
}
@Test
@@ -115,6 +117,10 @@ public class BubbleTest extends SysuiTestCase {
"Oh, hello!", 0, "Mady").toBundle()});
// Should be the last one only.
- assertEquals("Mady: Oh, hello!", mBubble.getUpdateMessage(mContext));
+ assertEquals("Oh, hello!",
+ BubbleViewInfoTask.extractFlyoutMessage(mContext, mEntry).message);
+ assertEquals("Mady",
+ BubbleViewInfoTask.extractFlyoutMessage(mContext,
+ mEntry).senderName);
}
}