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

Commit df898fd6 authored by Mady Mellor's avatar Mady Mellor
Browse files

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
parent 871fe0a9
Loading
Loading
Loading
Loading
+34 −9
Original line number Diff line number Diff line
@@ -15,17 +15,40 @@
  -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <FrameLayout
    <LinearLayout
        android:id="@+id/bubble_flyout_text_container"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:orientation="horizontal"
        android:clipToPadding="false"
        android:paddingLeft="@dimen/bubble_flyout_padding_x"
        android:paddingRight="@dimen/bubble_flyout_padding_x"
        android:clipChildren="false"
        android:paddingStart="@dimen/bubble_flyout_padding_x"
        android:paddingEnd="@dimen/bubble_flyout_padding_x"
        android:paddingTop="@dimen/bubble_flyout_padding_y"
        android:paddingBottom="@dimen/bubble_flyout_padding_y"
        android:translationZ="@dimen/bubble_flyout_elevation">

        <ImageView
            android:id="@+id/bubble_flyout_avatar"
            android:layout_width="30dp"
            android:layout_height="30dp"
            android:layout_marginEnd="@dimen/bubble_flyout_avatar_message_space"
            android:scaleType="centerInside"
            android:src="@drawable/ic_create_bubble"/>

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/bubble_flyout_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:fontFamily="@*android:string/config_bodyFontFamilyMedium"
                android:maxLines="1"
                android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/>

            <TextView
                android:id="@+id/bubble_flyout_text"
                android:layout_width="wrap_content"
@@ -34,6 +57,8 @@
                android:maxLines="2"
                android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body2"/>

    </FrameLayout>
        </LinearLayout>

    </LinearLayout>

</merge>
 No newline at end of file
+4 −2
Original line number Diff line number Diff line
@@ -1086,13 +1086,15 @@
    <!-- How much the bubble flyout text container is elevated. -->
    <dimen name="bubble_flyout_elevation">4dp</dimen>
    <!-- How much padding is around the left and right sides of the flyout text. -->
    <dimen name="bubble_flyout_padding_x">16dp</dimen>
    <dimen name="bubble_flyout_padding_x">12dp</dimen>
    <!-- How much padding is around the top and bottom of the flyout text. -->
    <dimen name="bubble_flyout_padding_y">8dp</dimen>
    <dimen name="bubble_flyout_padding_y">10dp</dimen>
    <!-- Size of the triangle that points from the flyout to the bubble stack. -->
    <dimen name="bubble_flyout_pointer_size">6dp</dimen>
    <!-- How much space to leave between the flyout (tip of the arrow) and the bubble stack. -->
    <dimen name="bubble_flyout_space_from_bubble">8dp</dimen>
    <!-- How much space to leave between the flyout text and the avatar displayed in the flyout. -->
    <dimen name="bubble_flyout_avatar_message_space">6dp</dimen>
    <!-- Padding between status bar and bubbles when displayed in expanded state -->
    <dimen name="bubble_padding_top">16dp</dimen>
    <!-- Size of individual bubbles. -->
+18 −70
Original line number Diff line number Diff line
@@ -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<Notification.MessagingStyle.Message> 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;
+52 −12
Original line number Diff line number Diff line
@@ -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(
+5 −4
Original line number Diff line number Diff line
@@ -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 */,
Loading