• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.internal.widget;
18 
19 import android.annotation.AttrRes;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.StyleRes;
24 import android.app.Flags;
25 import android.app.Person;
26 import android.content.Context;
27 import android.content.res.ColorStateList;
28 import android.content.res.Resources;
29 import android.graphics.Color;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Icon;
33 import android.text.TextUtils;
34 import android.util.AttributeSet;
35 import android.util.DisplayMetrics;
36 import android.util.TypedValue;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.view.ViewParent;
41 import android.view.ViewTreeObserver;
42 import android.widget.ImageView;
43 import android.widget.LinearLayout;
44 import android.widget.ProgressBar;
45 import android.widget.RemoteViews;
46 import android.widget.TextView;
47 
48 import com.android.internal.R;
49 
50 import java.lang.annotation.Retention;
51 import java.lang.annotation.RetentionPolicy;
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 /**
56  * A message of a {@link MessagingLayout}.
57  */
58 @RemoteViews.RemoteView
59 public class MessagingGroup extends NotificationOptimizedLinearLayout implements
60         MessagingLinearLayout.MessagingChild {
61 
62     private static final MessagingPool<MessagingGroup> sInstancePool =
63             new MessagingPool<>(10);
64 
65     /**
66      * Images are displayed inline.
67      */
68     public static final int IMAGE_DISPLAY_LOCATION_INLINE = 0;
69 
70     /**
71      * Images are displayed at the end of the group.
72      */
73     public static final int IMAGE_DISPLAY_LOCATION_AT_END = 1;
74 
75     /**
76      *     Images are displayed externally.
77      */
78     public static final int IMAGE_DISPLAY_LOCATION_EXTERNAL = 2;
79 
80 
81     private MessagingLinearLayout mMessageContainer;
82     ImageFloatingTextView mSenderView;
83     private ImageView mAvatarView;
84     private String mAvatarSymbol = "";
85     private int mLayoutColor;
86     private CharSequence mAvatarName = "";
87     private Icon mAvatarIcon;
88     private int mTextColor;
89     private int mSendingTextColor;
90     private List<MessagingMessage> mMessages;
91     private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>();
92     private boolean mFirstLayout;
93     private boolean mIsHidingAnimated;
94     private boolean mNeedsGeneratedAvatar;
95     private Person mSender;
96     private @ImageDisplayLocation int mImageDisplayLocation;
97     private ViewGroup mImageContainer;
98     private MessagingImageMessage mIsolatedMessage;
99     private boolean mClippingDisabled;
100     private Point mDisplaySize = new Point();
101     private ProgressBar mSendingSpinner;
102     private View mSendingSpinnerContainer;
103     private boolean mShowingAvatar = true;
104     private CharSequence mSenderName;
105     private boolean mSingleLine = false;
106     private boolean mIsCollapsed = false;
107     private LinearLayout mContentContainer;
108     private int mRequestedMaxDisplayedLines = Integer.MAX_VALUE;
109     private int mSenderTextPaddingSingleLine;
110     private boolean mIsFirstGroupInLayout = true;
111     private boolean mCanHideSenderIfFirst;
112     private boolean mIsInConversation = true;
113     private ViewGroup mMessagingIconContainer;
114     private int mConversationContentStart;
115     private int mNonConversationContentStart;
116     private int mNonConversationPaddingStart;
117     private int mConversationAvatarSize;
118     private int mNonConversationAvatarSize;
119     private int mNotificationTextMarginTop;
120 
MessagingGroup(@onNull Context context)121     public MessagingGroup(@NonNull Context context) {
122         super(context);
123     }
124 
MessagingGroup(@onNull Context context, @Nullable AttributeSet attrs)125     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
126         super(context, attrs);
127     }
128 
MessagingGroup(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)129     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
130             @AttrRes int defStyleAttr) {
131         super(context, attrs, defStyleAttr);
132     }
133 
MessagingGroup(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)134     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
135             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
136         super(context, attrs, defStyleAttr, defStyleRes);
137     }
138 
139     @Override
onFinishInflate()140     protected void onFinishInflate() {
141         super.onFinishInflate();
142         mMessageContainer = findViewById(R.id.group_message_container);
143         mSenderView = findViewById(R.id.message_name);
144         mAvatarView = findViewById(R.id.message_icon);
145         mImageContainer = findViewById(R.id.messaging_group_icon_container);
146         mSendingSpinner = findViewById(R.id.messaging_group_sending_progress);
147         mMessagingIconContainer = findViewById(R.id.message_icon_container);
148         mContentContainer = findViewById(R.id.messaging_group_content_container);
149         mSendingSpinnerContainer = findViewById(R.id.messaging_group_sending_progress_container);
150         Resources res = getResources();
151         DisplayMetrics displayMetrics = res.getDisplayMetrics();
152         mDisplaySize.x = displayMetrics.widthPixels;
153         mDisplaySize.y = displayMetrics.heightPixels;
154         mSenderTextPaddingSingleLine = res.getDimensionPixelSize(
155                 R.dimen.messaging_group_singleline_sender_padding_end);
156         mConversationContentStart = res.getDimensionPixelSize(R.dimen.conversation_content_start);
157         mNonConversationContentStart = res.getDimensionPixelSize(
158                 R.dimen.notification_content_margin_start);
159         mNonConversationPaddingStart = res.getDimensionPixelSize(
160                 R.dimen.messaging_layout_icon_padding_start);
161         mConversationAvatarSize = res.getDimensionPixelSize(R.dimen.messaging_avatar_size);
162         mNonConversationAvatarSize = res.getDimensionPixelSize(
163                 R.dimen.notification_icon_circle_size);
164         mNotificationTextMarginTop = res.getDimensionPixelSize(
165                 R.dimen.notification_text_margin_top);
166     }
167 
updateClipRect()168     public void updateClipRect() {
169         // We want to clip to the senderName if it's available, otherwise our images will come
170         // from a weird position
171         Rect clipRect;
172         if (mSenderView.getVisibility() != View.GONE && !mClippingDisabled) {
173             int top;
174             if (mSingleLine) {
175                 top = 0;
176             } else {
177                 top = getDistanceFromParent(mSenderView, mContentContainer)
178                         - getDistanceFromParent(mMessageContainer, mContentContainer)
179                         + mSenderView.getHeight();
180             }
181             int size = Math.max(mDisplaySize.x, mDisplaySize.y);
182             clipRect = new Rect(-size, top, size, size);
183         } else {
184             clipRect = null;
185         }
186         mMessageContainer.setClipBounds(clipRect);
187     }
188 
getDistanceFromParent(View searchedView, ViewGroup parent)189     private int getDistanceFromParent(View searchedView, ViewGroup parent) {
190         int position = 0;
191         View view = searchedView;
192         while(view != parent) {
193             position += view.getTop() + view.getTranslationY();
194             view = (View) view.getParent();
195         }
196         return position;
197     }
198 
setSender(Person sender, CharSequence nameOverride)199     public void setSender(Person sender, CharSequence nameOverride) {
200         mSender = sender;
201         if (nameOverride == null) {
202             nameOverride = sender.getName();
203         }
204         if (Flags.cleanUpSpansAndNewLines() && nameOverride != null) {
205             // remove formatting from sender name
206             nameOverride = nameOverride.toString();
207         }
208         mSenderName = nameOverride;
209         if (mSingleLine && !TextUtils.isEmpty(nameOverride)) {
210             nameOverride = mContext.getResources().getString(
211                     R.string.conversation_single_line_name_display, nameOverride);
212         }
213         mSenderView.setText(nameOverride);
214         mNeedsGeneratedAvatar = sender.getIcon() == null;
215         if (!mNeedsGeneratedAvatar) {
216             setAvatar(sender.getIcon());
217         }
218         updateSenderVisibility();
219     }
220 
221     /**
222      * Should the avatar be shown for this view.
223      *
224      * @param showingAvatar should it be shown
225      */
setShowingAvatar(boolean showingAvatar)226     public void setShowingAvatar(boolean showingAvatar) {
227         mAvatarView.setVisibility(showingAvatar ? VISIBLE : GONE);
228         mShowingAvatar = showingAvatar;
229     }
230 
setSending(boolean sending)231     public void setSending(boolean sending) {
232         int visibility = sending ? VISIBLE : GONE;
233         if (mSendingSpinnerContainer.getVisibility() != visibility) {
234             mSendingSpinnerContainer.setVisibility(visibility);
235             updateMessageColor();
236         }
237     }
238 
calculateSendingTextColor()239     private int calculateSendingTextColor() {
240         TypedValue alphaValue = new TypedValue();
241         mContext.getResources().getValue(
242                 R.dimen.notification_secondary_text_disabled_alpha, alphaValue, true);
243         float alpha = alphaValue.getFloat();
244         return Color.valueOf(
245                 Color.red(mTextColor),
246                 Color.green(mTextColor),
247                 Color.blue(mTextColor),
248                 alpha).toArgb();
249     }
250 
setAvatar(Icon icon)251     public void setAvatar(Icon icon) {
252         mAvatarIcon = icon;
253         if (mShowingAvatar || icon == null) {
254             mAvatarView.setImageIcon(icon);
255         }
256         mAvatarSymbol = "";
257         mAvatarName = "";
258     }
259 
createGroup(MessagingLinearLayout layout)260     static MessagingGroup createGroup(MessagingLinearLayout layout) {;
261         MessagingGroup createdGroup = sInstancePool.acquire();
262         if (createdGroup == null) {
263             createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate(
264                     getMessagingGroupLayoutResource(), layout,
265                     false);
266             createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
267         }
268         layout.addView(createdGroup);
269         return createdGroup;
270     }
271 
getMessagingGroupLayoutResource()272     private static int getMessagingGroupLayoutResource() {
273         if (Flags.notificationsRedesignTemplates()) {
274             return R.layout.notification_2025_messaging_group;
275         } else {
276             return R.layout.notification_template_messaging_group;
277         }
278     }
279 
removeMessage(MessagingMessage messagingMessage, ArrayList<MessagingLinearLayout.MessagingChild> toRecycle)280     public void removeMessage(MessagingMessage messagingMessage,
281             ArrayList<MessagingLinearLayout.MessagingChild> toRecycle) {
282         View view = messagingMessage.getView();
283         boolean wasShown = view.isShown();
284         ViewGroup messageParent = (ViewGroup) view.getParent();
285         if (messageParent == null) {
286             return;
287         }
288         messageParent.removeView(view);
289         if (wasShown && !MessagingLinearLayout.isGone(view)) {
290             messageParent.addTransientView(view, 0);
291             performRemoveAnimation(view, () -> {
292                 messageParent.removeTransientView(view);
293                 messagingMessage.recycle();
294             });
295         } else {
296             toRecycle.add(messagingMessage);
297         }
298     }
299 
recycle()300     public void recycle() {
301         if (mIsolatedMessage != null) {
302             mImageContainer.removeView(mIsolatedMessage);
303         }
304         for (int i = 0; i < mMessages.size(); i++) {
305             MessagingMessage message = mMessages.get(i);
306             mMessageContainer.removeView(message.getView());
307             message.recycle();
308         }
309         setAvatar(null);
310         mAvatarView.setAlpha(1.0f);
311         mAvatarView.setTranslationY(0.0f);
312         mSenderView.setAlpha(1.0f);
313         mSenderView.setTranslationY(0.0f);
314         setAlpha(1.0f);
315         mIsolatedMessage = null;
316         mMessages = null;
317         mSenderName = null;
318         mAddedMessages.clear();
319         mFirstLayout = true;
320         setCanHideSenderIfFirst(false);
321         setIsFirstInLayout(true);
322 
323         setMaxDisplayedLines(Integer.MAX_VALUE);
324         setSingleLine(false);
325         setShowingAvatar(true);
326         MessagingPropertyAnimator.recycle(this);
327         sInstancePool.release(MessagingGroup.this);
328     }
329 
removeGroupAnimated(Runnable endAction)330     public void removeGroupAnimated(Runnable endAction) {
331         performRemoveAnimation(this, () -> {
332             setAlpha(1.0f);
333             MessagingPropertyAnimator.setToLaidOutPosition(this);
334             if (endAction != null) {
335                 endAction.run();
336             }
337         });
338     }
339 
performRemoveAnimation(View message, Runnable endAction)340     public void performRemoveAnimation(View message, Runnable endAction) {
341         performRemoveAnimation(message, -message.getHeight(), endAction);
342     }
343 
performRemoveAnimation(View view, int disappearTranslation, Runnable endAction)344     private void performRemoveAnimation(View view, int disappearTranslation, Runnable endAction) {
345         MessagingPropertyAnimator.startLocalTranslationTo(view, disappearTranslation,
346                 MessagingLayout.FAST_OUT_LINEAR_IN);
347         MessagingPropertyAnimator.fadeOut(view, endAction);
348     }
349 
getSenderName()350     public CharSequence getSenderName() {
351         return mSenderName;
352     }
353 
dropCache()354     public static void dropCache() {
355         sInstancePool.clear();
356     }
357 
358     @Override
getMeasuredType()359     public int getMeasuredType() {
360         if (mIsolatedMessage != null) {
361             // We only want to show one group if we have an inline image, so let's return shortened
362             // to avoid displaying the other ones.
363             return MEASURED_SHORTENED;
364         }
365         boolean hasNormal = false;
366         for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
367             View child = mMessageContainer.getChildAt(i);
368             if (child.getVisibility() == GONE) {
369                 continue;
370             }
371             if (child instanceof MessagingLinearLayout.MessagingChild) {
372                 int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType();
373                 boolean tooSmall = type == MEASURED_TOO_SMALL;
374                 final MessagingLinearLayout.LayoutParams lp =
375                         (MessagingLinearLayout.LayoutParams) child.getLayoutParams();
376                 tooSmall |= lp.hide;
377                 if (tooSmall) {
378                     if (hasNormal) {
379                         return MEASURED_SHORTENED;
380                     } else {
381                         return MEASURED_TOO_SMALL;
382                     }
383                 } else if (type == MEASURED_SHORTENED) {
384                     return MEASURED_SHORTENED;
385                 } else {
386                     hasNormal = true;
387                 }
388             }
389         }
390         return MEASURED_NORMAL;
391     }
392 
393     @Override
getConsumedLines()394     public int getConsumedLines() {
395         int result = 0;
396         for (int i = 0; i < mMessageContainer.getChildCount(); i++) {
397             View child = mMessageContainer.getChildAt(i);
398             if (child instanceof MessagingLinearLayout.MessagingChild) {
399                 result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines();
400             }
401         }
402         result = mIsolatedMessage != null ? Math.max(result, 1) : result;
403         // A group is usually taking up quite some space with the padding and the name, let's add 1
404         return result + 1;
405     }
406 
407     @Override
setMaxDisplayedLines(int lines)408     public void setMaxDisplayedLines(int lines) {
409         mRequestedMaxDisplayedLines = lines;
410         updateMaxDisplayedLines();
411     }
412 
updateMaxDisplayedLines()413     private void updateMaxDisplayedLines() {
414         mMessageContainer.setMaxDisplayedLines(mSingleLine ? 1 : mRequestedMaxDisplayedLines);
415     }
416 
417     @Override
hideAnimated()418     public void hideAnimated() {
419         setIsHidingAnimated(true);
420         removeGroupAnimated(() -> setIsHidingAnimated(false));
421     }
422 
423     @Override
isHidingAnimated()424     public boolean isHidingAnimated() {
425         return mIsHidingAnimated;
426     }
427 
428     @Override
setIsFirstInLayout(boolean first)429     public void setIsFirstInLayout(boolean first) {
430         if (first != mIsFirstGroupInLayout) {
431             mIsFirstGroupInLayout = first;
432             updateSenderVisibility();
433         }
434     }
435 
436     /**
437      * @param canHide true if the sender can be hidden if it is first
438      */
setCanHideSenderIfFirst(boolean canHide)439     public void setCanHideSenderIfFirst(boolean canHide) {
440         if (mCanHideSenderIfFirst != canHide) {
441             mCanHideSenderIfFirst = canHide;
442             updateSenderVisibility();
443         }
444     }
445 
updateSenderVisibility()446     private void updateSenderVisibility() {
447         boolean hidden = (mIsFirstGroupInLayout || mSingleLine) && mCanHideSenderIfFirst
448                 || TextUtils.isEmpty(mSenderName);
449         mSenderView.setVisibility(hidden ? GONE : VISIBLE);
450     }
451 
updateIconVisibility()452     private void updateIconVisibility() {
453         if (Flags.notificationsRedesignTemplates()) {
454             // We don't show any icon (other than the app or person icon) in the collapsed form.
455             mMessagingIconContainer.setVisibility(mIsCollapsed ? GONE : VISIBLE);
456         }
457     }
458 
459     @Override
hasDifferentHeightWhenFirst()460     public boolean hasDifferentHeightWhenFirst() {
461         return mCanHideSenderIfFirst && !mSingleLine && !TextUtils.isEmpty(mSenderName);
462     }
463 
setIsHidingAnimated(boolean isHiding)464     private void setIsHidingAnimated(boolean isHiding) {
465         ViewParent parent = getParent();
466         mIsHidingAnimated = isHiding;
467         invalidate();
468         if (parent instanceof ViewGroup) {
469             ((ViewGroup) parent).invalidate();
470         }
471     }
472 
473     @Override
hasOverlappingRendering()474     public boolean hasOverlappingRendering() {
475         return false;
476     }
477 
getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol, int layoutColor)478     public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol,
479             int layoutColor) {
480         if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol)
481                 && layoutColor == mLayoutColor) {
482             return mAvatarIcon;
483         }
484         return null;
485     }
486 
setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol, int layoutColor)487     public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol,
488             int layoutColor) {
489         if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol)
490                 || layoutColor != mLayoutColor) {
491             setAvatar(cachedIcon);
492             mAvatarSymbol = avatarSymbol;
493             setLayoutColor(layoutColor);
494             mAvatarName = avatarName;
495         }
496     }
497 
setTextColors(int senderTextColor, int messageTextColor)498     public void setTextColors(int senderTextColor, int messageTextColor) {
499         mTextColor = messageTextColor;
500         mSendingTextColor = calculateSendingTextColor();
501         updateMessageColor();
502         mSenderView.setTextColor(senderTextColor);
503     }
504 
setLayoutColor(int layoutColor)505     public void setLayoutColor(int layoutColor) {
506         if (layoutColor != mLayoutColor){
507             mLayoutColor = layoutColor;
508             mSendingSpinner.setIndeterminateTintList(ColorStateList.valueOf(mLayoutColor));
509         }
510     }
511 
updateMessageColor()512     private void updateMessageColor() {
513         if (mMessages != null) {
514             int color = mSendingSpinnerContainer.getVisibility() == View.VISIBLE
515                     ? mSendingTextColor : mTextColor;
516             for (MessagingMessage message : mMessages) {
517                 final boolean isRemoteInputHistory =
518                         message.getMessage() != null && message.getMessage().isRemoteInputHistory();
519                 message.setColor(isRemoteInputHistory ? color : mTextColor);
520             }
521         }
522     }
523 
setMessages(List<MessagingMessage> group)524     public void setMessages(List<MessagingMessage> group) {
525         // Let's now make sure all children are added and in the correct order
526         int textMessageIndex = 0;
527         MessagingImageMessage isolatedMessage = null;
528         for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) {
529             MessagingMessage message = group.get(messageIndex);
530             if (message.getGroup() != this) {
531                 message.setMessagingGroup(this);
532                 mAddedMessages.add(message);
533             }
534             boolean isImage = message instanceof MessagingImageMessage;
535             if (mImageDisplayLocation != IMAGE_DISPLAY_LOCATION_INLINE && isImage) {
536                 isolatedMessage = (MessagingImageMessage) message;
537             } else {
538                 if (removeFromParentIfDifferent(message, mMessageContainer)) {
539                     ViewGroup.LayoutParams layoutParams = message.getView().getLayoutParams();
540                     if (layoutParams != null
541                             && !(layoutParams instanceof MessagingLinearLayout.LayoutParams)) {
542                         message.getView().setLayoutParams(
543                                 mMessageContainer.generateDefaultLayoutParams());
544                     }
545                     mMessageContainer.addView(message.getView(), textMessageIndex);
546                 }
547                 if (isImage) {
548                     ((MessagingImageMessage) message).setIsolated(false);
549                 }
550                 // Let's sort them properly
551                 if (textMessageIndex != mMessageContainer.indexOfChild(message.getView())) {
552                     mMessageContainer.removeView(message.getView());
553                     mMessageContainer.addView(message.getView(), textMessageIndex);
554                 }
555                 textMessageIndex++;
556             }
557         }
558         if (isolatedMessage != null) {
559             if (mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_AT_END
560                     && removeFromParentIfDifferent(isolatedMessage, mImageContainer)) {
561                 mImageContainer.removeAllViews();
562                 mImageContainer.addView(isolatedMessage.getView());
563             } else if (mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_EXTERNAL) {
564                 mImageContainer.removeAllViews();
565             }
566             isolatedMessage.setIsolated(true);
567         } else if (mIsolatedMessage != null) {
568             mImageContainer.removeAllViews();
569         }
570         mIsolatedMessage = isolatedMessage;
571         updateImageContainerVisibility();
572         mMessages = group;
573         if (android.widget.flags.Flags.dropNonExistingMessages()) {
574             // remove messages from mAddedMessages when they are no longer in mMessages.
575             mAddedMessages.removeIf(message -> !mMessages.contains(message));
576         }
577         updateMessageColor();
578     }
579 
updateImageContainerVisibility()580     private void updateImageContainerVisibility() {
581         mImageContainer.setVisibility(mIsolatedMessage != null
582                 && mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_AT_END
583                 ? View.VISIBLE : View.GONE);
584     }
585 
586     /**
587      * Remove the message from the parent if the parent isn't the one provided
588      * @return whether the message was removed
589      */
removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent)590     private boolean removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent) {
591         ViewParent parent = message.getView().getParent();
592         if (parent != newParent) {
593             if (parent instanceof ViewGroup) {
594                 ((ViewGroup) parent).removeView(message.getView());
595             }
596             return true;
597         }
598         return false;
599     }
600 
601     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)602     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
603         super.onLayout(changed, left, top, right, bottom);
604         if (!mAddedMessages.isEmpty()) {
605             final boolean firstLayout = mFirstLayout;
606             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
607                 @Override
608                 public boolean onPreDraw() {
609                     for (MessagingMessage message : mAddedMessages) {
610                         if (!message.getView().isShown()) {
611                             continue;
612                         }
613                         MessagingPropertyAnimator.fadeIn(message.getView());
614                         if (!firstLayout) {
615                             MessagingPropertyAnimator.startLocalTranslationFrom(message.getView(),
616                                     message.getView().getHeight(),
617                                     MessagingLayout.LINEAR_OUT_SLOW_IN);
618                         }
619                     }
620                     mAddedMessages.clear();
621                     getViewTreeObserver().removeOnPreDrawListener(this);
622                     return true;
623                 }
624             });
625         }
626         mFirstLayout = false;
627         updateClipRect();
628     }
629 
630     /**
631      * Calculates the group compatibility between this and another group.
632      *
633      * @param otherGroup the other group to compare it with
634      *
635      * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if
636      *         they match.
637      */
calculateGroupCompatibility(MessagingGroup otherGroup)638     public int calculateGroupCompatibility(MessagingGroup otherGroup) {
639         if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) {
640             int result = 1;
641             for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) {
642                 MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i);
643                 MessagingMessage otherMessage = otherGroup.mMessages.get(
644                         otherGroup.mMessages.size() - 1 - i);
645                 if (!ownMessage.sameAs(otherMessage)) {
646                     return result;
647                 }
648                 result++;
649             }
650             return result;
651         }
652         return 0;
653     }
654 
getSenderView()655     public TextView getSenderView() {
656         return mSenderView;
657     }
658 
getAvatar()659     public View getAvatar() {
660         return mAvatarView;
661     }
662 
getAvatarIcon()663     public Icon getAvatarIcon() {
664         return mAvatarIcon;
665     }
666 
getMessageContainer()667     public MessagingLinearLayout getMessageContainer() {
668         return mMessageContainer;
669     }
670 
getIsolatedMessage()671     public MessagingImageMessage getIsolatedMessage() {
672         return mIsolatedMessage;
673     }
674 
needsGeneratedAvatar()675     public boolean needsGeneratedAvatar() {
676         return mNeedsGeneratedAvatar;
677     }
678 
getSender()679     public Person getSender() {
680         return mSender;
681     }
682 
setClippingDisabled(boolean disabled)683     public void setClippingDisabled(boolean disabled) {
684         mClippingDisabled = disabled;
685     }
686 
setImageDisplayLocation(@mageDisplayLocation int displayLocation)687     public void setImageDisplayLocation(@ImageDisplayLocation int displayLocation) {
688         if (mImageDisplayLocation != displayLocation) {
689             mImageDisplayLocation = displayLocation;
690             updateImageContainerVisibility();
691         }
692     }
693 
getMessages()694     public List<MessagingMessage> getMessages() {
695         return mMessages;
696     }
697 
698     /**
699      * Set this layout to be single line and therefore displaying both the sender and the text on
700      * the same line.
701      *
702      * @param singleLine should be layout be single line
703      */
setSingleLine(boolean singleLine)704     public void setSingleLine(boolean singleLine) {
705         if (singleLine != mSingleLine) {
706             mSingleLine = singleLine;
707             MarginLayoutParams p = (MarginLayoutParams) mMessageContainer.getLayoutParams();
708             p.topMargin = singleLine ? 0 : mNotificationTextMarginTop;
709             mMessageContainer.setLayoutParams(p);
710             mContentContainer.setOrientation(
711                     singleLine ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
712             MarginLayoutParams layoutParams = (MarginLayoutParams) mSenderView.getLayoutParams();
713             layoutParams.setMarginEnd(singleLine ? mSenderTextPaddingSingleLine : 0);
714             mSenderView.setSingleLine(singleLine);
715             updateMaxDisplayedLines();
716             updateClipRect();
717             updateSenderVisibility();
718         }
719     }
720 
721     /**
722      * Sets whether this is in a collapsed layout or not. Certain elements like icons are not shown
723      * when the notification is collapsed.
724      */
setIsCollapsed(boolean isCollapsed)725     public void setIsCollapsed(boolean isCollapsed) {
726         mIsCollapsed = isCollapsed;
727         updateIconVisibility();
728     }
729 
isSingleLine()730     public boolean isSingleLine() {
731         return mSingleLine;
732     }
733 
734     /**
735      * Set this group to be displayed in a conversation and adjust the visual appearance
736      *
737      * @param isInConversation is this in a conversation
738      */
setIsInConversation(boolean isInConversation)739     public void setIsInConversation(boolean isInConversation) {
740         if (mIsInConversation != isInConversation) {
741             mIsInConversation = isInConversation;
742 
743             if (Flags.notificationsRedesignTemplates()) {
744                 updateIconVisibility();
745                 // No other alignment adjustments are necessary in the redesign, as the size of the
746                 // icons in both conversations and old messaging notifications are the same.
747                 return;
748             }
749 
750             MarginLayoutParams layoutParams =
751                     (MarginLayoutParams) mMessagingIconContainer.getLayoutParams();
752             layoutParams.width = mIsInConversation
753                     ? mConversationContentStart
754                     : mNonConversationContentStart;
755             mMessagingIconContainer.setLayoutParams(layoutParams);
756             int imagePaddingStart = isInConversation ? 0 : mNonConversationPaddingStart;
757             mMessagingIconContainer.setPaddingRelative(imagePaddingStart, 0, 0, 0);
758 
759             ViewGroup.LayoutParams avatarLayoutParams = mAvatarView.getLayoutParams();
760             int size = mIsInConversation ? mConversationAvatarSize : mNonConversationAvatarSize;
761             avatarLayoutParams.height = size;
762             avatarLayoutParams.width = size;
763             mAvatarView.setLayoutParams(avatarLayoutParams);
764         }
765     }
766 
767     @IntDef(prefix = {"IMAGE_DISPLAY_LOCATION_"}, value = {
768             IMAGE_DISPLAY_LOCATION_INLINE,
769             IMAGE_DISPLAY_LOCATION_AT_END,
770             IMAGE_DISPLAY_LOCATION_EXTERNAL
771     })
772     @Retention(RetentionPolicy.SOURCE)
773     private @interface ImageDisplayLocation {
774     }
775 }
776