diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index f050eaa4aa075..d15d1b72132aa 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5116,6 +5116,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); @@ -6641,6 +6642,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; } @@ -8151,12 +8156,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; + } } /** @@ -8242,18 +8249,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; @@ -8271,23 +8284,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) { @@ -8296,14 +8312,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", @@ -8329,6 +8337,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", @@ -8336,6 +8348,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); @@ -8370,12 +8398,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 7506cf8624bf5..90c0691d7ce46 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3015,6 +3015,7 @@ + @@ -3647,6 +3648,7 @@ + @@ -4063,7 +4065,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 8fae7200b56f8..500838fac5f98 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.R; import com.android.systemui.animation.ActivityLaunchAnimator; @@ -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