From df898fd63d6ba48c230eafa263d7df59b17b229b Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Thu, 9 Jan 2020 09:26:36 -0800 Subject: [PATCH] Group chat flyout support * Moved the flyout message extraction code into BubbleViewInfo task thing since it's where we extract info etc * FlyoutMessage object contains anything we might display in the flyout including avatar, name, message, and isGroupChat * If we have an avatar & group chat is yes then we show the avatar I'll need to do a follow up CL where we get the "generated" avatars for group chats where the developer hasn't set an explicit avatar for the person. Bug: 138258523 Test: manual with test app Change-Id: Ie326b5354ce5f9268ef58c2f67cdecc3df0bf4e4 --- .../SystemUI/res/layout/bubble_flyout.xml | 43 +++++++-- packages/SystemUI/res/values/dimens.xml | 6 +- .../com/android/systemui/bubbles/Bubble.java | 88 ++++--------------- .../systemui/bubbles/BubbleFlyoutView.java | 64 +++++++++++--- .../systemui/bubbles/BubbleStackView.java | 9 +- .../systemui/bubbles/BubbleViewInfoTask.java | 84 ++++++++++++++++++ .../bubbles/BubbleFlyoutViewTest.java | 20 +++-- .../android/systemui/bubbles/BubbleTest.java | 20 +++-- 8 files changed, 225 insertions(+), 109 deletions(-) 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 53df02588f11d..4ad69671cd586 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1086,13 +1086,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 77c8e0b071586..9f72ce1523482 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 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 15c1c5524a74b..4522159ebe2ae 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -1376,9 +1376,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 @@ -1431,8 +1432,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 41f5028ed5c78..0019f479a87bd 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); } }