diff options
35 files changed, 1757 insertions, 155 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index f59ccfb13636..061e5ff35f55 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; 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable } 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() { @@ -7390,7 +7394,7 @@ public class Notification implements Parcelable 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; @@ -7480,19 +7484,18 @@ public class Notification implements Parcelable */ @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; @@ -7533,14 +7536,21 @@ public class Notification implements Parcelable 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.setInt(R.id.status_bar_latest_event_content, + "setNotificationBackgroundColor", + mBuilder.resolveBackgroundColor(p)); + 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.setCharSequence(R.id.status_bar_latest_event_content, + "setConversationTitle", conversationTitle); + 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; @@ -7601,9 +7611,11 @@ public class Notification implements Parcelable */ @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 8ec5df85dc7b..18e0132e2c4e 100644 --- a/core/java/android/view/NotificationHeaderView.java +++ b/core/java/android/view/NotificationHeaderView.java @@ -35,6 +35,7 @@ import android.widget.RemoteViews; 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 @@ public class NotificationHeaderView extends ViewGroup { 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 @@ public class NotificationHeaderView extends ViewGroup { 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 @@ public class NotificationHeaderView extends ViewGroup { return mIconColor; } - @RemotableViewMethod - public void setOriginalNotificationColor(int color) { - mOriginalNotificationColor = color; - } - public int getOriginalNotificationColor() { - return mOriginalNotificationColor; + return mExpandButton.getOriginalNotificationColor(); } @RemotableViewMethod @@ -371,7 +366,7 @@ public class NotificationHeaderView extends ViewGroup { 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/CachingIconView.java b/core/java/com/android/internal/widget/CachingIconView.java index 74ad81566ef4..bd0623e1144e 100644 --- a/core/java/com/android/internal/widget/CachingIconView.java +++ b/core/java/com/android/internal/widget/CachingIconView.java @@ -32,6 +32,7 @@ import android.widget.ImageView; import android.widget.RemoteViews; import java.util.Objects; +import java.util.function.Consumer; /** * An ImageView for displaying an Icon. Avoids reloading the Icon when possible. @@ -44,6 +45,7 @@ public class CachingIconView extends ImageView { private boolean mInternalSetDrawable; private boolean mForceHidden; private int mDesiredVisibility; + private Consumer<Integer> mOnVisibilityChangedListener; @UnsupportedAppUsage public CachingIconView(Context context, @Nullable AttributeSet attrs) { @@ -198,6 +200,13 @@ public class CachingIconView extends ImageView { private void updateVisibility() { int visibility = mDesiredVisibility == VISIBLE && mForceHidden ? INVISIBLE : mDesiredVisibility; + if (mOnVisibilityChangedListener != null) { + mOnVisibilityChangedListener.accept(visibility); + } super.setVisibility(visibility); } + + public void setOnVisibilityChangedListener(Consumer<Integer> listener) { + mOnVisibilityChangedListener = listener; + } } 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 000000000000..07be113d9a53 --- /dev/null +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -0,0 +1,866 @@ +/* + * 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.content.res.ColorStateList; +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 mConversationText; + private View mConversationIconBadge; + private Icon mLargeIcon; + private View mExpandButtonContainer; + private ViewGroup mExpandButtonAndContentContainer; + private NotificationExpandButton mExpandButton; + private int mExpandButtonExpandedTopMargin; + private int mBadgedSideMargins; + private int mIconSizeBadged; + private int mIconSizeCentered; + private CachingIconView mIcon; + private int mExpandedGroupTopMargin; + private int mExpandButtonExpandedSize; + private View mConversationFacePile; + private int mNotificationBackgroundColor; + private CharSequence mFallbackChatName; + private CharSequence mFallbackGroupChatName; + private CharSequence mConversationTitle; + + 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); + mIcon.setOnVisibilityChangedListener((visibility) -> { + // Always keep the badge visibility in sync with the icon. This is necessary in cases + // Where the icon is being hidden externally like in group children. + mConversationIconBadge.setVisibility(visibility); + }); + mConversationText = findViewById(R.id.conversation_text); + mExpandButtonContainer = findViewById(R.id.expand_button_container); + mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container); + mExpandButton = findViewById(R.id.expand_button); + mExpandButtonExpandedTopMargin = getResources().getDimensionPixelSize( + R.dimen.conversation_expand_button_top_margin_expanded); + mExpandButtonExpandedSize = getResources().getDimensionPixelSize( + R.dimen.conversation_expand_button_expanded_size); + 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); + mConversationFacePile = findViewById(R.id.conversation_face_pile); + mFallbackChatName = getResources().getString( + R.string.conversation_title_fallback_one_to_one); + mFallbackGroupChatName = getResources().getString( + R.string.conversation_title_fallback_group_chat); + } + + @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(); + + updateConversationLayout(); + + } + + /** + * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc); + */ + private void updateConversationLayout() { + // TODO: resolve this from shortcuts + // Set avatar and name + CharSequence conversationText = mConversationTitle; + // TODO: display the secondary text somewhere + if (mIsOneToOne) { + // Let's resolve the icon / text from the last sender + mConversationIcon.setVisibility(VISIBLE); + mConversationFacePile.setVisibility(GONE); + CharSequence userKey = getKey(mUser); + for (int i = mGroups.size() - 1; i >= 0; i--) { + MessagingGroup messagingGroup = mGroups.get(i); + Person messageSender = messagingGroup.getSender(); + if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender))) + || i == 0) { + if (TextUtils.isEmpty(conversationText)) { + // We use the sendername as header text if no conversation title is provided + // (This usually happens for most 1:1 conversations) + conversationText = messagingGroup.getSenderName(); + } + Icon avatarIcon = messagingGroup.getAvatarIcon(); + if (avatarIcon == null) { + avatarIcon = createAvatarSymbol(conversationText, "", mLayoutColor); + } + mConversationIcon.setImageIcon(avatarIcon); + break; + } + } + } else { + if (mIsCollapsed) { + if (mLargeIcon != null) { + mConversationIcon.setVisibility(VISIBLE); + mConversationFacePile.setVisibility(GONE); + mConversationIcon.setImageIcon(mLargeIcon); + } else { + mConversationIcon.setVisibility(GONE); + // This will also inflate it! + mConversationFacePile.setVisibility(VISIBLE); + mConversationFacePile = findViewById(R.id.conversation_face_pile); + bindFacePile(); + } + } else { + mConversationFacePile.setVisibility(GONE); + mConversationIcon.setVisibility(GONE); + } + } + if (TextUtils.isEmpty(conversationText)) { + conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName; + } + 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); + } + updateIconPositionAndSize(); + } + + private void bindFacePile() { + // Let's bind the face pile + View bottomBackground = mConversationFacePile.findViewById( + R.id.conversation_face_pile_bottom_background); + applyNotificationBackgroundColor(bottomBackground); + ImageView bottomView = mConversationFacePile.findViewById( + R.id.conversation_face_pile_bottom); + ImageView topView = mConversationFacePile.findViewById( + R.id.conversation_face_pile_top); + // Let's find the two last conversations: + Icon secondLastIcon = null; + CharSequence lastKey = null; + Icon lastIcon = null; + CharSequence userKey = getKey(mUser); + for (int i = mGroups.size() - 1; i >= 0; i--) { + MessagingGroup messagingGroup = mGroups.get(i); + Person messageSender = messagingGroup.getSender(); + boolean notUser = messageSender != null + && !TextUtils.equals(userKey, getKey(messageSender)); + boolean notIncluded = messageSender != null + && !TextUtils.equals(lastKey, getKey(messageSender)); + if ((notUser && notIncluded) + || (i == 0 && lastKey == null)) { + if (lastIcon == null) { + lastIcon = messagingGroup.getAvatarIcon(); + lastKey = getKey(messageSender); + } else { + secondLastIcon = messagingGroup.getAvatarIcon(); + break; + } + } + } + if (lastIcon == null) { + lastIcon = createAvatarSymbol(" ", "", mLayoutColor); + } + bottomView.setImageIcon(lastIcon); + if (secondLastIcon == null) { + secondLastIcon = createAvatarSymbol("", "", mLayoutColor); + } + topView.setImageIcon(secondLastIcon); + } + + /** + * update the icon position and sizing + */ + private void updateIconPositionAndSize() { + 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; + } + 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; + } + + /** + * Sets the conversation title of this conversation. + * + * @param conversationTitle the conversation title + */ + @RemotableViewMethod + public void setConversationTitle(CharSequence conversationTitle) { + mConversationTitle = conversationTitle; + } + + 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; + } + + /** + * @param color the color of the notification background + */ + @RemotableViewMethod + public void setNotificationBackgroundColor(int color) { + mNotificationBackgroundColor = color; + applyNotificationBackgroundColor(mConversationIconBadge); + } + + private void applyNotificationBackgroundColor(View view) { + view.setBackgroundTintList(ColorStateList.valueOf(mNotificationBackgroundColor)); + } + + @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 = getKey(sender); + 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); + } + } + + private CharSequence getKey(Person person) { + return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); + } + + /** + * 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; + ViewGroup newContainer; + int newContainerHeight; + if (mIsCollapsed) { + drawableId = R.drawable.ic_expand_notification; + contentDescriptionId = R.string.expand_button_content_description_collapsed; + gravity = Gravity.CENTER; + newContainer = mExpandButtonAndContentContainer; + newContainerHeight = LayoutParams.MATCH_PARENT; + } else { + drawableId = R.drawable.ic_collapse_notification; + contentDescriptionId = R.string.expand_button_content_description_expanded; + gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; + topMargin = mExpandButtonExpandedTopMargin; + newContainer = this; + newContainerHeight = mExpandButtonExpandedSize; + } + mExpandButton.setImageDrawable(getContext().getDrawable(drawableId)); + mExpandButton.setColorFilter(mExpandButton.getOriginalNotificationColor()); + + // We need to make sure that the expand button is in the linearlayout pushing over the + // content when collapsed, but allows the content to flow under it when expanded. + if (newContainer != mExpandButtonContainer.getParent()) { + ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer); + newContainer.addView(mExpandButtonContainer); + MarginLayoutParams layoutParams = + (MarginLayoutParams) mExpandButtonContainer.getLayoutParams(); + layoutParams.height = newContainerHeight; + mExpandButtonContainer.setLayoutParams(layoutParams); + } + + // 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 000000000000..149d05641a0b --- /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 c9a916187d33..99779032eebc 100644 --- a/core/java/com/android/internal/widget/MessagingGroup.java +++ b/core/java/com/android/internal/widget/MessagingGroup.java @@ -55,8 +55,9 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou 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 = ""; @@ -72,10 +73,18 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou private boolean mImagesAtEnd; private ViewGroup mImageContainer; private MessagingImageMessage mIsolatedMessage; - private boolean mTransformingImages; + private boolean mClippingDisabled; 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; + private boolean mIsFirstGroupInLayout = true; + private boolean mCanHideSenderIfFirst; public MessagingGroup(@NonNull Context context) { super(context); @@ -99,26 +108,34 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou 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 && !mClippingDisabled) { + 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 +157,31 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou if (nameOverride == null) { nameOverride = sender.getName(); } - mSenderName.setText(nameOverride); + mSenderName = nameOverride; + if (mSingleLine && !TextUtils.isEmpty(nameOverride)) { + nameOverride = mContext.getResources().getString( + R.string.conversation_single_line_name_display, nameOverride); + } + mSenderView.setText(nameOverride); mNeedsGeneratedAvatar = sender.getIcon() == null; if (!mNeedsGeneratedAvatar) { setAvatar(sender.getIcon()); } - mAvatarView.setVisibility(VISIBLE); - mSenderName.setVisibility(TextUtils.isEmpty(nameOverride) ? GONE : VISIBLE); + updateSenderVisibility(); + } + + /** + * 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 +202,9 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou public void setAvatar(Icon icon) { mAvatarIcon = icon; - mAvatarView.setImageIcon(icon); + if (mShowingAvatar || icon == null) { + mAvatarView.setImageIcon(icon); + } mAvatarSymbol = ""; mAvatarName = ""; } @@ -220,13 +253,20 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou 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; + setCanHideSenderIfFirst(false); + setIsFirstInLayout(true); + + setMaxDisplayedLines(Integer.MAX_VALUE); + setSingleLine(false); + setShowingAvatar(true); MessagingPropertyAnimator.recycle(this); sInstancePool.release(MessagingGroup.this); } @@ -252,7 +292,7 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou } public CharSequence getSenderName() { - return mSenderName.getText(); + return mSenderName; } public static void dropCache() { @@ -310,7 +350,12 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou @Override public void setMaxDisplayedLines(int lines) { - mMessageContainer.setMaxDisplayedLines(lines); + mRequestedMaxDisplayedLines = lines; + updateMaxDisplayedLines(); + } + + private void updateMaxDisplayedLines() { + mMessageContainer.setMaxDisplayedLines(mSingleLine ? 1 : mRequestedMaxDisplayedLines); } @Override @@ -324,6 +369,35 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou return mIsHidingAnimated; } + @Override + public void setIsFirstInLayout(boolean first) { + if (first != mIsFirstGroupInLayout) { + mIsFirstGroupInLayout = first; + updateSenderVisibility(); + } + } + + /** + * @param canHide true if the sender can be hidden if it is first + */ + public void setCanHideSenderIfFirst(boolean canHide) { + if (mCanHideSenderIfFirst != canHide) { + mCanHideSenderIfFirst = canHide; + updateSenderVisibility(); + } + } + + private void updateSenderVisibility() { + boolean hidden = (mIsFirstGroupInLayout || mSingleLine) && mCanHideSenderIfFirst + || TextUtils.isEmpty(mSenderName); + mSenderView.setVisibility(hidden ? GONE : VISIBLE); + } + + @Override + public boolean hasDifferentHeightWhenFirst() { + return mCanHideSenderIfFirst && !mSingleLine && !TextUtils.isEmpty(mSenderName); + } + private void setIsHidingAnimated(boolean isHiding) { ViewParent parent = getParent(); mIsHidingAnimated = isHiding; @@ -362,7 +436,7 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou mTextColor = messageTextColor; mSendingTextColor = calculateSendingTextColor(); updateMessageColor(); - mSenderName.setTextColor(senderTextColor); + mSenderView.setTextColor(senderTextColor); } public void setLayoutColor(int layoutColor) { @@ -506,13 +580,17 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou } public View getSenderView() { - return mSenderName; + return mSenderView; } public View getAvatar() { return mAvatarView; } + public Icon getAvatarIcon() { + return mAvatarIcon; + } + public MessagingLinearLayout getMessageContainer() { return mMessageContainer; } @@ -529,8 +607,8 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou return mSender; } - public void setTransformingImages(boolean transformingImages) { - mTransformingImages = transformingImages; + public void setClippingDisabled(boolean disabled) { + mClippingDisabled = disabled; } public void setDisplayImagesAtEnd(boolean atEnd) { @@ -543,4 +621,27 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou 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(); + updateSenderVisibility(); + } + } + + public boolean isSingleLine() { + return mSingleLine; + } } diff --git a/core/java/com/android/internal/widget/MessagingImageMessage.java b/core/java/com/android/internal/widget/MessagingImageMessage.java index 64650a7ebc2f..c243f3b583e5 100644 --- a/core/java/com/android/internal/widget/MessagingImageMessage.java +++ b/core/java/com/android/internal/widget/MessagingImageMessage.java @@ -120,7 +120,7 @@ public class MessagingImageMessage extends ImageView implements MessagingMessage 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 f6089589a994..503f3f161c96 100644 --- a/core/java/com/android/internal/widget/MessagingLayout.java +++ b/core/java/com/android/internal/widget/MessagingLayout.java @@ -58,7 +58,8 @@ import java.util.regex.Pattern; * 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,29 @@ public class MessagingLayout extends FrameLayout implements ImageMessageConsumer mNameReplacement = nameReplacement; } + /** + * Set this layout to show the collapsed representation. + * + * @param isCollapsed is it collapsed + */ + @RemotableViewMethod + public void setIsCollapsed(boolean isCollapsed) { + mDisplayImagesAtEnd = isCollapsed; + } + + @RemotableViewMethod + public void setLargeIcon(Icon largeIcon) { + // Unused + } + + /** + * Sets the conversation title of this conversation. + * + * @param conversationTitle the conversation title + */ @RemotableViewMethod - public void setDisplayImagesAtEnd(boolean atEnd) { - mDisplayImagesAtEnd = atEnd; + public void setConversationTitle(CharSequence conversationTitle) { + // Unused } @RemotableViewMethod @@ -371,6 +392,15 @@ public class MessagingLayout extends FrameLayout implements ImageMessageConsumer mSenderTextColor = color; } + + /** + * @param color the color of the notification background + */ + @RemotableViewMethod + public void setNotificationBackgroundColor(int color) { + // Nothing to do with this + } + @RemotableViewMethod public void setMessageTextColor(int color) { mMessageTextColor = color; diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java index 0c8613b460f6..ac04862d9a7d 100644 --- a/core/java/com/android/internal/widget/MessagingLinearLayout.java +++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java @@ -43,7 +43,7 @@ public class MessagingLinearLayout extends ViewGroup { private int mMaxDisplayedLines = Integer.MAX_VALUE; - private MessagingLayout mMessagingLayout; + private IMessagingLayout mMessagingLayout; public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -84,6 +84,11 @@ public class MessagingLinearLayout extends ViewGroup { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.hide = true; + if (child instanceof MessagingChild) { + MessagingChild messagingChild = (MessagingChild) child; + // Whenever we encounter the message first, it's always first in the layout + messagingChild.setIsFirstInLayout(true); + } } totalHeight = mPaddingTop + mPaddingBottom; @@ -91,6 +96,11 @@ public class MessagingLinearLayout extends ViewGroup { int linesRemaining = mMaxDisplayedLines; // Starting from the bottom: we measure every view as if it were the only one. If it still // fits, we take it, otherwise we stop there. + MessagingChild previousChild = null; + View previousView = null; + int previousChildHeight = 0; + int previousTotalHeight = 0; + int previousLinesConsumed = 0; for (int i = count - 1; i >= 0 && totalHeight < targetHeight; i--) { if (getChildAt(i).getVisibility() == GONE) { continue; @@ -99,7 +109,16 @@ public class MessagingLinearLayout extends ViewGroup { LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); MessagingChild messagingChild = null; int spacing = mSpacing; + int previousChildIncrease = 0; if (child instanceof MessagingChild) { + // We need to remeasure the previous child again if it's not the first anymore + if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) { + previousChild.setIsFirstInLayout(false); + measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec, + previousTotalHeight - previousChildHeight); + previousChildIncrease = previousView.getMeasuredHeight() - previousChildHeight; + linesRemaining -= previousChild.getConsumedLines() - previousLinesConsumed; + } messagingChild = (MessagingChild) child; messagingChild.setMaxDisplayedLines(linesRemaining); spacing += messagingChild.getExtraSpacing(); @@ -110,18 +129,26 @@ public class MessagingLinearLayout extends ViewGroup { final int childHeight = child.getMeasuredHeight(); int newHeight = Math.max(totalHeight, totalHeight + childHeight + lp.topMargin + - lp.bottomMargin + spacing); + lp.bottomMargin + spacing + previousChildIncrease); int measureType = MessagingChild.MEASURED_NORMAL; if (messagingChild != null) { measureType = messagingChild.getMeasuredType(); - linesRemaining -= messagingChild.getConsumedLines(); } // We never measure the first item as too small, we want to at least show something. boolean isTooSmall = measureType == MessagingChild.MEASURED_TOO_SMALL && !first; boolean isShortened = measureType == MessagingChild.MEASURED_SHORTENED || measureType == MessagingChild.MEASURED_TOO_SMALL && first; - if (newHeight <= targetHeight && !isTooSmall) { + boolean showView = newHeight <= targetHeight && !isTooSmall; + if (showView) { + if (messagingChild != null) { + previousLinesConsumed = messagingChild.getConsumedLines(); + linesRemaining -= previousLinesConsumed; + previousChild = messagingChild; + previousView = child; + previousChildHeight = childHeight; + previousTotalHeight = totalHeight; + } totalHeight = newHeight; measuredWidth = Math.max(measuredWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin @@ -131,6 +158,16 @@ public class MessagingLinearLayout extends ViewGroup { break; } } else { + // We now became too short, let's make sure to reset any previous views to be first + // and remeasure it. + if (previousChild != null && previousChild.hasDifferentHeightWhenFirst()) { + previousChild.setIsFirstInLayout(true); + // We need to remeasure the previous child again since it became first + measureChildWithMargins(previousView, widthMeasureSpec, 0, heightMeasureSpec, + previousTotalHeight - previousChildHeight); + // The totalHeight is already correct here since we only set it during the + // first pass + } break; } first = false; @@ -255,11 +292,11 @@ public class MessagingLinearLayout extends ViewGroup { mMaxDisplayedLines = numberLines; } - public void setMessagingLayout(MessagingLayout layout) { + public void setMessagingLayout(IMessagingLayout layout) { mMessagingLayout = layout; } - public MessagingLayout getMessagingLayout() { + public IMessagingLayout getMessagingLayout() { return mMessagingLayout; } @@ -273,6 +310,20 @@ public class MessagingLinearLayout extends ViewGroup { void setMaxDisplayedLines(int lines); void hideAnimated(); boolean isHidingAnimated(); + + /** + * Set that this view is first in layout. Relevant and only set if + * {@link #hasDifferentHeightWhenFirst()}. + * @param first is this first? + */ + default void setIsFirstInLayout(boolean first) {} + + /** + * @return if this layout has different height it is first in the layout + */ + default boolean hasDifferentHeightWhenFirst() { + return false; + } default int getExtraSpacing() { return 0; } diff --git a/core/java/com/android/internal/widget/MessagingMessage.java b/core/java/com/android/internal/widget/MessagingMessage.java index c32d3705bba7..8c8437951402 100644 --- a/core/java/com/android/internal/widget/MessagingMessage.java +++ b/core/java/com/android/internal/widget/MessagingMessage.java @@ -32,7 +32,7 @@ public interface MessagingMessage extends MessagingLinearLayout.MessagingChild { **/ 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 4081a866f993..d778c5967046 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.text.Layout; 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 @@ public class MessagingTextMessage extends ImageFloatingTextView implements Messa 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 39f82a5fb349..a49980696e6b 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.annotation.Nullable; 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 @@ import android.widget.RemoteViews; @RemoteViews.RemoteView public class NotificationExpandButton extends ImageView { + private int mOriginalNotificationColor; + public NotificationExpandButton(Context context) { super(context); } @@ -56,6 +58,15 @@ public class NotificationExpandButton extends ImageView { 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 e352b45ef413..7b154a54fc85 100644 --- a/core/java/com/android/internal/widget/RemeasuringLinearLayout.java +++ b/core/java/com/android/internal/widget/RemeasuringLinearLayout.java @@ -23,6 +23,8 @@ import android.view.View; import android.widget.LinearLayout; import android.widget.RemoteViews; +import java.util.ArrayList; + /** * A LinearLayout that sets it's height again after the last measure pass. This is needed for * MessagingLayouts where groups need to be able to snap it's height to. @@ -30,6 +32,8 @@ import android.widget.RemoteViews; @RemoteViews.RemoteView public class RemeasuringLinearLayout extends LinearLayout { + private ArrayList<View> mMatchParentViews = new ArrayList<>(); + public RemeasuringLinearLayout(Context context) { super(context); } @@ -53,6 +57,8 @@ public class RemeasuringLinearLayout extends LinearLayout { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = getChildCount(); int height = 0; + boolean isVertical = getOrientation() == LinearLayout.VERTICAL; + boolean isWrapContent = getLayoutParams().height == LayoutParams.WRAP_CONTENT; for (int i = 0; i < count; ++i) { final View child = getChildAt(i); if (child == null || child.getVisibility() == View.GONE) { @@ -60,9 +66,25 @@ public class RemeasuringLinearLayout extends LinearLayout { } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - height = Math.max(height, height + child.getMeasuredHeight() + lp.topMargin + - lp.bottomMargin); + if (!isWrapContent || lp.height != LayoutParams.MATCH_PARENT || isVertical) { + int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + height = Math.max(height, isVertical ? height + childHeight : childHeight); + } else { + // We have match parent children in a wrap content view, let's measure the + // view properly + mMatchParentViews.add(child); + } + } + if (mMatchParentViews.size() > 0) { + int exactHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + for (View child : mMatchParentViews) { + child.measure(getChildMeasureSpec( + widthMeasureSpec, getPaddingStart() + getPaddingEnd(), + child.getLayoutParams().width), + exactHeightSpec); + } } + mMatchParentViews.clear(); 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 000000000000..0dd0dcda40fb --- /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 124e99e3a4bb..ca4f0ed27a0b 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 @@ Copyright (C) 2015 The Android Open Source Project 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 847e3269398d..a080ce43cfec 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 @@ Copyright (C) 2015 The Android Open Source Project 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/conversation_face_pile_layout.xml b/core/res/res/layout/conversation_face_pile_layout.xml new file mode 100644 index 000000000000..1db38702f926 --- /dev/null +++ b/core/res/res/layout/conversation_face_pile_layout.xml @@ -0,0 +1,45 @@ +<?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 + --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/conversation_face_pile" + android:layout_width="@dimen/conversation_avatar_size" + android:layout_height="@dimen/conversation_avatar_size" + android:forceHasOverlappingRendering="false" + > + <ImageView + android:id="@+id/conversation_face_pile_top" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="centerCrop" + android:layout_gravity="end|top" + /> + <FrameLayout + android:id="@+id/conversation_face_pile_bottom_background" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_gravity="start|bottom" + android:background="@drawable/conversation_badge_background"> + <ImageView + android:id="@+id/conversation_face_pile_bottom" + android:layout_width="36dp" + android:layout_height="36dp" + android:scaleType="centerCrop" + android:layout_gravity="center" + /> + </FrameLayout> +</FrameLayout> diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index f5fa1b6a795a..6f36aae8a1d4 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 000000000000..dc52e979a310 --- /dev/null +++ b/core/res/res/layout/notification_template_material_conversation.xml @@ -0,0 +1,205 @@ +<?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" + /> + + <ViewStub + android:layout="@layout/conversation_face_pile_layout" + android:layout_width="@dimen/conversation_avatar_size" + android:layout_height="@dimen/conversation_avatar_size" + android:id="@+id/conversation_face_pile" + /> + + <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--> + <com.android.internal.widget.RemeasuringLinearLayout + android:id="@+id/expand_button_and_content_container" + 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:id="@+id/conversation_header" + 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/conversation_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/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 /> --> + + <!-- This is where the expand button will be placed when collapsed--> + </com.android.internal.widget.RemeasuringLinearLayout> + + <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> + + <!--This is dynamically placed between here and at the end of the layout--> + <FrameLayout + android:id="@+id/expand_button_container" + android:layout_width="wrap_content" + android:layout_height="@dimen/conversation_expand_button_expanded_size" + android:layout_gravity="end|top" + android:paddingStart="16dp" + android:paddingEnd="@dimen/notification_content_margin_end"> + <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> +</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 483b479538a1..15146c073e46 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 2faa0c9c8036..e3fe982c226d 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -682,7 +682,25 @@ <!-- The size of the right icon image when on low ram --> <dimen name="notification_right_icon_size_low_ram">@dimen/notification_right_icon_size</dimen> - <dimen name="messaging_avatar_size">52dp</dimen> + <dimen name="messaging_avatar_size">@dimen/notification_right_icon_size</dimen> + <dimen name="conversation_avatar_size">52dp</dimen> + <!-- Start of the content in the conversation template --> + <dimen name="conversation_content_start">80dp</dimen> + <!-- Size of the expand button when expanded --> + <dimen name="conversation_expand_button_expanded_size">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/strings.xml b/core/res/res/values/strings.xml index 789628d63d1b..f101f590cab1 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5440,6 +5440,15 @@ <string name="as_app_forced_to_restricted_bucket"> <xliff:g id="package_name" example="com.android.example">%1$s</xliff:g> has been put into the RESTRICTED bucket</string> + <!-- The way a conversation name is displayed when single line. The text will be displayed to the end of this text with some spacing --> + <string name="conversation_single_line_name_display"><xliff:g id="sender_name" example="Sara">%1$s</xliff:g>:</string> + + <!-- Conversation Title fallback if the there is no name provided in a 1:1 conversation [CHAR LIMIT=40]--> + <string name="conversation_title_fallback_one_to_one">Conversation</string> + + <!-- Conversation Title fallback if the there is no name provided in a group chat conversation [CHAR LIMIT=40]--> + <string name="conversation_title_fallback_group_chat">Group Conversation</string> + <!-- ResolverActivity - profile tabs --> <!-- Label of a tab on a screen. A user can tap this tap to switch to the 'Personal' view (that shows their personal content) if they have a work profile on their device. [CHAR LIMIT=NONE] --> <string name="resolver_personal_tab">Personal</string> diff --git a/core/res/res/values/styles_device_defaults.xml b/core/res/res/values/styles_device_defaults.xml index 966f495c96e5..64768cf4c730 100644 --- a/core/res/res/values/styles_device_defaults.xml +++ b/core/res/res/values/styles_device_defaults.xml @@ -146,7 +146,7 @@ easier. <item name="textAppearance">@style/TextAppearance.DeviceDefault.Notification</item> </style> <style name="Widget.DeviceDefault.Notification.MessagingName" parent="Widget.Material.Notification.MessagingName"> - <item name="textAppearance">@style/TextAppearance.DeviceDefault.Notification.MessagingName</item> + <item name="textAppearance">@style/TextAppearance.DeviceDefault.Notification.Title</item> </style> <style name="Widget.DeviceDefault.PreferenceFrameLayout" parent="Widget.Material.PreferenceFrameLayout"/> <style name="Widget.DeviceDefault.ProgressBar.Inverse" parent="Widget.Material.ProgressBar.Inverse"/> @@ -290,9 +290,6 @@ easier. <style name="TextAppearance.DeviceDefault.Notification.Title" parent="TextAppearance.Material.Notification.Title"> <item name="fontFamily">@string/config_headlineFontFamilyMedium</item> </style> - <style name="TextAppearance.DeviceDefault.Notification.MessagingName" parent="TextAppearance.DeviceDefault.Notification.Title"> - <item name="textSize">16sp</item> - </style> <style name="TextAppearance.DeviceDefault.Notification.Reply" parent="TextAppearance.Material.Notification.Reply"> <item name="fontFamily">@string/config_bodyFontFamily</item> </style> diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml index 63ac0e6bfc3e..2415837cf826 100644 --- a/core/res/res/values/styles_material.xml +++ b/core/res/res/values/styles_material.xml @@ -504,7 +504,7 @@ please see styles_device_defaults.xml. <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 a9008d78e19a..49a0f17aad1a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3876,6 +3876,30 @@ <java-symbol type="array" name="config_defaultImperceptibleKillingExemptionPkgs" /> <java-symbol type="array" name="config_defaultImperceptibleKillingExemptionProcStates" /> + <java-symbol type="string" name="conversation_single_line_name_display" /> + <java-symbol type="string" name="conversation_title_fallback_one_to_one" /> + <java-symbol type="string" name="conversation_title_fallback_group_chat" /> + <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="id" name="expand_button_and_content_container" /> + <java-symbol type="id" name="conversation_header" /> + <java-symbol type="id" name="conversation_face_pile_bottom_background" /> + <java-symbol type="id" name="conversation_face_pile_bottom" /> + <java-symbol type="id" name="conversation_face_pile_top" /> + <java-symbol type="id" name="conversation_face_pile" /> + <java-symbol type="id" name="conversation_text" /> + <java-symbol type="dimen" name="conversation_expand_button_top_margin_expanded" /> + <java-symbol type="dimen" name="conversation_expand_button_expanded_size" /> + <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" /> + <java-symbol type="layout" name="conversation_face_pile_layout" /> + <!-- Intent resolver and share sheet --> <java-symbol type="color" name="resolver_tabs_active_color" /> <java-symbol type="color" name="resolver_tabs_inactive_color" /> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 10b47e69184c..e45cbecd3aa1 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -124,9 +124,6 @@ <!-- Increased height of a collapsed media notification in the status bar --> <dimen name="notification_min_height_media">160dp</dimen> - <!-- Increased height of a collapsed messaging notification in the status bar --> - <dimen name="notification_min_height_messaging">118dp</dimen> - <!-- Height of a small notification in the status bar which was used before android N --> <dimen name="notification_min_height_legacy">64dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java index 0bfcdbdb1118..4759d56099f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationHeaderUtil.java @@ -23,6 +23,7 @@ import android.graphics.drawable.Icon; import android.text.TextUtils; import android.view.NotificationHeaderView; import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -182,24 +183,24 @@ public class NotificationHeaderUtil { private void sanitizeChild(View child) { if (child != null) { - NotificationHeaderView header = (NotificationHeaderView) child.findViewById( + ViewGroup header = child.findViewById( com.android.internal.R.id.notification_header); sanitizeHeader(header); } } - private void sanitizeHeader(NotificationHeaderView rowHeader) { + private void sanitizeHeader(ViewGroup rowHeader) { if (rowHeader == null) { return; } final int childCount = rowHeader.getChildCount(); View time = rowHeader.findViewById(com.android.internal.R.id.time); boolean hasVisibleText = false; - for (int i = 1; i < childCount - 1 ; i++) { + for (int i = 0; i < childCount; i++) { View child = rowHeader.getChildAt(i); if (child instanceof TextView && child.getVisibility() != View.GONE - && !mDividers.contains(Integer.valueOf(child.getId())) + && !mDividers.contains(child.getId()) && child != time) { hasVisibleText = true; break; @@ -212,14 +213,14 @@ public class NotificationHeaderUtil { time.setVisibility(timeVisibility); View left = null; View right; - for (int i = 1; i < childCount - 1 ; i++) { + for (int i = 0; i < childCount; i++) { View child = rowHeader.getChildAt(i); - if (mDividers.contains(Integer.valueOf(child.getId()))) { + if (mDividers.contains(child.getId())) { boolean visible = false; // Lets find the item to the right - for (i++; i < childCount - 1; i++) { + for (i++; i < childCount; i++) { right = rowHeader.getChildAt(i); - if (mDividers.contains(Integer.valueOf(right.getId()))) { + if (mDividers.contains(right.getId())) { // A divider was found, this needs to be hidden i--; break; @@ -276,14 +277,18 @@ public class NotificationHeaderUtil { if (!mApply) { return; } - NotificationHeaderView header = row.getContractedNotificationHeader(); - if (header == null) { - // No header found. We still consider this to be the same to avoid weird flickering + View contractedChild = row.getPrivateLayout().getContractedChild(); + if (contractedChild == null) { + return; + } + View ownView = contractedChild.findViewById(mId); + if (ownView == null) { + // No view found. We still consider this to be the same to avoid weird flickering // when for example showing an undo notification return; } Object childData = mExtractor == null ? null : mExtractor.extractData(row); - mApply = mComparator.compare(mParentView, header.findViewById(mId), + mApply = mComparator.compare(mParentView, ownView, mParentData, childData); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java index 7b5a70eb5430..2a45bc210580 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ViewTransformationHelper.java @@ -41,6 +41,7 @@ public class ViewTransformationHelper implements TransformableView, private static final int TAG_CONTAINS_TRANSFORMED_VIEW = R.id.contains_transformed_view; private ArrayMap<Integer, View> mTransformedViews = new ArrayMap<>(); + private ArraySet<Integer> mKeysTransformingToSimilar = new ArraySet<>(); private ArrayMap<Integer, CustomTransformation> mCustomTransformations = new ArrayMap<>(); private ValueAnimator mViewTransformationAnimation; @@ -48,8 +49,22 @@ public class ViewTransformationHelper implements TransformableView, mTransformedViews.put(key, transformedView); } + /** + * Add a view that transforms to a similar sibling, meaning that we should consider any mapping + * found treated as the same viewType. This is useful for imageViews, where it's hard to compare + * if the source images are the same when they are bitmap based. + * + * @param key The key how this is added + * @param transformedView the view that is added + */ + public void addViewTransformingToSimilar(int key, View transformedView) { + addTransformedView(key, transformedView); + mKeysTransformingToSimilar.add(key); + } + public void reset() { mTransformedViews.clear(); + mKeysTransformingToSimilar.clear(); } public void setCustomTransformation(CustomTransformation transformation, int viewType) { @@ -60,7 +75,11 @@ public class ViewTransformationHelper implements TransformableView, public TransformState getCurrentState(int fadingView) { View view = mTransformedViews.get(fadingView); if (view != null && view.getVisibility() != View.GONE) { - return TransformState.createFrom(view, this); + TransformState transformState = TransformState.createFrom(view, this); + if (mKeysTransformingToSimilar.contains(fadingView)) { + transformState.setIsSameAsAnyView(true); + } + return transformState; } return null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java index b732966b32db..9383f537db45 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java @@ -21,9 +21,9 @@ import android.util.Pools; import android.view.View; import android.view.ViewGroup; +import com.android.internal.widget.IMessagingLayout; import com.android.internal.widget.MessagingGroup; import com.android.internal.widget.MessagingImageMessage; -import com.android.internal.widget.MessagingLayout; import com.android.internal.widget.MessagingLinearLayout; import com.android.internal.widget.MessagingMessage; import com.android.internal.widget.MessagingPropertyAnimator; @@ -41,7 +41,7 @@ public class MessagingLayoutTransformState extends TransformState { private static Pools.SimplePool<MessagingLayoutTransformState> sInstancePool = new Pools.SimplePool<>(40); private MessagingLinearLayout mMessageContainer; - private MessagingLayout mMessagingLayout; + private IMessagingLayout mMessagingLayout; private HashMap<MessagingGroup, MessagingGroup> mGroupMap = new HashMap<>(); private float mRelativeTranslationOffset; @@ -266,8 +266,9 @@ public class MessagingLayoutTransformState extends TransformState { transformView(transformationAmount, to, child, otherChild, false, /* sameAsAny */ useLinearTransformation); boolean otherIsIsolated = otherGroup.getIsolatedMessage() == otherChild; - if (transformationAmount == 0.0f && otherIsIsolated) { - ownGroup.setTransformingImages(true); + if (transformationAmount == 0.0f + && (otherIsIsolated || otherGroup.isSingleLine())) { + ownGroup.setClippingDisabled(true); } if (otherChild == null) { child.setTranslationY(previousTranslation); @@ -291,11 +292,20 @@ public class MessagingLayoutTransformState extends TransformState { if (useLinearTransformation) { ownState.setDefaultInterpolator(Interpolators.LINEAR); } - ownState.setIsSameAsAnyView(sameAsAny); + ownState.setIsSameAsAnyView(sameAsAny && !isGone(otherView)); if (to) { if (otherView != null) { TransformState otherState = TransformState.createFrom(otherView, mTransformInfo); - ownState.transformViewTo(otherState, transformationAmount); + if (!isGone(otherView)) { + ownState.transformViewTo(otherState, transformationAmount); + } else { + if (!isGone(ownView)) { + ownState.disappear(transformationAmount, null); + } + // We still want to transform vertically if the view is gone, + // since avatars serve as anchors for the rest of the layout transition + ownState.transformViewVerticalTo(otherState, transformationAmount); + } otherState.recycle(); } else { ownState.disappear(transformationAmount, null); @@ -303,7 +313,16 @@ public class MessagingLayoutTransformState extends TransformState { } else { if (otherView != null) { TransformState otherState = TransformState.createFrom(otherView, mTransformInfo); - ownState.transformViewFrom(otherState, transformationAmount); + if (!isGone(otherView)) { + ownState.transformViewFrom(otherState, transformationAmount); + } else { + if (!isGone(ownView)) { + ownState.appear(transformationAmount, null); + } + // We still want to transform vertically if the view is gone, + // since avatars serve as anchors for the rest of the layout transition + ownState.transformViewVerticalFrom(otherState, transformationAmount); + } otherState.recycle(); } else { ownState.appear(transformationAmount, null); @@ -337,6 +356,9 @@ public class MessagingLayoutTransformState extends TransformState { } private boolean isGone(View view) { + if (view == null) { + return true; + } if (view.getVisibility() == View.GONE) { return true; } @@ -408,7 +430,7 @@ public class MessagingLayoutTransformState extends TransformState { ownGroup.getMessageContainer().setTranslationY(0); ownGroup.getSenderView().setTranslationY(0); } - ownGroup.setTransformingImages(false); + ownGroup.setClippingDisabled(false); ownGroup.updateClipRect(); } } 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 9a4e789a2e03..f61fe9830939 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 @@ -31,7 +31,6 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.Notification; import android.app.NotificationChannel; import android.content.Context; import android.content.pm.PackageInfo; @@ -151,7 +150,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mNotificationMinHeight; private int mNotificationMinHeightLarge; private int mNotificationMinHeightMedia; - private int mNotificationMinHeightMessaging; private int mNotificationMaxHeight; private int mIncreasedPaddingBetweenElements; private int mNotificationLaunchHeight; @@ -640,16 +638,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView && expandedView.findViewById(com.android.internal.R.id.media_actions) != null; boolean showCompactMediaSeekbar = mMediaManager.getShowCompactMediaSeekbar(); - Class<? extends Notification.Style> style = - mEntry.getSbn().getNotification().getNotificationStyle(); - boolean isMessagingLayout = Notification.MessagingStyle.class.equals(style); - if (customView && beforeP && !mIsSummaryWithChildren) { minHeight = beforeN ? mNotificationMinHeightBeforeN : mNotificationMinHeightBeforeP; } else if (isMediaLayout && showCompactMediaSeekbar) { minHeight = mNotificationMinHeightMedia; - } else if (isMessagingLayout) { - minHeight = mNotificationMinHeightMessaging; } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) { minHeight = mNotificationMinHeightLarge; } else { @@ -1057,19 +1049,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView return getShowingLayout().getVisibleNotificationHeader(); } - - /** - * @return the contracted notification header. This can be different from - * {@link #getNotificationHeader()} and also {@link #getVisibleNotificationHeader()} and only - * returns the contracted version. - */ - public NotificationHeaderView getContractedNotificationHeader() { - if (mIsSummaryWithChildren) { - return mChildrenContainer.getHeaderView(); - } - return mPrivateLayout.getContractedNotificationHeader(); - } - public void setLongPressListener(LongPressListener longPressListener) { mLongPressListener = longPressListener; } @@ -1654,8 +1633,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView R.dimen.notification_min_height_increased); mNotificationMinHeightMedia = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_min_height_media); - mNotificationMinHeightMessaging = NotificationUtils.getFontScaledHeight(mContext, - R.dimen.notification_min_height_messaging); mNotificationMaxHeight = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_height); mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 27fd1b2c5aed..8b8a9012cbdc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1492,13 +1492,6 @@ public class NotificationContentView extends FrameLayout { } } - public NotificationHeaderView getContractedNotificationHeader() { - if (mContractedChild != null) { - return mContractedWrapper.getNotificationHeader(); - } - return null; - } - public NotificationHeaderView getVisibleNotificationHeader() { NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType); return wrapper == null ? null : wrapper.getNotificationHeader(); 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 new file mode 100644 index 000000000000..1e2571b5c801 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -0,0 +1,100 @@ +/* + * 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.systemui.statusbar.notification.row.wrapper + +import android.content.Context +import android.view.View + +import com.android.internal.widget.ConversationLayout +import com.android.internal.widget.MessagingLinearLayout +import com.android.systemui.R +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow + +/** + * Wraps a notification containing a converation template + */ +class NotificationConversationTemplateViewWrapper constructor( + ctx: Context, + view: View, + row: ExpandableNotificationRow +) + : NotificationTemplateViewWrapper(ctx, view, row) { + + private val minHeightWithActions: Int + private val conversationLayout: ConversationLayout + private var conversationIcon: View? = null + private var conversationBadge: View? = null + private var expandButton: View? = null + private var messagingLinearLayout: MessagingLinearLayout? = null + + init { + conversationLayout = view as ConversationLayout + minHeightWithActions = NotificationUtils.getFontScaledHeight(ctx, + R.dimen.notification_messaging_actions_min_height) + } + + private fun resolveViews() { + messagingLinearLayout = conversationLayout.messagingLinearLayout + conversationIcon = conversationLayout.requireViewById( + com.android.internal.R.id.conversation_icon) + conversationBadge = conversationLayout.requireViewById( + com.android.internal.R.id.conversation_icon_badge) + expandButton = conversationLayout.requireViewById( + com.android.internal.R.id.expand_button) + } + + override fun onContentUpdated(row: ExpandableNotificationRow) { + // Reinspect the notification. Before the super call, because the super call also updates + // the transformation types and we need to have our values set by then. + resolveViews() + super.onContentUpdated(row) + } + + override fun updateTransformedTypes() { + // This also clears the existing types + super.updateTransformedTypes() + messagingLinearLayout?.let { + mTransformationHelper.addTransformedView(it.id, it) + } + conversationIcon?.let { + mTransformationHelper.addViewTransformingToSimilar(it.id, it) + } + conversationBadge?.let { + mTransformationHelper.addViewTransformingToSimilar(it.id, it) + } + expandButton?.let { + mTransformationHelper.addViewTransformingToSimilar(it.id, it) + } + } + + override fun setRemoteInputVisible(visible: Boolean) { + conversationLayout.showHistoricMessages(visible) + } + + override fun updateExpandability(expandable: Boolean, onClickListener: View.OnClickListener?) { + conversationLayout.updateExpandability(expandable, onClickListener) + } + + override fun getMinLayoutHeight(): Int { + if (mActionsContainer != null && mActionsContainer.visibility != View.GONE) { + return minHeightWithActions + } else { + return super.getMinLayoutHeight() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 5e52c0a5a66f..1d061989a84c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -53,12 +53,13 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { protected final ViewTransformationHelper mTransformationHelper; protected int mColor; - private ImageView mIcon; + private ImageView mIcon; private NotificationExpandButton mExpandButton; protected NotificationHeaderView mNotificationHeader; private TextView mHeaderText; private ImageView mWorkProfileImage; + private boolean mIsLowPriority; private boolean mTransformLowPriorityTitle; private boolean mShowExpandButtonAtEnd; @@ -105,12 +106,16 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header); - mNotificationHeader.setShowExpandButtonAtEnd(mShowExpandButtonAtEnd); - mColor = mNotificationHeader.getOriginalIconColor(); + if (mNotificationHeader != null) { + mNotificationHeader.setShowExpandButtonAtEnd(mShowExpandButtonAtEnd); + mColor = mNotificationHeader.getOriginalIconColor(); + } } private void addAppOpsOnClickListener(ExpandableNotificationRow row) { - mNotificationHeader.setAppOpsOnClickListener(row.getAppOpsOnClickListener()); + if (mNotificationHeader != null) { + mNotificationHeader.setAppOpsOnClickListener(row.getAppOpsOnClickListener()); + } } @Override @@ -127,9 +132,11 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { updateCropToPaddingForImageViews(); Notification notification = row.getEntry().getSbn().getNotification(); mIcon.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon()); - // The work profile image is always the same lets just set the icon tag for it not to - // animate - mWorkProfileImage.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon()); + if (mWorkProfileImage != null) { + // The work profile image is always the same lets just set the icon tag for it not to + // animate + mWorkProfileImage.setTag(ImageTransformState.ICON_TAG, notification.getSmallIcon()); + } // We need to reset all views that are no longer transforming in case a view was previously // transformed, but now we decided to transform its container instead. @@ -174,8 +181,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { protected void updateTransformedTypes() { mTransformationHelper.reset(); - mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon); - if (mIsLowPriority) { + mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, + mIcon); + if (mIsLowPriority && mHeaderText != null) { mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, mHeaderText); } @@ -184,7 +192,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { @Override public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) { mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE); - mNotificationHeader.setOnClickListener(expandable ? onClickListener : null); + if (mNotificationHeader != null) { + mNotificationHeader.setOnClickListener(expandable ? onClickListener : null); + } } @Override 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 0a1a2fe3ee54..d41f5af6c524 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 @@ -353,8 +353,12 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp @Override public void setHeaderVisibleAmount(float headerVisibleAmount) { super.setHeaderVisibleAmount(headerVisibleAmount); - mNotificationHeader.setAlpha(headerVisibleAmount); - mHeaderTranslation = (1.0f - headerVisibleAmount) * mFullHeaderTranslation; + float headerTranslation = 0f; + if (mNotificationHeader != null) { + mNotificationHeader.setAlpha(headerVisibleAmount); + headerTranslation = (1.0f - headerVisibleAmount) * mFullHeaderTranslation; + } + mHeaderTranslation = headerTranslation; mView.setTranslationY(mHeaderTranslation); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index c2eff8a6a776..c834e4b376ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -35,6 +35,7 @@ import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ContrastColorUtil; +import com.android.internal.widget.ConversationLayout; import com.android.systemui.statusbar.CrossFadeHelper; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.notification.TransformState; @@ -61,6 +62,9 @@ public abstract class NotificationViewWrapper implements TransformableView { return new NotificationMediaTemplateViewWrapper(ctx, v, row); } else if ("messaging".equals(v.getTag())) { return new NotificationMessagingTemplateViewWrapper(ctx, v, row); + } else if ("conversation".equals(v.getTag())) { + return new NotificationConversationTemplateViewWrapper(ctx, (ConversationLayout) v, + row); } Class<? extends Notification.Style> style = row.getEntry().getSbn().getNotification().getNotificationStyle(); |