Baseline for the new ConversationLayout
Introduced a new Layout for Conversation Notifications.
There are still various open issues that will be
fixed in follow up Cls
Bug: 150905003
Test: Add conversations, obverve visuals
Change-Id: I8ab2e2988d3205a4491006df68ec14235109466f
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 32e7d84..4ef4cfa 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -21,6 +21,7 @@
import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
import static com.android.internal.util.ContrastColorUtil.satisfiesTextContrast;
+import static com.android.internal.widget.ConversationLayout.CONVERSATION_LAYOUT_ENABLED;
import android.annotation.ColorInt;
import android.annotation.DimenRes;
@@ -389,6 +390,7 @@
STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_text);
STANDARD_LAYOUTS.add(R.layout.notification_template_material_inbox);
STANDARD_LAYOUTS.add(R.layout.notification_template_material_messaging);
+ STANDARD_LAYOUTS.add(R.layout.notification_template_material_conversation);
STANDARD_LAYOUTS.add(R.layout.notification_template_material_media);
STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_media);
STANDARD_LAYOUTS.add(R.layout.notification_template_header);
@@ -5138,7 +5140,7 @@
int color = isColorized(p) ? getPrimaryTextColor(p) : getSecondaryTextColor(p);
contentView.setDrawableTint(R.id.expand_button, false, color,
PorterDuff.Mode.SRC_ATOP);
- contentView.setInt(R.id.notification_header, "setOriginalNotificationColor",
+ contentView.setInt(R.id.expand_button, "setOriginalNotificationColor",
color);
}
@@ -6116,7 +6118,9 @@
}
private int getMessagingLayoutResource() {
- return R.layout.notification_template_material_messaging;
+ return CONVERSATION_LAYOUT_ENABLED
+ ? R.layout.notification_template_material_conversation
+ : R.layout.notification_template_material_messaging;
}
private int getActionLayoutResource() {
@@ -7379,7 +7383,7 @@
public RemoteViews makeContentView(boolean increasedHeight) {
mBuilder.mOriginalActions = mBuilder.mActions;
mBuilder.mActions = new ArrayList<>();
- RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */,
+ RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */,
false /* hideLargeIcon */);
mBuilder.mActions = mBuilder.mOriginalActions;
mBuilder.mOriginalActions = null;
@@ -7469,19 +7473,18 @@
*/
@Override
public RemoteViews makeBigContentView() {
- return makeMessagingView(false /* displayImagesAtEnd */, true /* hideLargeIcon */);
+ return makeMessagingView(false /* isCollapsed */, true /* hideLargeIcon */);
}
/**
* Create a messaging layout.
*
- * @param displayImagesAtEnd should images be displayed at the end of the content instead
- * of inline.
+ * @param isCollapsed Should this use the collapsed layout
* @param hideRightIcons Should the reply affordance be shown at the end of the notification
* @return the created remoteView.
*/
@NonNull
- private RemoteViews makeMessagingView(boolean displayImagesAtEnd, boolean hideRightIcons) {
+ private RemoteViews makeMessagingView(boolean isCollapsed, boolean hideRightIcons) {
CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
? super.mBigContentTitle
: mConversationTitle;
@@ -7522,14 +7525,16 @@
mBuilder.getPrimaryTextColor(p));
contentView.setInt(R.id.status_bar_latest_event_content, "setMessageTextColor",
mBuilder.getSecondaryTextColor(p));
- contentView.setBoolean(R.id.status_bar_latest_event_content, "setDisplayImagesAtEnd",
- displayImagesAtEnd);
+ contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsCollapsed",
+ isCollapsed);
contentView.setIcon(R.id.status_bar_latest_event_content, "setAvatarReplacement",
avatarReplacement);
contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement",
nameReplacement);
contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne",
isOneToOne);
+ contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
+ mBuilder.mN.mLargeIcon);
contentView.setBundle(R.id.status_bar_latest_event_content, "setData",
mBuilder.mN.extras);
return contentView;
@@ -7590,9 +7595,11 @@
*/
@Override
public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
- RemoteViews remoteViews = makeMessagingView(true /* displayImagesAtEnd */,
+ RemoteViews remoteViews = makeMessagingView(true /* isCollapsed */,
true /* hideLargeIcon */);
- remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
+ if (!CONVERSATION_LAYOUT_ENABLED) {
+ remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
+ }
return remoteViews;
}
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index 8ec5df8..18e0132 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -35,6 +35,7 @@
import com.android.internal.R;
import com.android.internal.widget.CachingIconView;
+import com.android.internal.widget.NotificationExpandButton;
import java.util.ArrayList;
@@ -56,7 +57,7 @@
private OnClickListener mAppOpsListener;
private HeaderTouchListener mTouchListener = new HeaderTouchListener();
private LinearLayout mTransferChip;
- private ImageView mExpandButton;
+ private NotificationExpandButton mExpandButton;
private CachingIconView mIcon;
private View mProfileBadge;
private View mOverlayIcon;
@@ -65,7 +66,6 @@
private View mAppOps;
private View mAudiblyAlertedIcon;
private int mIconColor;
- private int mOriginalNotificationColor;
private boolean mExpanded;
private boolean mShowExpandButtonAtEnd;
private boolean mShowWorkBadgeAtEnd;
@@ -324,13 +324,8 @@
return mIconColor;
}
- @RemotableViewMethod
- public void setOriginalNotificationColor(int color) {
- mOriginalNotificationColor = color;
- }
-
public int getOriginalNotificationColor() {
- return mOriginalNotificationColor;
+ return mExpandButton.getOriginalNotificationColor();
}
@RemotableViewMethod
@@ -371,7 +366,7 @@
contentDescriptionId = R.string.expand_button_content_description_collapsed;
}
mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
- mExpandButton.setColorFilter(mOriginalNotificationColor);
+ mExpandButton.setColorFilter(getOriginalNotificationColor());
mExpandButton.setContentDescription(mContext.getText(contentDescriptionId));
}
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
new file mode 100644
index 0000000..128f544
--- /dev/null
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+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;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+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.function.Consumer;
+import java.util.regex.Pattern;
+
+/**
+ * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal
+ * messages and adapts the layout accordingly.
+ */
+@RemoteViews.RemoteView
+public class ConversationLayout extends FrameLayout
+ implements ImageMessageConsumer, IMessagingLayout {
+
+ public static final boolean CONVERSATION_LAYOUT_ENABLED = true;
+ 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<MessagingMessage> REMOVE_MESSAGE
+ = MessagingMessage::removeMessage;
+ public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+ public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ 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 List<MessagingMessage> mMessages = new ArrayList<>();
+ private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
+ private MessagingLinearLayout mMessagingLinearLayout;
+ private boolean mShowHistoricMessages;
+ private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
+ private TextView mTitleView;
+ 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 Icon mAvatarReplacement;
+ private boolean mIsOneToOne;
+ private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
+ private Person mUser;
+ private CharSequence mNameReplacement;
+ private boolean mIsCollapsed;
+ private ImageResolver mImageResolver;
+ private ImageView mConversationIcon;
+ private TextView mHeaderText;
+ private View mConversationIconBadge;
+ private Icon mLargeIcon;
+ private View mExpandButtonContainer;
+ private NotificationExpandButton mExpandButton;
+ private int mExpandButtonExpandedTopMargin;
+ private int mBadgedSideMargins;
+ private int mIconSizeBadged;
+ private int mIconSizeCentered;
+ private View mIcon;
+ private int mExpandedGroupTopMargin;
+
+ public ConversationLayout(@NonNull Context context) {
+ super(context);
+ }
+
+ public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMessagingLinearLayout = findViewById(R.id.notification_messaging);
+ mMessagingLinearLayout.setMessagingLayout(this);
+ // 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);
+ mConversationIcon = findViewById(R.id.conversation_icon);
+ mIcon = findViewById(R.id.icon);
+ mConversationIconBadge = findViewById(R.id.conversation_icon_badge);
+ mHeaderText = findViewById(R.id.header_text);
+ mExpandButtonContainer = findViewById(R.id.expand_button_container);
+ mExpandButton = findViewById(R.id.expand_button);
+ mExpandButtonExpandedTopMargin = getResources().getDimensionPixelSize(
+ R.dimen.conversation_expand_button_top_margin_expanded);
+ mBadgedSideMargins = getResources().getDimensionPixelSize(
+ R.dimen.conversation_badge_side_margin);
+ mIconSizeBadged = getResources().getDimensionPixelSize(
+ R.dimen.conversation_icon_size_badged);
+ mIconSizeCentered = getResources().getDimensionPixelSize(
+ R.dimen.conversation_icon_size_centered);
+ mExpandedGroupTopMargin = getResources().getDimensionPixelSize(
+ R.dimen.conversation_icon_margin_top_centered);
+ }
+
+ @RemotableViewMethod
+ public void setAvatarReplacement(Icon icon) {
+ mAvatarReplacement = icon;
+ }
+
+ @RemotableViewMethod
+ public void setNameReplacement(CharSequence nameReplacement) {
+ mNameReplacement = nameReplacement;
+ }
+
+ /**
+ * Set this layout to show the collapsed representation.
+ *
+ * @param isCollapsed is it collapsed
+ */
+ @RemotableViewMethod
+ public void setIsCollapsed(boolean isCollapsed) {
+ mIsCollapsed = isCollapsed;
+ mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE);
+ updateExpandButton();
+ }
+
+ @RemotableViewMethod
+ public void setData(Bundle extras) {
+ Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
+ List<Notification.MessagingStyle.Message> newMessages
+ = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages);
+ Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
+ List<Notification.MessagingStyle.Message> newHistoricMessages
+ = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+
+ // mUser now set (would be nice to avoid the side effect but WHATEVER)
+ setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
+
+
+ // Append remote input history to newMessages (again, side effect is lame but WHATEVS)
+ RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[])
+ extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ addRemoteInputHistoryToMessages(newMessages, history);
+
+ boolean showSpinner =
+ extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
+
+ // bind it, baby
+ bind(newMessages, newHistoricMessages, showSpinner);
+ }
+
+ @Override
+ public void setImageResolver(ImageResolver resolver) {
+ mImageResolver = resolver;
+ }
+
+ private void addRemoteInputHistoryToMessages(
+ List<Notification.MessagingStyle.Message> newMessages,
+ RemoteInputHistoryItem[] remoteInputHistory) {
+ if (remoteInputHistory == null || remoteInputHistory.length == 0) {
+ return;
+ }
+ for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
+ RemoteInputHistoryItem historyMessage = remoteInputHistory[i];
+ Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message(
+ historyMessage.getText(), 0, (Person) null, true /* remoteHistory */);
+ if (historyMessage.getUri() != null) {
+ message.setData(historyMessage.getMimeType(), historyMessage.getUri());
+ }
+ newMessages.add(message);
+ }
+ }
+
+ private void bind(List<Notification.MessagingStyle.Message> newMessages,
+ List<Notification.MessagingStyle.Message> newHistoricMessages,
+ boolean showSpinner) {
+ // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding
+ // if they exist
+ List<MessagingMessage> historicMessages = createMessages(newHistoricMessages,
+ true /* isHistoric */);
+ List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */);
+
+ // Copy our groups, before they get clobbered
+ ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups);
+
+ // Add our new MessagingMessages to groups
+ List<List<MessagingMessage>> groups = new ArrayList<>();
+ List<Person> senders = new ArrayList<>();
+
+ // Lets first find the groups (populate `groups` and `senders`)
+ findGroups(historicMessages, messages, groups, senders);
+
+ // Let's now create the views and reorder them accordingly
+ // side-effect: updates mGroups, mAddedGroups
+ createGroupViews(groups, senders, showSpinner);
+
+ // Let's first check which groups were removed altogether and remove them in one animation
+ removeGroups(oldGroups);
+
+ // Let's remove the remaining messages
+ mMessages.forEach(REMOVE_MESSAGE);
+ mHistoricMessages.forEach(REMOVE_MESSAGE);
+
+ mMessages = messages;
+ mHistoricMessages = historicMessages;
+
+ updateHistoricMessageVisibility();
+ updateTitleAndNamesDisplay();
+
+ updateConversationIconAndHeaderText();
+
+ }
+
+ private void updateConversationIconAndHeaderText() {
+ // TODO: resolve this from shortcuts
+ // Set avatar and name
+ if (mIsOneToOne) {
+ // Let's resolve the icon / text from the last sender
+ mConversationIcon.setVisibility(VISIBLE);
+ mHeaderText.setVisibility(VISIBLE);
+ boolean found = false;
+ for (int i = mGroups.size() - 1; i >= 0; i--) {
+ MessagingGroup messagingGroup = mGroups.get(i);
+ Person messageSender = messagingGroup.getSender();
+ if (!mUser.equals(messageSender)) {
+ // Make sure the header is actually visible
+ // TODO: figure out what to do if there's a converationtitle + a Sender
+ mHeaderText.setText(messagingGroup.getSenderName());
+ mConversationIcon.setImageIcon(messagingGroup.getAvatarIcon());
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ mHeaderText.setText(mUser.getName());
+ mConversationIcon.setImageIcon(mUser.getIcon());
+ }
+ } else {
+ mHeaderText.setVisibility(GONE);
+ if (mIsCollapsed) {
+ mConversationIcon.setVisibility(VISIBLE);
+ if (mLargeIcon != null) {
+ mConversationIcon.setImageIcon(mLargeIcon);
+ } else {
+ // TODO: generate LargeIcon from Conversation
+ }
+ } else {
+ mConversationIcon.setVisibility(GONE);
+ }
+ }
+ // update the icon position and sizing
+ int gravity;
+ int marginStart;
+ int marginTop;
+ int iconSize;
+ if (mIsOneToOne || mIsCollapsed) {
+ // Baded format
+ gravity = Gravity.LEFT;
+ marginStart = mBadgedSideMargins;
+ marginTop = mBadgedSideMargins;
+ iconSize = mIconSizeBadged;
+ } else {
+ gravity = Gravity.CENTER_HORIZONTAL;
+ marginStart = 0;
+ marginTop = mExpandedGroupTopMargin;
+ iconSize = mIconSizeCentered;
+ }
+ FrameLayout.LayoutParams layoutParams =
+ (LayoutParams) mConversationIconBadge.getLayoutParams();
+ layoutParams.gravity = gravity;
+ layoutParams.topMargin = marginTop;
+ layoutParams.setMarginStart(marginStart);
+ mConversationIconBadge.setLayoutParams(layoutParams);
+ ViewGroup.LayoutParams iconParams = mIcon.getLayoutParams();
+ iconParams.width = iconSize;
+ iconParams.height = iconSize;
+ mIcon.setLayoutParams(iconParams);
+ }
+
+ @RemotableViewMethod
+ public void setLargeIcon(Icon largeIcon) {
+ mLargeIcon = largeIcon;
+ }
+
+ private void removeGroups(ArrayList<MessagingGroup> oldGroups) {
+ int size = oldGroups.size();
+ for (int i = 0; i < size; i++) {
+ MessagingGroup group = oldGroups.get(i);
+ if (!mGroups.contains(group)) {
+ List<MessagingMessage> messages = group.getMessages();
+ Runnable endRunnable = () -> {
+ mMessagingLinearLayout.removeTransientView(group);
+ group.recycle();
+ };
+
+ boolean wasShown = group.isShown();
+ mMessagingLinearLayout.removeView(group);
+ if (wasShown && !MessagingLinearLayout.isGone(group)) {
+ mMessagingLinearLayout.addTransientView(group, 0);
+ group.removeGroupAnimated(endRunnable);
+ } else {
+ endRunnable.run();
+ }
+ mMessages.removeAll(messages);
+ mHistoricMessages.removeAll(messages);
+ }
+ }
+ }
+
+ private void updateTitleAndNamesDisplay() {
+ ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>();
+ ArrayMap<Character, CharSequence> 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);
+ }
+ }
+ }
+
+ // Now that we have the correct symbols, let's look what we have cached
+ ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>();
+ for (int i = 0; i < mGroups.size(); i++) {
+ // Let's now set the avatars
+ MessagingGroup group = mGroups.get(i);
+ boolean isOwnMessage = group.getSender() == mUser;
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
+ || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) {
+ continue;
+ }
+ String symbol = uniqueNames.get(senderName);
+ Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName,
+ symbol, mLayoutColor);
+ if (cachedIcon != null) {
+ cachedAvatars.put(senderName, cachedIcon);
+ }
+ }
+
+ for (int i = 0; i < mGroups.size(); i++) {
+ // Let's now set the avatars
+ MessagingGroup group = mGroups.get(i);
+ CharSequence senderName = group.getSenderName();
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) {
+ group.setAvatar(mAvatarReplacement);
+ } else {
+ Icon cachedIcon = cachedAvatars.get(senderName);
+ if (cachedIcon == null) {
+ cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName),
+ mLayoutColor);
+ cachedAvatars.put(senderName, cachedIcon);
+ }
+ group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName),
+ mLayoutColor);
+ }
+ }
+ }
+
+ private 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(),
+ 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);
+ }
+ }
+
+ private int findColor(CharSequence senderName, int layoutColor) {
+ double luminance = ContrastColorUtil.calculateLuminance(layoutColor);
+ float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f;
+
+ // we need to offset the range if the luminance is too close to the borders
+ shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0);
+ shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0);
+ return ContrastColorUtil.getShiftedColor(layoutColor,
+ (int) (shift * COLOR_SHIFT_AMOUNT));
+ }
+
+ private String findNameSplit(String existingName) {
+ String[] split = existingName.split(" ");
+ if (split.length > 1) {
+ return Character.toString(split[0].charAt(0))
+ + Character.toString(split[1].charAt(0));
+ }
+ return existingName.substring(0, 1);
+ }
+
+ @RemotableViewMethod
+ public void setLayoutColor(int color) {
+ mLayoutColor = color;
+ }
+
+ @RemotableViewMethod
+ public void setIsOneToOne(boolean oneToOne) {
+ mIsOneToOne = oneToOne;
+ }
+
+ @RemotableViewMethod
+ public void setSenderTextColor(int color) {
+ mSenderTextColor = color;
+ }
+
+ @RemotableViewMethod
+ public void setMessageTextColor(int color) {
+ mMessageTextColor = color;
+ }
+
+ private void setUser(Person user) {
+ mUser = user;
+ if (mUser.getIcon() == null) {
+ Icon userIcon = Icon.createWithResource(getContext(),
+ R.drawable.messaging_user);
+ userIcon.setTint(mLayoutColor);
+ mUser = mUser.toBuilder().setIcon(userIcon).build();
+ }
+ }
+
+ private void createGroupViews(List<List<MessagingMessage>> groups,
+ List<Person> senders, boolean showSpinner) {
+ mGroups.clear();
+ for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
+ List<MessagingMessage> group = groups.get(groupIndex);
+ MessagingGroup newGroup = null;
+ // we'll just take the first group that exists or create one there is none
+ for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) {
+ MessagingMessage message = group.get(messageIndex);
+ newGroup = message.getGroup();
+ if (newGroup != null) {
+ break;
+ }
+ }
+ // Create a new group, adding it to the linear layout as well
+ if (newGroup == null) {
+ newGroup = MessagingGroup.createGroup(mMessagingLinearLayout);
+ mAddedGroups.add(newGroup);
+ }
+ newGroup.setDisplayImagesAtEnd(mIsCollapsed);
+ newGroup.setLayoutColor(mLayoutColor);
+ newGroup.setTextColors(mSenderTextColor, mMessageTextColor);
+ Person sender = senders.get(groupIndex);
+ CharSequence nameOverride = null;
+ if (sender != mUser && mNameReplacement != null) {
+ nameOverride = mNameReplacement;
+ }
+ newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed);
+ newGroup.setSingleLine(mIsCollapsed);
+ newGroup.setSender(sender, nameOverride);
+ newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner);
+ mGroups.add(newGroup);
+
+ // Reposition to the correct place (if we're re-using a group)
+ if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
+ mMessagingLinearLayout.removeView(newGroup);
+ mMessagingLinearLayout.addView(newGroup, groupIndex);
+ }
+ newGroup.setMessages(group);
+ }
+ }
+
+ private void findGroups(List<MessagingMessage> historicMessages,
+ List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
+ CharSequence currentSenderKey = null;
+ List<MessagingMessage> currentGroup = null;
+ int histSize = historicMessages.size();
+ for (int i = 0; i < histSize + messages.size(); i++) {
+ MessagingMessage message;
+ if (i < histSize) {
+ message = historicMessages.get(i);
+ } else {
+ message = messages.get(i - histSize);
+ }
+ boolean isNewGroup = currentGroup == null;
+ Person sender = message.getMessage().getSenderPerson();
+ CharSequence key = sender == null ? null
+ : sender.getKey() == null ? sender.getName() : sender.getKey();
+ isNewGroup |= !TextUtils.equals(key, currentSenderKey);
+ if (isNewGroup) {
+ currentGroup = new ArrayList<>();
+ groups.add(currentGroup);
+ if (sender == null) {
+ sender = mUser;
+ }
+ senders.add(sender);
+ currentSenderKey = key;
+ }
+ currentGroup.add(message);
+ }
+ }
+
+ /**
+ * Creates new messages, reusing existing ones if they are available.
+ *
+ * @param newMessages the messages to parse.
+ */
+ private List<MessagingMessage> createMessages(
+ List<Notification.MessagingStyle.Message> newMessages, boolean historic) {
+ List<MessagingMessage> result = new ArrayList<>();
+ for (int i = 0; i < newMessages.size(); i++) {
+ Notification.MessagingStyle.Message m = newMessages.get(i);
+ MessagingMessage message = findAndRemoveMatchingMessage(m);
+ if (message == null) {
+ message = MessagingMessage.createMessage(this, m, mImageResolver);
+ }
+ message.setIsHistoric(historic);
+ result.add(message);
+ }
+ return result;
+ }
+
+ private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) {
+ for (int i = 0; i < mMessages.size(); i++) {
+ MessagingMessage existing = mMessages.get(i);
+ if (existing.sameAs(m)) {
+ mMessages.remove(i);
+ return existing;
+ }
+ }
+ for (int i = 0; i < mHistoricMessages.size(); i++) {
+ MessagingMessage existing = mHistoricMessages.get(i);
+ if (existing.sameAs(m)) {
+ mHistoricMessages.remove(i);
+ return existing;
+ }
+ }
+ return null;
+ }
+
+ public void showHistoricMessages(boolean show) {
+ mShowHistoricMessages = show;
+ updateHistoricMessageVisibility();
+ }
+
+ private void updateHistoricMessageVisibility() {
+ int numHistoric = mHistoricMessages.size();
+ for (int i = 0; i < numHistoric; i++) {
+ MessagingMessage existing = mHistoricMessages.get(i);
+ existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE);
+ }
+ int numGroups = mGroups.size();
+ for (int i = 0; i < numGroups; i++) {
+ MessagingGroup group = mGroups.get(i);
+ int visibleChildren = 0;
+ List<MessagingMessage> messages = group.getMessages();
+ int numGroupMessages = messages.size();
+ for (int j = 0; j < numGroupMessages; j++) {
+ MessagingMessage message = messages.get(j);
+ if (message.getVisibility() != GONE) {
+ visibleChildren++;
+ }
+ }
+ if (visibleChildren > 0 && group.getVisibility() == GONE) {
+ group.setVisibility(VISIBLE);
+ } else if (visibleChildren == 0 && group.getVisibility() != GONE) {
+ group.setVisibility(GONE);
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (!mAddedGroups.isEmpty()) {
+ getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ for (MessagingGroup group : mAddedGroups) {
+ if (!group.isShown()) {
+ continue;
+ }
+ MessagingPropertyAnimator.fadeIn(group.getAvatar());
+ MessagingPropertyAnimator.fadeIn(group.getSenderView());
+ MessagingPropertyAnimator.startLocalTranslationFrom(group,
+ group.getHeight(), LINEAR_OUT_SLOW_IN);
+ }
+ mAddedGroups.clear();
+ getViewTreeObserver().removeOnPreDrawListener(this);
+ return true;
+ }
+ });
+ }
+ }
+
+ public MessagingLinearLayout getMessagingLinearLayout() {
+ return mMessagingLinearLayout;
+ }
+
+ public ArrayList<MessagingGroup> getMessagingGroups() {
+ return mGroups;
+ }
+
+ private void updateExpandButton() {
+ int drawableId;
+ int contentDescriptionId;
+ int gravity;
+ int topMargin = 0;
+ if (mIsCollapsed) {
+ drawableId = R.drawable.ic_expand_notification;
+ contentDescriptionId = R.string.expand_button_content_description_collapsed;
+ gravity = Gravity.CENTER;
+ } else {
+ drawableId = R.drawable.ic_collapse_notification;
+ contentDescriptionId = R.string.expand_button_content_description_expanded;
+ gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
+ topMargin = mExpandButtonExpandedTopMargin;
+ }
+ mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
+ mExpandButton.setColorFilter(mExpandButton.getOriginalNotificationColor());
+
+ // update if the expand button is centered
+ FrameLayout.LayoutParams layoutParams = (LayoutParams) mExpandButton.getLayoutParams();
+ layoutParams.gravity = gravity;
+ layoutParams.topMargin = topMargin;
+ mExpandButton.setLayoutParams(layoutParams);
+
+ mExpandButtonContainer.setContentDescription(mContext.getText(contentDescriptionId));
+ }
+
+ public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) {
+ if (expandable) {
+ mExpandButtonContainer.setVisibility(VISIBLE);
+ mExpandButtonContainer.setOnClickListener(onClickListener);
+ } else {
+ // TODO: handle content paddings to end of layout
+ mExpandButtonContainer.setVisibility(GONE);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/widget/IMessagingLayout.java b/core/java/com/android/internal/widget/IMessagingLayout.java
new file mode 100644
index 0000000..149d056
--- /dev/null
+++ b/core/java/com/android/internal/widget/IMessagingLayout.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.internal.widget;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+
+/**
+ * An interface for a MessagingLayout
+ */
+public interface IMessagingLayout {
+
+ /**
+ * @return the layout containing the messages
+ */
+ MessagingLinearLayout getMessagingLinearLayout();
+
+ /**
+ * @return the context of this view
+ */
+ Context getContext();
+
+ /**
+ * @return the list of messaging groups
+ */
+ ArrayList<MessagingGroup> getMessagingGroups();
+}
diff --git a/core/java/com/android/internal/widget/MessagingGroup.java b/core/java/com/android/internal/widget/MessagingGroup.java
index c9a9161..3238131 100644
--- a/core/java/com/android/internal/widget/MessagingGroup.java
+++ b/core/java/com/android/internal/widget/MessagingGroup.java
@@ -55,8 +55,9 @@
private static Pools.SimplePool<MessagingGroup> sInstancePool
= new Pools.SynchronizedPool<>(10);
private MessagingLinearLayout mMessageContainer;
- private ImageFloatingTextView mSenderName;
+ ImageFloatingTextView mSenderView;
private ImageView mAvatarView;
+ private View mAvatarContainer;
private String mAvatarSymbol = "";
private int mLayoutColor;
private CharSequence mAvatarName = "";
@@ -76,6 +77,12 @@
private Point mDisplaySize = new Point();
private ProgressBar mSendingSpinner;
private View mSendingSpinnerContainer;
+ private boolean mShowingAvatar = true;
+ private CharSequence mSenderName;
+ private boolean mSingleLine = false;
+ private LinearLayout mContentContainer;
+ private int mRequestedMaxDisplayedLines = Integer.MAX_VALUE;
+ private int mSenderTextPaddingSingleLine;
public MessagingGroup(@NonNull Context context) {
super(context);
@@ -99,26 +106,34 @@
protected void onFinishInflate() {
super.onFinishInflate();
mMessageContainer = findViewById(R.id.group_message_container);
- mSenderName = findViewById(R.id.message_name);
+ mSenderView = findViewById(R.id.message_name);
mAvatarView = findViewById(R.id.message_icon);
mImageContainer = findViewById(R.id.messaging_group_icon_container);
mSendingSpinner = findViewById(R.id.messaging_group_sending_progress);
+ mContentContainer = findViewById(R.id.messaging_group_content_container);
mSendingSpinnerContainer = findViewById(R.id.messaging_group_sending_progress_container);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
mDisplaySize.x = displayMetrics.widthPixels;
mDisplaySize.y = displayMetrics.heightPixels;
+ mSenderTextPaddingSingleLine = getResources().getDimensionPixelSize(
+ R.dimen.messaging_group_singleline_sender_padding_end);
}
public void updateClipRect() {
// We want to clip to the senderName if it's available, otherwise our images will come
// from a weird position
Rect clipRect;
- if (mSenderName.getVisibility() != View.GONE && !mTransformingImages) {
- ViewGroup parent = (ViewGroup) mSenderName.getParent();
- int top = getDistanceFromParent(mSenderName, parent) - getDistanceFromParent(
- mMessageContainer, parent) + mSenderName.getHeight();
+ if (mSenderView.getVisibility() != View.GONE && !mTransformingImages) {
+ int top;
+ if (mSingleLine) {
+ top = 0;
+ } else {
+ top = getDistanceFromParent(mSenderView, mContentContainer)
+ - getDistanceFromParent(mMessageContainer, mContentContainer)
+ + mSenderView.getHeight();
+ }
int size = Math.max(mDisplaySize.x, mDisplaySize.y);
- clipRect = new Rect(0, top, size, size);
+ clipRect = new Rect(-size, top, size, size);
} else {
clipRect = null;
}
@@ -140,17 +155,27 @@
if (nameOverride == null) {
nameOverride = sender.getName();
}
- mSenderName.setText(nameOverride);
+ mSenderName = nameOverride;
+ mSenderView.setText(nameOverride);
mNeedsGeneratedAvatar = sender.getIcon() == null;
if (!mNeedsGeneratedAvatar) {
setAvatar(sender.getIcon());
}
- mAvatarView.setVisibility(VISIBLE);
- mSenderName.setVisibility(TextUtils.isEmpty(nameOverride) ? GONE : VISIBLE);
+ mSenderView.setVisibility(TextUtils.isEmpty(nameOverride) ? GONE : VISIBLE);
+ }
+
+ /**
+ * Should the avatar be shown for this view.
+ *
+ * @param showingAvatar should it be shown
+ */
+ public void setShowingAvatar(boolean showingAvatar) {
+ mAvatarView.setVisibility(showingAvatar ? VISIBLE : GONE);
+ mShowingAvatar = showingAvatar;
}
public void setSending(boolean sending) {
- int visibility = sending ? View.VISIBLE : View.GONE;
+ int visibility = sending ? VISIBLE : GONE;
if (mSendingSpinnerContainer.getVisibility() != visibility) {
mSendingSpinnerContainer.setVisibility(visibility);
updateMessageColor();
@@ -171,7 +196,9 @@
public void setAvatar(Icon icon) {
mAvatarIcon = icon;
- mAvatarView.setImageIcon(icon);
+ if (mShowingAvatar || icon == null) {
+ mAvatarView.setImageIcon(icon);
+ }
mAvatarSymbol = "";
mAvatarName = "";
}
@@ -220,13 +247,17 @@
setAvatar(null);
mAvatarView.setAlpha(1.0f);
mAvatarView.setTranslationY(0.0f);
- mSenderName.setAlpha(1.0f);
- mSenderName.setTranslationY(0.0f);
+ mSenderView.setAlpha(1.0f);
+ mSenderView.setTranslationY(0.0f);
setAlpha(1.0f);
mIsolatedMessage = null;
mMessages = null;
+ mSenderName = null;
mAddedMessages.clear();
mFirstLayout = true;
+ setMaxDisplayedLines(Integer.MAX_VALUE);
+ setSingleLine(false);
+ setShowingAvatar(true);
MessagingPropertyAnimator.recycle(this);
sInstancePool.release(MessagingGroup.this);
}
@@ -252,7 +283,7 @@
}
public CharSequence getSenderName() {
- return mSenderName.getText();
+ return mSenderName;
}
public static void dropCache() {
@@ -310,7 +341,12 @@
@Override
public void setMaxDisplayedLines(int lines) {
- mMessageContainer.setMaxDisplayedLines(lines);
+ mRequestedMaxDisplayedLines = lines;
+ updateMaxDisplayedLines();
+ }
+
+ private void updateMaxDisplayedLines() {
+ mMessageContainer.setMaxDisplayedLines(mSingleLine ? 1 : mRequestedMaxDisplayedLines);
}
@Override
@@ -362,7 +398,7 @@
mTextColor = messageTextColor;
mSendingTextColor = calculateSendingTextColor();
updateMessageColor();
- mSenderName.setTextColor(senderTextColor);
+ mSenderView.setTextColor(senderTextColor);
}
public void setLayoutColor(int layoutColor) {
@@ -506,13 +542,17 @@
}
public View getSenderView() {
- return mSenderName;
+ return mSenderView;
}
public View getAvatar() {
return mAvatarView;
}
+ public Icon getAvatarIcon() {
+ return mAvatarIcon;
+ }
+
public MessagingLinearLayout getMessageContainer() {
return mMessageContainer;
}
@@ -543,4 +583,22 @@
public List<MessagingMessage> getMessages() {
return mMessages;
}
+
+ /**
+ * Set this layout to be single line and therefore displaying both the sender and the text on
+ * the same line.
+ *
+ * @param singleLine should be layout be single line
+ */
+ public void setSingleLine(boolean singleLine) {
+ if (singleLine != mSingleLine) {
+ mSingleLine = singleLine;
+ mContentContainer.setOrientation(
+ singleLine ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
+ MarginLayoutParams layoutParams = (MarginLayoutParams) mSenderView.getLayoutParams();
+ layoutParams.setMarginEnd(singleLine ? mSenderTextPaddingSingleLine : 0);
+ updateMaxDisplayedLines();
+ updateClipRect();
+ }
+ }
}
diff --git a/core/java/com/android/internal/widget/MessagingImageMessage.java b/core/java/com/android/internal/widget/MessagingImageMessage.java
index 64650a7..c243f3b 100644
--- a/core/java/com/android/internal/widget/MessagingImageMessage.java
+++ b/core/java/com/android/internal/widget/MessagingImageMessage.java
@@ -120,7 +120,7 @@
return true;
}
- static MessagingMessage createMessage(MessagingLayout layout,
+ static MessagingMessage createMessage(IMessagingLayout layout,
Notification.MessagingStyle.Message m, ImageResolver resolver) {
MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
MessagingImageMessage createdMessage = sInstancePool.acquire();
diff --git a/core/java/com/android/internal/widget/MessagingLayout.java b/core/java/com/android/internal/widget/MessagingLayout.java
index f608958..bb2faecf 100644
--- a/core/java/com/android/internal/widget/MessagingLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLayout.java
@@ -58,7 +58,8 @@
* messages and adapts the layout accordingly.
*/
@RemoteViews.RemoteView
-public class MessagingLayout extends FrameLayout implements ImageMessageConsumer {
+public class MessagingLayout extends FrameLayout
+ implements ImageMessageConsumer, IMessagingLayout {
private static final float COLOR_SHIFT_AMOUNT = 60;
/**
@@ -143,9 +144,19 @@
mNameReplacement = nameReplacement;
}
+ /**
+ * Set this layout to show the collapsed representation.
+ *
+ * @param isCollapsed is it collapsed
+ */
@RemotableViewMethod
- public void setDisplayImagesAtEnd(boolean atEnd) {
- mDisplayImagesAtEnd = atEnd;
+ public void setIsCollapsed(boolean isCollapsed) {
+ mDisplayImagesAtEnd = isCollapsed;
+ }
+
+ @RemotableViewMethod
+ public void setLargeIcon(Icon largeIcon) {
+ // Unused
}
@RemotableViewMethod
diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java
index 0c8613b..9e54d11 100644
--- a/core/java/com/android/internal/widget/MessagingLinearLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java
@@ -43,7 +43,7 @@
private int mMaxDisplayedLines = Integer.MAX_VALUE;
- private MessagingLayout mMessagingLayout;
+ private IMessagingLayout mMessagingLayout;
public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -255,11 +255,11 @@
mMaxDisplayedLines = numberLines;
}
- public void setMessagingLayout(MessagingLayout layout) {
+ public void setMessagingLayout(IMessagingLayout layout) {
mMessagingLayout = layout;
}
- public MessagingLayout getMessagingLayout() {
+ public IMessagingLayout getMessagingLayout() {
return mMessagingLayout;
}
diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java
index c32d370..8c84379 100644
--- a/core/java/com/android/internal/widget/MessagingMessage.java
+++ b/core/java/com/android/internal/widget/MessagingMessage.java
@@ -32,7 +32,7 @@
**/
String IMAGE_MIME_TYPE_PREFIX = "image/";
- static MessagingMessage createMessage(MessagingLayout layout,
+ static MessagingMessage createMessage(IMessagingLayout layout,
Notification.MessagingStyle.Message m, ImageResolver resolver) {
if (hasImage(m) && !ActivityManager.isLowRamDeviceStatic()) {
return MessagingImageMessage.createMessage(layout, m, resolver);
diff --git a/core/java/com/android/internal/widget/MessagingTextMessage.java b/core/java/com/android/internal/widget/MessagingTextMessage.java
index 4081a86..d778c59 100644
--- a/core/java/com/android/internal/widget/MessagingTextMessage.java
+++ b/core/java/com/android/internal/widget/MessagingTextMessage.java
@@ -26,14 +26,10 @@
import android.util.AttributeSet;
import android.util.Pools;
import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.view.ViewParent;
import android.widget.RemoteViews;
import com.android.internal.R;
-import java.util.Objects;
-
/**
* A message of a {@link MessagingLayout}.
*/
@@ -74,7 +70,7 @@
return true;
}
- static MessagingMessage createMessage(MessagingLayout layout,
+ static MessagingMessage createMessage(IMessagingLayout layout,
Notification.MessagingStyle.Message m) {
MessagingLinearLayout messagingLinearLayout = layout.getMessagingLinearLayout();
MessagingTextMessage createdMessage = sInstancePool.acquire();
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index 39f82a5..a499806 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -20,7 +20,7 @@
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
-import android.view.View;
+import android.view.RemotableViewMethod;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Button;
import android.widget.ImageView;
@@ -32,6 +32,8 @@
@RemoteViews.RemoteView
public class NotificationExpandButton extends ImageView {
+ private int mOriginalNotificationColor;
+
public NotificationExpandButton(Context context) {
super(context);
}
@@ -56,6 +58,15 @@
extendRectToMinTouchSize(outRect);
}
+ @RemotableViewMethod
+ public void setOriginalNotificationColor(int color) {
+ mOriginalNotificationColor = color;
+ }
+
+ public int getOriginalNotificationColor() {
+ return mOriginalNotificationColor;
+ }
+
private void extendRectToMinTouchSize(Rect rect) {
int touchTargetSize = (int) (getResources().getDisplayMetrics().density * 48);
rect.left = rect.centerX() - touchTargetSize / 2;
diff --git a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
index e352b45..729c9e3 100644
--- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
+++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java
@@ -53,6 +53,7 @@
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
int height = 0;
+ boolean isVertical = getOrientation() == LinearLayout.VERTICAL;
for (int i = 0; i < count; ++i) {
final View child = getChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
@@ -60,8 +61,8 @@
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- height = Math.max(height, height + child.getMeasuredHeight() + lp.topMargin +
- lp.bottomMargin);
+ int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
+ height = Math.max(height, isVertical ? height + childHeight : childHeight);
}
setMeasuredDimension(getMeasuredWidth(), height);
}
diff --git a/core/res/res/drawable/conversation_badge_background.xml b/core/res/res/drawable/conversation_badge_background.xml
new file mode 100644
index 0000000..0dd0dcd
--- /dev/null
+++ b/core/res/res/drawable/conversation_badge_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <solid
+ android:color="#ffffff"/>
+
+ <size
+ android:width="26dp"
+ android:height="26dp"/>
+</shape>
+
diff --git a/core/res/res/drawable/ic_collapse_notification.xml b/core/res/res/drawable/ic_collapse_notification.xml
index 124e99e..ca4f0ed 100644
--- a/core/res/res/drawable/ic_collapse_notification.xml
+++ b/core/res/res/drawable/ic_collapse_notification.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
-Copyright (C) 2015 The Android Open Source Project
+Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,11 +15,14 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14.0dp"
- android:height="14.0dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:width="22.0dp"
+ android:height="22.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
- android:pathData="M12.0,8.0l-6.0,6.0l1.4,1.4l4.6,-4.6l4.6,4.6L18.0,14.0L12.0,8.0z"/>
-</vector>
+ android:pathData="M18.59,16.41L20.0,15.0l-8.0,-8.0 -8.0,8.0 1.41,1.41L12.0,9.83"/>
+ <path
+ android:pathData="M0 0h24v24H0V0z"
+ android:fillColor="#00000000"/>
+</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_expand_notification.xml b/core/res/res/drawable/ic_expand_notification.xml
index 847e326..a080ce4 100644
--- a/core/res/res/drawable/ic_expand_notification.xml
+++ b/core/res/res/drawable/ic_expand_notification.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
-Copyright (C) 2015 The Android Open Source Project
+Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,11 +15,14 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="14.0dp"
- android:height="14.0dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
+ android:width="22.0dp"
+ android:height="22.0dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
- android:pathData="M16.6,8.6L12.0,13.2L7.4,8.6L6.0,10.0l6.0,6.0l6.0,-6.0L16.6,8.6z"/>
-</vector>
+ android:pathData="M5.41,7.59L4.0,9.0l8.0,8.0 8.0,-8.0 -1.41,-1.41L12.0,14.17"/>
+ <path
+ android:pathData="M24 24H0V0h24v24z"
+ android:fillColor="#00000000"/>
+</vector>
\ No newline at end of file
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index f5fa1b6a..6f36aae 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -91,7 +91,6 @@
android:textAppearance="@style/TextAppearance.Material.Notification.Time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center"
android:layout_marginStart="@dimen/notification_header_separating_margin"
android:layout_marginEnd="@dimen/notification_header_separating_margin"
android:showRelative="true"
diff --git a/core/res/res/layout/notification_template_material_conversation.xml b/core/res/res/layout/notification_template_material_conversation.xml
new file mode 100644
index 0000000..f0721173
--- /dev/null
+++ b/core/res/res/layout/notification_template_material_conversation.xml
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<com.android.internal.widget.ConversationLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/status_bar_latest_event_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:tag="conversation"
+ android:theme="@style/Theme.DeviceDefault.Notification"
+ >
+
+ <FrameLayout
+ android:layout_width="@dimen/conversation_content_start"
+ android:layout_height="wrap_content"
+ android:gravity="start|top"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:paddingTop="12dp"
+ >
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|center_horizontal"
+ >
+
+ <!-- Big icon: 52x52, 12dp padding left + top, 16dp padding right -->
+ <ImageView
+ android:id="@+id/conversation_icon"
+ android:layout_width="@dimen/conversation_avatar_size"
+ android:layout_height="@dimen/conversation_avatar_size"
+ android:scaleType="centerCrop"
+ android:importantForAccessibility="no"
+ />
+
+ <FrameLayout
+ android:id="@+id/conversation_icon_badge"
+ android:layout_width="20dp"
+ android:layout_height="20dp"
+ android:layout_marginLeft="@dimen/conversation_badge_side_margin"
+ android:layout_marginTop="@dimen/conversation_badge_side_margin"
+ android:background="@drawable/conversation_badge_background" >
+ <!-- Badge: 20x20, 48dp padding left + top -->
+ <com.android.internal.widget.CachingIconView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/conversation_icon_size_badged"
+ android:layout_height="@dimen/conversation_icon_size_badged"
+ android:layout_gravity="center"
+ />
+ </FrameLayout>
+ </FrameLayout>
+ </FrameLayout>
+
+ <!-- Wraps entire "expandable" notification -->
+ <com.android.internal.widget.RemeasuringLinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"
+ android:clipToPadding="false"
+ android:clipChildren="false"
+ android:orientation="vertical"
+ >
+ <!-- LinearLayout for Expand Button-->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:gravity="start|top"
+ android:orientation="horizontal"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+ <!--TODO: move this into a separate layout and share logic with the header to bring back app opps etc-->
+ <FrameLayout
+ android:id="@+id/notification_action_list_margin_target"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1">
+
+ <!-- Header -->
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingTop="16dp"
+ android:paddingStart="@dimen/conversation_content_start"
+ >
+ <TextView
+ android:id="@+id/header_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/notification_header_separating_margin"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
+ android:textSize="16sp"
+ android:singleLine="true"
+ />
+ <TextView
+ android:id="@+id/header_text_divider"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/notificationHeaderTextAppearance"
+ android:layout_marginStart="@dimen/notification_header_separating_margin"
+ android:layout_marginEnd="4dp"
+ android:text="@string/notification_header_divider_symbol"
+ android:paddingTop="1sp"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/header_text_secondary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
+ android:layout_marginEnd="@dimen/notification_header_separating_margin"
+ android:textSize="16sp"
+ android:visibility="gone"
+ android:singleLine="true"/>
+
+ <TextView
+ android:id="@+id/time_divider"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/notificationHeaderTextAppearance"
+ android:layout_marginStart="@dimen/notification_header_separating_margin"
+ android:layout_marginEnd="@dimen/notification_header_separating_margin"
+ android:text="@string/notification_header_divider_symbol"
+ android:layout_gravity="center"
+ android:paddingTop="1sp"
+ android:singleLine="true"
+ android:visibility="gone"
+ />
+
+ <DateTimeView
+ android:id="@+id/time"
+ android:textAppearance="@style/TextAppearance.Material.Notification.Time"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginStart="@dimen/notification_header_separating_margin"
+ android:paddingTop="1sp"
+ android:showRelative="true"
+ android:singleLine="true"
+ android:visibility="gone"
+ />
+
+ <ImageView
+ android:id="@+id/profile_badge"
+ android:layout_width="@dimen/notification_badge_size"
+ android:layout_height="@dimen/notification_badge_size"
+ android:layout_gravity="center"
+ android:layout_marginStart="4dp"
+ android:paddingTop="2dp"
+ android:scaleType="fitCenter"
+ android:visibility="gone"
+ android:contentDescription="@string/notification_work_profile_content_description"
+ />
+ </LinearLayout>
+
+ <!-- Messages -->
+ <com.android.internal.widget.MessagingLinearLayout
+ android:id="@+id/notification_messaging"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="40dp"
+ android:spacing="@dimen/notification_messaging_spacing"
+ android:clipToPadding="false"
+ android:clipChildren="false"
+ />
+ </FrameLayout>
+ <!-- Unread Count -->
+ <!-- <TextView /> -->
+
+ <FrameLayout
+ android:id="@+id/expand_button_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingStart="16dp"
+ android:paddingEnd="@dimen/notification_content_margin_end">
+ <!--TODO: figure out tinting for exander-->
+ <com.android.internal.widget.NotificationExpandButton
+ android:id="@+id/expand_button"
+ android:layout_width="@dimen/notification_header_expand_icon_size"
+ android:layout_height="@dimen/notification_header_expand_icon_size"
+ android:layout_gravity="center"
+ android:drawable="@drawable/ic_expand_notification"
+ android:clickable="false"
+ android:importantForAccessibility="no"
+ />
+ </FrameLayout>
+ />
+ </LinearLayout>
+
+ <include layout="@layout/notification_template_smart_reply_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_content_margin"
+ android:layout_marginStart="@dimen/conversation_content_start"
+ android:layout_marginEnd="@dimen/notification_content_margin_end" />
+ <include layout="@layout/notification_material_action_list" />
+ </com.android.internal.widget.RemeasuringLinearLayout>
+</com.android.internal.widget.ConversationLayout>
diff --git a/core/res/res/layout/notification_template_messaging_group.xml b/core/res/res/layout/notification_template_messaging_group.xml
index 483b479..15146c0 100644
--- a/core/res/res/layout/notification_template_messaging_group.xml
+++ b/core/res/res/layout/notification_template_messaging_group.xml
@@ -20,14 +20,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
- <ImageView
- android:id="@+id/message_icon"
- android:layout_width="@dimen/messaging_avatar_size"
- android:layout_height="@dimen/messaging_avatar_size"
- android:layout_marginEnd="12dp"
- android:scaleType="centerCrop"
- android:importantForAccessibility="no" />
+ <FrameLayout
+ android:layout_width="@dimen/conversation_content_start"
+ android:layout_height="wrap_content"> <!--TODO: make sure to make this padding dynamic-->
+ <ImageView
+ android:layout_gravity="top|center_horizontal"
+ android:id="@+id/message_icon"
+ android:layout_width="@dimen/messaging_avatar_size"
+ android:layout_height="@dimen/messaging_avatar_size"
+ android:scaleType="centerCrop"
+ android:importantForAccessibility="no" />
+ </FrameLayout>
<com.android.internal.widget.RemeasuringLinearLayout
+ android:id="@+id/messaging_group_content_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
@@ -43,7 +48,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/notification_text_margin_top"
- android:spacing="2dp"/>
+ android:spacing="2dp" />
</com.android.internal.widget.RemeasuringLinearLayout>
<FrameLayout
android:id="@+id/messaging_group_icon_container"
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 9118617..c437184 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -683,6 +683,22 @@
<dimen name="notification_right_icon_size_low_ram">@dimen/notification_right_icon_size</dimen>
<dimen name="messaging_avatar_size">52dp</dimen>
+ <dimen name="conversation_avatar_size">52dp</dimen>
+ <!-- Start of the content in the conversation template -->
+ <dimen name="conversation_content_start">80dp</dimen>
+ <!-- Top margin of the expand button for conversations when expanded -->
+ <dimen name="conversation_expand_button_top_margin_expanded">18dp</dimen>
+ <!-- Side margins of the conversation badge in relation to the conversation icon -->
+ <dimen name="conversation_badge_side_margin">36dp</dimen>
+ <!-- size of the notification icon when badged in a conversation -->
+ <dimen name="conversation_icon_size_badged">15dp</dimen>
+ <!-- size of the notification icon when centered in a conversation -->
+ <dimen name="conversation_icon_size_centered">20dp</dimen>
+ <!-- margin on the top when the icon is centered for group conversations -->
+ <dimen name="conversation_icon_margin_top_centered">5dp</dimen>
+
+ <!-- Padding between text and sender when singleline -->
+ <dimen name="messaging_group_singleline_sender_padding_end">4dp</dimen>
<dimen name="messaging_group_sending_progress_size">24dp</dimen>
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 63ac0e6..2415837 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -504,7 +504,7 @@
<style name="Widget.Material.Notification.MessagingText" parent="Widget.Material.Notification.Text">
<item name="layout_width">wrap_content</item>
<item name="layout_height">wrap_content</item>
- <item name="ellipsize">end</item>z
+ <item name="ellipsize">end</item>
</style>
<style name="Widget.Material.Notification.MessagingName" parent="Widget.Material.Light.TextView">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 826379d..f4d8c1c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3876,6 +3876,18 @@
<java-symbol type="array" name="config_defaultImperceptibleKillingExemptionPkgs" />
<java-symbol type="array" name="config_defaultImperceptibleKillingExemptionProcStates" />
+ <java-symbol type="id" name="conversation_icon" />
+ <java-symbol type="id" name="conversation_icon_badge" />
+ <java-symbol type="id" name="expand_button_container" />
+ <java-symbol type="id" name="messaging_group_content_container" />
+ <java-symbol type="dimen" name="conversation_expand_button_top_margin_expanded" />
+ <java-symbol type="dimen" name="messaging_group_singleline_sender_padding_end" />
+ <java-symbol type="dimen" name="conversation_badge_side_margin" />
+ <java-symbol type="dimen" name="conversation_icon_size_badged" />
+ <java-symbol type="dimen" name="conversation_icon_size_centered" />
+ <java-symbol type="dimen" name="conversation_icon_margin_top_centered" />
+ <java-symbol type="layout" name="notification_template_material_conversation" />
+
<!-- Intent resolver and share sheet -->
<java-symbol type="color" name="resolver_tabs_active_color" />
<java-symbol type="color" name="resolver_tabs_inactive_color" />