From f82dcec612e97e05d59b4bfe99aa34224575abd8 Mon Sep 17 00:00:00 2001 From: Jeff DeCew Date: Tue, 23 Mar 2021 11:22:51 -0400 Subject: [PATCH] Clean up MessagingLayout notifications * Collapsed state now appears with the 'headerless' style used for all other collapsed notification templates (except conversations / calls which have their own style). * Expanded state contnues to use the 'header' treatment of all our big states, but messaging faces will now appear in the left icon gutter, and text will always align with other styles. * Image messages show up in the 'right icon' location when collapsed, but will not be moved to the 'left icon' when grouped. * 1-1 conversations will highlight the other user's name as the title, in the collapsed state. Fixes: 173204301 Test: post and inspect lots of messaging notifications Change-Id: I5a56d11ea98be1a22caca0d85040ab3fc375216f --- core/java/android/app/Notification.java | 83 ++++--- .../internal/widget/ConversationLayout.java | 39 +--- .../internal/widget/MessagingGroup.java | 39 +++- .../internal/widget/MessagingLayout.java | 152 ++++++------- .../android/internal/widget/PeopleHelper.java | 58 +++++ ...cation_template_material_big_messaging.xml | 62 ++++++ ...cation_template_material_heads_up_base.xml | 2 +- ...tification_template_material_messaging.xml | 202 +++++++++++++++--- core/res/res/values/dimens.xml | 4 +- core/res/res/values/symbols.xml | 4 +- .../row/ExpandableNotificationRow.java | 11 - ...ficationConversationTemplateViewWrapper.kt | 33 +-- ...ificationMessagingTemplateViewWrapper.java | 55 ++++- .../NotificationTemplateViewWrapper.java | 3 + 14 files changed, 511 insertions(+), 236 deletions(-) create mode 100644 core/res/res/layout/notification_template_material_big_messaging.xml diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index fa35025d7d18c..d76d343301a24 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5110,6 +5110,7 @@ public class Notification implements Parcelable TemplateBindResult result) { p.headerless(resId == getBaseLayoutResource() || resId == getHeadsUpBaseLayoutResource() + || resId == getMessagingLayoutResource() || resId == R.layout.notification_template_material_media); RemoteViews contentView = new BuilderRemoteViews(mContext.getApplicationInfo(), resId); @@ -6635,6 +6636,10 @@ public class Notification implements Parcelable return R.layout.notification_template_material_messaging; } + private int getBigMessagingLayoutResource() { + return R.layout.notification_template_material_big_messaging; + } + private int getConversationLayoutResource() { return R.layout.notification_template_material_conversation; } @@ -8145,12 +8150,14 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeContentView(boolean increasedHeight) { + // All messaging templates contain the actions ArrayList originalActions = mBuilder.mActions; - mBuilder.mActions = new ArrayList<>(); - RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */, - false /* hideLargeIcon */); - mBuilder.mActions = originalActions; - return remoteViews; + try { + mBuilder.mActions = new ArrayList<>(); + return makeMessagingView(StandardTemplateParams.VIEW_TYPE_NORMAL); + } finally { + mBuilder.mActions = originalActions; + } } /** @@ -8236,18 +8243,24 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeBigContentView() { - return makeMessagingView(false /* isCollapsed */, true /* hideLargeIcon */); + return makeMessagingView(StandardTemplateParams.VIEW_TYPE_BIG); } /** * Create a messaging layout. * - * @param isCollapsed Should this use the collapsed layout - * @param hideRightIcons Should the reply affordance be shown at the end of the notification + * @param viewType one of StandardTemplateParams.VIEW_TYPE_NORMAL, VIEW_TYPE_BIG, + * VIEW_TYPE_HEADS_UP * @return the created remoteView. */ @NonNull - private RemoteViews makeMessagingView(boolean isCollapsed, boolean hideRightIcons) { + private RemoteViews makeMessagingView(int viewType) { + boolean isCollapsed = viewType != StandardTemplateParams.VIEW_TYPE_BIG; + boolean hideRightIcons = viewType != StandardTemplateParams.VIEW_TYPE_NORMAL; + boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; + boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT; + boolean isHeaderless = !isConversationLayout && isCollapsed; + CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) ? super.mBigContentTitle : mConversationTitle; @@ -8265,23 +8278,26 @@ public class Notification implements Parcelable } else { isOneToOne = !isGroupConversation(); } - boolean isConversationLayout = mConversationType != CONVERSATION_TYPE_LEGACY; - boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT; + if (isHeaderless && isOneToOne && TextUtils.isEmpty(conversationTitle)) { + conversationTitle = getOtherPersonName(); + } + Icon largeIcon = mBuilder.mN.mLargeIcon; TemplateBindResult bindResult = new TemplateBindResult(); StandardTemplateParams p = mBuilder.mParams.reset() - .viewType(isCollapsed ? StandardTemplateParams.VIEW_TYPE_NORMAL - : StandardTemplateParams.VIEW_TYPE_BIG) + .viewType(viewType) .highlightExpander(isConversationLayout) .hideProgress(true) - .title(conversationTitle) + .title(isHeaderless ? conversationTitle : null) .text(null) .hideLargeIcon(hideRightIcons || isOneToOne) - .headerTextSecondary(conversationTitle); + .headerTextSecondary(isHeaderless ? null : conversationTitle); RemoteViews contentView = mBuilder.applyStandardTemplateWithActions( isConversationLayout ? mBuilder.getConversationLayoutResource() - : mBuilder.getMessagingLayoutResource(), + : isCollapsed + ? mBuilder.getMessagingLayoutResource() + : mBuilder.getBigMessagingLayoutResource(), p, bindResult); if (isConversationLayout) { @@ -8290,14 +8306,6 @@ public class Notification implements Parcelable } addExtras(mBuilder.mN.extras); - if (!isConversationLayout) { - // also update the end margin if there is an image - // NOTE: This template doesn't support moving this icon to the left, so we don't - // need to fully apply the MarginSet - contentView.setViewLayoutMargin(R.id.notification_messaging, RemoteViews.MARGIN_END, - bindResult.mHeadingExtraMarginSet.getDpValue(), - TypedValue.COMPLEX_UNIT_DIP); - } contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", mBuilder.getSmallIconColor(p)); contentView.setInt(R.id.status_bar_latest_event_content, "setSenderTextColor", @@ -8323,6 +8331,10 @@ public class Notification implements Parcelable contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsImportantConversation", isImportantConversation); } + if (isHeaderless) { + // Collapsed legacy messaging style has a 1-line limit. + contentView.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1); + } contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon", largeIcon); contentView.setBundle(R.id.status_bar_latest_event_content, "setData", @@ -8330,6 +8342,22 @@ public class Notification implements Parcelable return contentView; } + private CharSequence getKey(Person person) { + return person == null ? null + : person.getKey() == null ? person.getName() : person.getKey(); + } + + private CharSequence getOtherPersonName() { + CharSequence userKey = getKey(mUser); + for (int i = mMessages.size() - 1; i >= 0; i--) { + Person sender = mMessages.get(i).getSenderPerson(); + if (sender != null && !TextUtils.equals(userKey, getKey(sender))) { + return sender.getName(); + } + } + return null; + } + private boolean hasOnlyWhiteSpaceSenders() { for (int i = 0; i < mMessages.size(); i++) { Message m = mMessages.get(i); @@ -8364,12 +8392,7 @@ public class Notification implements Parcelable */ @Override public RemoteViews makeHeadsUpContentView(boolean increasedHeight) { - RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */, - true /* hideLargeIcon */); - if (mConversationType == CONVERSATION_TYPE_LEGACY) { - remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1); - } - return remoteViews; + return makeMessagingView(StandardTemplateParams.VIEW_TYPE_HEADS_UP); } public static final class Message { diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 8ecc80946141d..bab4e93bdd9ac 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -64,6 +64,7 @@ import com.android.internal.R; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -530,13 +531,7 @@ public class ConversationLayout extends FrameLayout mConversationText.setText(conversationText); // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) // This needs to happen after all of the above o update all of the groups - for (int i = mGroups.size() - 1; i >= 0; i--) { - MessagingGroup messagingGroup = mGroups.get(i); - CharSequence messageSender = messagingGroup.getSenderName(); - boolean canHide = mIsOneToOne - && TextUtils.equals(conversationText, messageSender); - messagingGroup.setCanHideSenderIfFirst(canHide); - } + mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); updateAppName(); updateIconPositionAndSize(); updateImageMessages(); @@ -779,35 +774,7 @@ public class ConversationLayout extends FrameLayout private void updateTitleAndNamesDisplay() { // Map of unique names to their prefix - ArrayMap uniqueNames = new ArrayMap<>(); - // Map of single-character string prefix to the only name which uses it, or null if multiple - ArrayMap uniqueCharacters = new ArrayMap<>(); - for (int i = 0; i < mGroups.size(); i++) { - MessagingGroup group = mGroups.get(i); - CharSequence senderName = group.getSenderName(); - if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { - continue; - } - if (!uniqueNames.containsKey(senderName)) { - String charPrefix = mPeopleHelper.findNamePrefix(senderName, null); - if (charPrefix == null) { - continue; - } - if (uniqueCharacters.containsKey(charPrefix)) { - // this character was already used, lets make it more unique. We first need to - // resolve the existing character if it exists - CharSequence existingName = uniqueCharacters.get(charPrefix); - if (existingName != null) { - uniqueNames.put(existingName, mPeopleHelper.findNameSplit(existingName)); - uniqueCharacters.put(charPrefix, null); - } - uniqueNames.put(senderName, mPeopleHelper.findNameSplit(senderName)); - } else { - uniqueNames.put(senderName, charPrefix); - uniqueCharacters.put(charPrefix, senderName); - } - } - } + Map uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups); // Now that we have the correct symbols, let's look what we have cached ArrayMap cachedAvatars = new ArrayMap<>(); diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java index f312d1d4f25db..f30b8442dc359 100644 --- a/core/java/com/android/internal/widget/MessagingGroup.java +++ b/core/java/com/android/internal/widget/MessagingGroup.java @@ -24,6 +24,7 @@ import android.annotation.StyleRes; import android.app.Person; import android.content.Context; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; @@ -109,7 +110,10 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou private boolean mIsInConversation = true; private ViewGroup mMessagingIconContainer; private int mConversationContentStart; - private int mNonConversationMarginEnd; + private int mNonConversationContentStart; + private int mNonConversationPaddingStart; + private int mConversationAvatarSize; + private int mNonConversationAvatarSize; private int mNotificationTextMarginTop; public MessagingGroup(@NonNull Context context) { @@ -141,16 +145,21 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou mMessagingIconContainer = findViewById(R.id.message_icon_container); mContentContainer = findViewById(R.id.messaging_group_content_container); mSendingSpinnerContainer = findViewById(R.id.messaging_group_sending_progress_container); - DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + Resources res = getResources(); + DisplayMetrics displayMetrics = res.getDisplayMetrics(); mDisplaySize.x = displayMetrics.widthPixels; mDisplaySize.y = displayMetrics.heightPixels; - mSenderTextPaddingSingleLine = getResources().getDimensionPixelSize( + mSenderTextPaddingSingleLine = res.getDimensionPixelSize( R.dimen.messaging_group_singleline_sender_padding_end); - mConversationContentStart = getResources().getDimensionPixelSize( - R.dimen.conversation_content_start); - mNonConversationMarginEnd = getResources().getDimensionPixelSize( - R.dimen.messaging_layout_margin_end); - mNotificationTextMarginTop = getResources().getDimensionPixelSize( + mConversationContentStart = res.getDimensionPixelSize(R.dimen.conversation_content_start); + mNonConversationContentStart = res.getDimensionPixelSize( + R.dimen.notification_content_margin_start); + mNonConversationPaddingStart = res.getDimensionPixelSize( + R.dimen.messaging_layout_icon_padding_start); + mConversationAvatarSize = res.getDimensionPixelSize(R.dimen.messaging_avatar_size); + mNonConversationAvatarSize = res.getDimensionPixelSize( + R.dimen.notification_icon_circle_size); + mNotificationTextMarginTop = res.getDimensionPixelSize( R.dimen.notification_text_margin_top); } @@ -696,10 +705,18 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou mIsInConversation = isInConversation; MarginLayoutParams layoutParams = (MarginLayoutParams) mMessagingIconContainer.getLayoutParams(); - layoutParams.width = mIsInConversation ? mConversationContentStart - : ViewPager.LayoutParams.WRAP_CONTENT; - layoutParams.setMarginEnd(mIsInConversation ? 0 : mNonConversationMarginEnd); + layoutParams.width = mIsInConversation + ? mConversationContentStart + : mNonConversationContentStart; mMessagingIconContainer.setLayoutParams(layoutParams); + int imagePaddingStart = isInConversation ? 0 : mNonConversationPaddingStart; + mMessagingIconContainer.setPaddingRelative(imagePaddingStart, 0, 0, 0); + + ViewGroup.LayoutParams avatarLayoutParams = mAvatarView.getLayoutParams(); + int size = mIsInConversation ? mConversationAvatarSize : mNonConversationAvatarSize; + avatarLayoutParams.height = size; + avatarLayoutParams.width = size; + mAvatarView.setLayoutParams(avatarLayoutParams); } } diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java index 27cd6e13d86c0..e1602a9819204 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -16,7 +16,7 @@ package com.android.internal.widget; -import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_AT_END; +import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL; import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE; import android.annotation.AttrRes; @@ -27,10 +27,6 @@ import android.app.Notification; import android.app.Person; import android.app.RemoteInputHistoryItem; import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Bundle; @@ -40,21 +36,22 @@ import android.util.ArrayMap; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.RemotableViewMethod; +import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.RemoteViews; -import android.widget.TextView; import com.android.internal.R; -import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.Consumer; -import java.util.regex.Pattern; /** * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal @@ -65,15 +62,6 @@ public class MessagingLayout extends FrameLayout implements ImageMessageConsumer, IMessagingLayout { private static final float COLOR_SHIFT_AMOUNT = 60; - /** - * Pattren for filter some ingonable characters. - * p{Z} for any kind of whitespace or invisible separator. - * p{C} for any kind of punctuation character. - */ - private static final Pattern IGNORABLE_CHAR_PATTERN - = Pattern.compile("[\\p{C}\\p{Z}]"); - private static final Pattern SPECIAL_CHAR_PATTERN - = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]"); private static final Consumer REMOVE_MESSAGE = MessagingMessage::removeMessage; public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); @@ -81,26 +69,26 @@ public class MessagingLayout extends FrameLayout public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR = new MessagingPropertyAnimator(); + private final PeopleHelper mPeopleHelper = new PeopleHelper(); private List mMessages = new ArrayList<>(); private List mHistoricMessages = new ArrayList<>(); private MessagingLinearLayout mMessagingLinearLayout; private boolean mShowHistoricMessages; private ArrayList mGroups = new ArrayList<>(); - private TextView mTitleView; + private MessagingLinearLayout mImageMessageContainer; + private ImageView mRightIconView; + private Rect mMessagingClipRect; private int mLayoutColor; private int mSenderTextColor; private int mMessageTextColor; - private int mAvatarSize; - private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private Paint mTextPaint = new Paint(); - private CharSequence mConversationTitle; private Icon mAvatarReplacement; private boolean mIsOneToOne; private ArrayList mAddedGroups = new ArrayList<>(); private Person mUser; private CharSequence mNameReplacement; - private boolean mDisplayImagesAtEnd; + private boolean mIsCollapsed; private ImageResolver mImageResolver; + private CharSequence mConversationTitle; public MessagingLayout(@NonNull Context context) { super(context); @@ -123,17 +111,16 @@ public class MessagingLayout extends FrameLayout @Override protected void onFinishInflate() { super.onFinishInflate(); + mPeopleHelper.init(getContext()); mMessagingLinearLayout = findViewById(R.id.notification_messaging); + mImageMessageContainer = findViewById(R.id.conversation_image_message_container); + mRightIconView = findViewById(R.id.right_icon); // We still want to clip, but only on the top, since views can temporarily out of bounds // during transitions. DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); - Rect rect = new Rect(0, 0, size, size); - mMessagingLinearLayout.setClipBounds(rect); - mTitleView = findViewById(R.id.title); - mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size); - mTextPaint.setTextAlign(Paint.Align.CENTER); - mTextPaint.setAntiAlias(true); + mMessagingClipRect = new Rect(0, 0, size, size); + setMessagingClippingDisabled(false); } @RemotableViewMethod @@ -153,7 +140,7 @@ public class MessagingLayout extends FrameLayout */ @RemotableViewMethod public void setIsCollapsed(boolean isCollapsed) { - mDisplayImagesAtEnd = isCollapsed; + mIsCollapsed = isCollapsed; } @RemotableViewMethod @@ -168,7 +155,7 @@ public class MessagingLayout extends FrameLayout */ @RemotableViewMethod public void setConversationTitle(CharSequence conversationTitle) { - // Unused + mConversationTitle = conversationTitle; } @RemotableViewMethod @@ -180,11 +167,6 @@ public class MessagingLayout extends FrameLayout List newHistoricMessages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON)); - mConversationTitle = null; - TextView headerText = findViewById(R.id.header_text); - if (headerText != null) { - mConversationTitle = headerText.getText(); - } RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); addRemoteInputHistoryToMessages(newMessages, history); @@ -238,6 +220,41 @@ public class MessagingLayout extends FrameLayout updateHistoricMessageVisibility(); updateTitleAndNamesDisplay(); + // after groups are finalized, hide the first sender name if it's showing as the title + mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, mConversationTitle); + updateImageMessages(); + } + + private void updateImageMessages() { + View newMessage = null; + if (mImageMessageContainer == null) { + return; + } + if (mIsCollapsed && !mGroups.isEmpty()) { + // When collapsed, we're displaying the image message in a dedicated container + // on the right of the layout instead of inline. Let's add the isolated image there + MessagingGroup messagingGroup = mGroups.get(mGroups.size() - 1); + MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); + if (isolatedMessage != null) { + newMessage = isolatedMessage.getView(); + } + } + // Remove all messages that don't belong into the image layout + View previousMessage = mImageMessageContainer.getChildAt(0); + if (previousMessage != newMessage) { + mImageMessageContainer.removeView(previousMessage); + if (newMessage != null) { + mImageMessageContainer.addView(newMessage); + } + } + mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE); + + // When showing an image message, do not show the large icon. Removing the drawable + // prevents it from being shown in the left_icon view (by the grouping util). + if (newMessage != null && mRightIconView != null && mRightIconView.getDrawable() != null) { + mRightIconView.setImageDrawable(null); + mRightIconView.setVisibility(GONE); + } } private void removeGroups(ArrayList oldGroups) { @@ -266,34 +283,7 @@ public class MessagingLayout extends FrameLayout } private void updateTitleAndNamesDisplay() { - ArrayMap uniqueNames = new ArrayMap<>(); - ArrayMap uniqueCharacters = new ArrayMap<>(); - for (int i = 0; i < mGroups.size(); i++) { - MessagingGroup group = mGroups.get(i); - CharSequence senderName = group.getSenderName(); - if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { - continue; - } - if (!uniqueNames.containsKey(senderName)) { - // Only use visible characters to get uniqueNames - String pureSenderName = IGNORABLE_CHAR_PATTERN - .matcher(senderName).replaceAll("" /* replacement */); - char c = pureSenderName.charAt(0); - if (uniqueCharacters.containsKey(c)) { - // this character was already used, lets make it more unique. We first need to - // resolve the existing character if it exists - CharSequence existingName = uniqueCharacters.get(c); - if (existingName != null) { - uniqueNames.put(existingName, findNameSplit((String) existingName)); - uniqueCharacters.put(c, null); - } - uniqueNames.put(senderName, findNameSplit((String) senderName)); - } else { - uniqueNames.put(senderName, Character.toString(c)); - uniqueCharacters.put(c, pureSenderName); - } - } - } + Map uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups); // Now that we have the correct symbols, let's look what we have cached ArrayMap cachedAvatars = new ArrayMap<>(); @@ -337,26 +327,7 @@ public class MessagingLayout extends FrameLayout } public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { - if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) || - SPECIAL_CHAR_PATTERN.matcher(symbol).find()) { - Icon avatarIcon = Icon.createWithResource(getContext(), - com.android.internal.R.drawable.messaging_user); - avatarIcon.setTint(findColor(senderName, layoutColor)); - return avatarIcon; - } else { - Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - float radius = mAvatarSize / 2.0f; - int color = findColor(senderName, layoutColor); - mPaint.setColor(color); - canvas.drawCircle(radius, radius, radius, mPaint); - boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f; - mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE); - mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f); - int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)); - canvas.drawText(symbol, radius, yPos, mTextPaint); - return Icon.createWithBitmap(bitmap); - } + return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor); } private int findColor(CharSequence senderName, int layoutColor) { @@ -449,8 +420,8 @@ public class MessagingLayout extends FrameLayout newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); mAddedGroups.add(newGroup); } - newGroup.setImageDisplayLocation(mDisplayImagesAtEnd - ? IMAGE_DISPLAY_LOCATION_AT_END + newGroup.setImageDisplayLocation(mIsCollapsed + ? IMAGE_DISPLAY_LOCATION_EXTERNAL : IMAGE_DISPLAY_LOCATION_INLINE); newGroup.setIsInConversation(false); newGroup.setLayoutColor(mLayoutColor); @@ -460,6 +431,8 @@ public class MessagingLayout extends FrameLayout if (sender != mUser && mNameReplacement != null) { nameOverride = mNameReplacement; } + newGroup.setSingleLine(mIsCollapsed); + newGroup.setShowingAvatar(!mIsCollapsed); newGroup.setSender(sender, nameOverride); newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); mGroups.add(newGroup); @@ -600,12 +573,17 @@ public class MessagingLayout extends FrameLayout return mMessagingLinearLayout; } + @Nullable + public ViewGroup getImageMessageContainer() { + return mImageMessageContainer; + } + public ArrayList getMessagingGroups() { return mGroups; } @Override public void setMessagingClippingDisabled(boolean clippingDisabled) { - // Don't do anything, this is only used for the ConversationLayout + mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect); } } diff --git a/core/java/com/android/internal/widget/PeopleHelper.java b/core/java/com/android/internal/widget/PeopleHelper.java index 77f4c8f6bedee..85cedc362b998 100644 --- a/core/java/com/android/internal/widget/PeopleHelper.java +++ b/core/java/com/android/internal/widget/PeopleHelper.java @@ -21,6 +21,7 @@ import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT; import android.annotation.ColorInt; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -28,12 +29,15 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Icon; import android.text.TextUtils; +import android.util.ArrayMap; import android.view.View; import com.android.internal.R; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; /** @@ -176,4 +180,58 @@ public class PeopleHelper { } return findNamePrefix(name, ""); } + + /** + * Creates a mapping of the unique sender names in the groups to the string 1- or 2-character + * prefix strings for the names, which are extracted as the initials, and should be used for + * generating the avatar. Senders not requiring a generated avatar, or with an empty name are + * omitted. + */ + public Map mapUniqueNamesToPrefix(List groups) { + // Map of unique names to their prefix + ArrayMap uniqueNames = new ArrayMap<>(); + // Map of single-character string prefix to the only name which uses it, or null if multiple + ArrayMap uniqueCharacters = new ArrayMap<>(); + for (int i = 0; i < groups.size(); i++) { + MessagingGroup group = groups.get(i); + CharSequence senderName = group.getSenderName(); + if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { + continue; + } + if (!uniqueNames.containsKey(senderName)) { + String charPrefix = findNamePrefix(senderName, null); + if (charPrefix == null) { + continue; + } + if (uniqueCharacters.containsKey(charPrefix)) { + // this character was already used, lets make it more unique. We first need to + // resolve the existing character if it exists + CharSequence existingName = uniqueCharacters.get(charPrefix); + if (existingName != null) { + uniqueNames.put(existingName, findNameSplit(existingName)); + uniqueCharacters.put(charPrefix, null); + } + uniqueNames.put(senderName, findNameSplit(senderName)); + } else { + uniqueNames.put(senderName, charPrefix); + uniqueCharacters.put(charPrefix, senderName); + } + } + } + return uniqueNames; + } + + /** + * Update whether the groups can hide the sender if they are first + * (happens only for 1:1 conversations where the given title matches the sender's name) + */ + public void maybeHideFirstSenderName(@NonNull List groups, + boolean isOneToOne, @Nullable CharSequence conversationTitle) { + for (int i = groups.size() - 1; i >= 0; i--) { + MessagingGroup messagingGroup = groups.get(i); + CharSequence messageSender = messagingGroup.getSenderName(); + boolean canHide = isOneToOne && TextUtils.equals(conversationTitle, messageSender); + messagingGroup.setCanHideSenderIfFirst(canHide); + } + } } diff --git a/core/res/res/layout/notification_template_material_big_messaging.xml b/core/res/res/layout/notification_template_material_big_messaging.xml new file mode 100644 index 0000000000000..01c37b7515e95 --- /dev/null +++ b/core/res/res/layout/notification_template_material_big_messaging.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/core/res/res/layout/notification_template_material_heads_up_base.xml b/core/res/res/layout/notification_template_material_heads_up_base.xml index d55499130dccd..a0d19b409cea0 100644 --- a/core/res/res/layout/notification_template_material_heads_up_base.xml +++ b/core/res/res/layout/notification_template_material_heads_up_base.xml @@ -38,7 +38,7 @@ diff --git a/core/res/res/layout/notification_template_material_messaging.xml b/core/res/res/layout/notification_template_material_messaging.xml index c3fd249d3ad57..3564f9755a5de 100644 --- a/core/res/res/layout/notification_template_material_messaging.xml +++ b/core/res/res/layout/notification_template_material_messaging.xml @@ -22,32 +22,186 @@ android:clipChildren="false" android:tag="messaging" > - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index fc2645c7c88b3..2bdf91f8a60d2 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -803,8 +803,8 @@ 38dp - - 12dp + + 4dp 4dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 8c8d99ec239ec..dc76c778a1606 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3008,6 +3008,7 @@ + @@ -3639,6 +3640,7 @@ + @@ -4055,7 +4057,7 @@ - + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 2ada28123f5f7..829c9aa148b75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -71,7 +71,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ContrastColorUtil; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.CallLayout; -import com.android.internal.widget.MessagingLayout; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; import com.android.systemui.R; @@ -658,10 +657,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView boolean beforeS = mEntry.targetSdk < Build.VERSION_CODES.S; int smallHeight; - View expandedView = layout.getExpandedChild(); - boolean isMediaLayout = expandedView != null - && expandedView.findViewById(com.android.internal.R.id.media_actions) != null; - boolean isMessagingLayout = contractedView instanceof MessagingLayout; boolean isCallLayout = contractedView instanceof CallLayout; if (customView && beforeS && !mIsSummaryWithChildren) { @@ -672,12 +667,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } else { smallHeight = mMaxSmallHeightBeforeS; } - } else if (isMessagingLayout) { - // TODO(b/173204301): MessagingStyle notifications currently look broken when we enforce - // the standard notification height, so we have to afford them more vertical space to - // make sure we don't crop them terribly. We actually need to revisit this and give - // them a headerless design, then remove this hack. - smallHeight = mMaxSmallHeightLarge; } else if (isCallLayout) { smallHeight = mMaxExpandedHeight; } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index 383bb7e41a913..08981f1215f28 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -23,12 +23,9 @@ import com.android.internal.widget.CachingIconView import com.android.internal.widget.ConversationLayout import com.android.internal.widget.MessagingLinearLayout import com.android.systemui.R -import com.android.systemui.statusbar.TransformableView -import com.android.systemui.statusbar.ViewTransformationHelper import com.android.systemui.statusbar.notification.NotificationUtils -import com.android.systemui.statusbar.notification.TransformState import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow -import com.android.systemui.statusbar.notification.row.HybridNotificationView +import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform /** * Wraps a notification containing a conversation template @@ -93,33 +90,7 @@ class NotificationConversationTemplateViewWrapper constructor( appName, conversationTitleView) - // Let's ignore the image message container since that is transforming as part of the - // messages already - mTransformationHelper.setCustomTransformation( - object : ViewTransformationHelper.CustomTransformation() { - override fun transformTo( - ownState: TransformState, - otherView: TransformableView, - transformationAmount: Float - ): Boolean { - if (otherView is HybridNotificationView) { - return false - } - // we're hidden by default by the transformState - ownState.ensureVisible() - // Let's do nothing otherwise, this is already handled by the messages - return true - } - - override fun transformFrom( - ownState: TransformState, - otherView: TransformableView, - transformationAmount: Float - ): Boolean = - transformTo(ownState, otherView, transformationAmount) - }, - imageMessageContainer.id - ) + setCustomImageMessageTransform(mTransformationHelper, imageMessageContainer) addViewsTransformingToSimilar( conversationIconView, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java index c9a274294d167..c587ce05b13ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapper.java @@ -18,12 +18,17 @@ package com.android.systemui.statusbar.notification.row.wrapper; import android.content.Context; import android.view.View; +import android.view.ViewGroup; import com.android.internal.widget.MessagingLayout; import com.android.internal.widget.MessagingLinearLayout; import com.android.systemui.R; +import com.android.systemui.statusbar.TransformableView; +import com.android.systemui.statusbar.ViewTransformationHelper; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.TransformState; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.row.HybridNotificationView; /** * Wraps a notification containing a messaging template @@ -31,12 +36,17 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow public class NotificationMessagingTemplateViewWrapper extends NotificationTemplateViewWrapper { private final int mMinHeightWithActions; + private final View mTitle; + private final View mTitleInHeader; private MessagingLayout mMessagingLayout; private MessagingLinearLayout mMessagingLinearLayout; + private ViewGroup mImageMessageContainer; protected NotificationMessagingTemplateViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { super(ctx, view, row); + mTitle = mView.findViewById(com.android.internal.R.id.title); + mTitleInHeader = mView.findViewById(com.android.internal.R.id.header_text_secondary); mMessagingLayout = (MessagingLayout) view; mMinHeightWithActions = NotificationUtils.getFontScaledHeight(ctx, R.dimen.notification_messaging_actions_min_height); @@ -44,6 +54,7 @@ public class NotificationMessagingTemplateViewWrapper extends NotificationTempla private void resolveViews() { mMessagingLinearLayout = mMessagingLayout.getMessagingLinearLayout(); + mImageMessageContainer = mMessagingLayout.getImageMessageContainer(); } @Override @@ -59,8 +70,48 @@ public class NotificationMessagingTemplateViewWrapper extends NotificationTempla // This also clears the existing types super.updateTransformedTypes(); if (mMessagingLinearLayout != null) { - mTransformationHelper.addTransformedView(mMessagingLinearLayout.getId(), - mMessagingLinearLayout); + mTransformationHelper.addTransformedView(mMessagingLinearLayout); + } + // The title is not as important for messaging, and stays in the header when expanded, + // but this ensures it animates cleanly between the two positions + if (mTitle == null && mTitleInHeader != null) { + mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, + mTitleInHeader); + } + setCustomImageMessageTransform(mTransformationHelper, mImageMessageContainer); + } + + static void setCustomImageMessageTransform( + ViewTransformationHelper transformationHelper, ViewGroup imageMessageContainer) { + if (imageMessageContainer != null) { + // Let's ignore the image message container since that is transforming as part of the + // messages already. This is also required to prevent a clipping artifact caused by the + // alpha layering triggering hardware rendering mode that in turn results in more + // aggressive clipping than we want. + transformationHelper.setCustomTransformation( + new ViewTransformationHelper.CustomTransformation() { + @Override + public boolean transformTo( + TransformState ownState, + TransformableView otherView, + float transformationAmount) { + if (otherView instanceof HybridNotificationView) { + return false; + } + // we're hidden by default by the transformState + ownState.ensureVisible(); + // Let's do nothing otherwise, this is already handled by the messages + return true; + } + + @Override + public boolean transformFrom( + TransformState ownState, + TransformableView otherView, + float transformationAmount) { + return transformTo(ownState, otherView, transformationAmount); + } + }, imageMessageContainer.getId()); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java index e0b58125aabd3..48f34b3b7716e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java @@ -56,6 +56,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp private ProgressBar mProgressBar; private TextView mTitle; private TextView mText; + protected View mSmartReplyContainer; protected View mActionsContainer; private int mContentHeight; @@ -160,6 +161,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp // It's still a viewstub mProgressBar = null; } + mSmartReplyContainer = mView.findViewById(com.android.internal.R.id.smart_reply_container); mActionsContainer = mView.findViewById(com.android.internal.R.id.actions_container); mActions = mView.findViewById(com.android.internal.R.id.actions); mRemoteInputHistory = mView.findViewById( @@ -275,6 +277,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp mProgressBar); } addViewsTransformingToSimilar(mLeftIcon); + addTransformedViews(mSmartReplyContainer); } @Override