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 View mAvatarContainer;
85     private String mAvatarSymbol = "";
86     private int mLayoutColor;
87     private CharSequence mAvatarName = "";
88     private Icon mAvatarIcon;
89     private int mTextColor;
90     private int mSendingTextColor;
91     private List<MessagingMessage> mMessages;
92     private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>();
93     private boolean mFirstLayout;
94     private boolean mIsHidingAnimated;
95     private boolean mNeedsGeneratedAvatar;
96     private Person mSender;
97     private @ImageDisplayLocation int mImageDisplayLocation;
98     private ViewGroup mImageContainer;
99     private MessagingImageMessage mIsolatedMessage;
100     private boolean mClippingDisabled;
101     private Point mDisplaySize = new Point();
102     private ProgressBar mSendingSpinner;
103     private View mSendingSpinnerContainer;
104     private boolean mShowingAvatar = true;
105     private CharSequence mSenderName;
106     private boolean mSingleLine = 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                     R.layout.notification_template_messaging_group, layout,
265                     false);
266             createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
267         }
268         layout.addView(createdGroup);
269         return createdGroup;
270     }
271 
removeMessage(MessagingMessage messagingMessage, ArrayList<MessagingLinearLayout.MessagingChild> toRecycle)272     public void removeMessage(MessagingMessage messagingMessage,
273             ArrayList<MessagingLinearLayout.MessagingChild> toRecycle) {
274         View view = messagingMessage.getView();
275         boolean wasShown = view.isShown();
276         ViewGroup messageParent = (ViewGroup) view.getParent();
277         if (messageParent == null) {
278             return;
279         }
280         messageParent.removeView(view);
281         if (wasShown && !MessagingLinearLayout.isGone(view)) {
282             messageParent.addTransientView(view, 0);
283             performRemoveAnimation(view, () -> {
284                 messageParent.removeTransientView(view);
285                 messagingMessage.recycle();
286             });
287         } else {
288             toRecycle.add(messagingMessage);
289         }
290     }
291 
recycle()292     public void recycle() {
293         if (mIsolatedMessage != null) {
294             mImageContainer.removeView(mIsolatedMessage);
295         }
296         for (int i = 0; i < mMessages.size(); i++) {
297             MessagingMessage message = mMessages.get(i);
298             mMessageContainer.removeView(message.getView());
299             message.recycle();
300         }
301         setAvatar(null);
302         mAvatarView.setAlpha(1.0f);
303         mAvatarView.setTranslationY(0.0f);
304         mSenderView.setAlpha(1.0f);
305         mSenderView.setTranslationY(0.0f);
306         setAlpha(1.0f);
307         mIsolatedMessage = null;
308         mMessages = null;
309         mSenderName = null;
310         mAddedMessages.clear();
311         mFirstLayout = true;
312         setCanHideSenderIfFirst(false);
313         setIsFirstInLayout(true);
314 
315         setMaxDisplayedLines(Integer.MAX_VALUE);
316         setSingleLine(false);
317         setShowingAvatar(true);
318         MessagingPropertyAnimator.recycle(this);
319         sInstancePool.release(MessagingGroup.this);
320     }
321 
removeGroupAnimated(Runnable endAction)322     public void removeGroupAnimated(Runnable endAction) {
323         performRemoveAnimation(this, () -> {
324             setAlpha(1.0f);
325             MessagingPropertyAnimator.setToLaidOutPosition(this);
326             if (endAction != null) {
327                 endAction.run();
328             }
329         });
330     }
331 
performRemoveAnimation(View message, Runnable endAction)332     public void performRemoveAnimation(View message, Runnable endAction) {
333         performRemoveAnimation(message, -message.getHeight(), endAction);
334     }
335 
performRemoveAnimation(View view, int disappearTranslation, Runnable endAction)336     private void performRemoveAnimation(View view, int disappearTranslation, Runnable endAction) {
337         MessagingPropertyAnimator.startLocalTranslationTo(view, disappearTranslation,
338                 MessagingLayout.FAST_OUT_LINEAR_IN);
339         MessagingPropertyAnimator.fadeOut(view, endAction);
340     }
341 
getSenderName()342     public CharSequence getSenderName() {
343         return mSenderName;
344     }
345 
dropCache()346     public static void dropCache() {
347         sInstancePool.clear();
348     }
349 
350     @Override
getMeasuredType()351     public int getMeasuredType() {
352         if (mIsolatedMessage != null) {
353             // We only want to show one group if we have an inline image, so let's return shortened
354             // to avoid displaying the other ones.
355             return MEASURED_SHORTENED;
356         }
357         boolean hasNormal = false;
358         for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
359             View child = mMessageContainer.getChildAt(i);
360             if (child.getVisibility() == GONE) {
361                 continue;
362             }
363             if (child instanceof MessagingLinearLayout.MessagingChild) {
364                 int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType();
365                 boolean tooSmall = type == MEASURED_TOO_SMALL;
366                 final MessagingLinearLayout.LayoutParams lp =
367                         (MessagingLinearLayout.LayoutParams) child.getLayoutParams();
368                 tooSmall |= lp.hide;
369                 if (tooSmall) {
370                     if (hasNormal) {
371                         return MEASURED_SHORTENED;
372                     } else {
373                         return MEASURED_TOO_SMALL;
374                     }
375                 } else if (type == MEASURED_SHORTENED) {
376                     return MEASURED_SHORTENED;
377                 } else {
378                     hasNormal = true;
379                 }
380             }
381         }
382         return MEASURED_NORMAL;
383     }
384 
385     @Override
getConsumedLines()386     public int getConsumedLines() {
387         int result = 0;
388         for (int i = 0; i < mMessageContainer.getChildCount(); i++) {
389             View child = mMessageContainer.getChildAt(i);
390             if (child instanceof MessagingLinearLayout.MessagingChild) {
391                 result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines();
392             }
393         }
394         result = mIsolatedMessage != null ? Math.max(result, 1) : result;
395         // A group is usually taking up quite some space with the padding and the name, let's add 1
396         return result + 1;
397     }
398 
399     @Override
setMaxDisplayedLines(int lines)400     public void setMaxDisplayedLines(int lines) {
401         mRequestedMaxDisplayedLines = lines;
402         updateMaxDisplayedLines();
403     }
404 
updateMaxDisplayedLines()405     private void updateMaxDisplayedLines() {
406         mMessageContainer.setMaxDisplayedLines(mSingleLine ? 1 : mRequestedMaxDisplayedLines);
407     }
408 
409     @Override
hideAnimated()410     public void hideAnimated() {
411         setIsHidingAnimated(true);
412         removeGroupAnimated(() -> setIsHidingAnimated(false));
413     }
414 
415     @Override
isHidingAnimated()416     public boolean isHidingAnimated() {
417         return mIsHidingAnimated;
418     }
419 
420     @Override
setIsFirstInLayout(boolean first)421     public void setIsFirstInLayout(boolean first) {
422         if (first != mIsFirstGroupInLayout) {
423             mIsFirstGroupInLayout = first;
424             updateSenderVisibility();
425         }
426     }
427 
428     /**
429      * @param canHide true if the sender can be hidden if it is first
430      */
setCanHideSenderIfFirst(boolean canHide)431     public void setCanHideSenderIfFirst(boolean canHide) {
432         if (mCanHideSenderIfFirst != canHide) {
433             mCanHideSenderIfFirst = canHide;
434             updateSenderVisibility();
435         }
436     }
437 
updateSenderVisibility()438     private void updateSenderVisibility() {
439         boolean hidden = (mIsFirstGroupInLayout || mSingleLine) && mCanHideSenderIfFirst
440                 || TextUtils.isEmpty(mSenderName);
441         mSenderView.setVisibility(hidden ? GONE : VISIBLE);
442     }
443 
444     @Override
hasDifferentHeightWhenFirst()445     public boolean hasDifferentHeightWhenFirst() {
446         return mCanHideSenderIfFirst && !mSingleLine && !TextUtils.isEmpty(mSenderName);
447     }
448 
setIsHidingAnimated(boolean isHiding)449     private void setIsHidingAnimated(boolean isHiding) {
450         ViewParent parent = getParent();
451         mIsHidingAnimated = isHiding;
452         invalidate();
453         if (parent instanceof ViewGroup) {
454             ((ViewGroup) parent).invalidate();
455         }
456     }
457 
458     @Override
hasOverlappingRendering()459     public boolean hasOverlappingRendering() {
460         return false;
461     }
462 
getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol, int layoutColor)463     public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol,
464             int layoutColor) {
465         if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol)
466                 && layoutColor == mLayoutColor) {
467             return mAvatarIcon;
468         }
469         return null;
470     }
471 
setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol, int layoutColor)472     public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol,
473             int layoutColor) {
474         if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol)
475                 || layoutColor != mLayoutColor) {
476             setAvatar(cachedIcon);
477             mAvatarSymbol = avatarSymbol;
478             setLayoutColor(layoutColor);
479             mAvatarName = avatarName;
480         }
481     }
482 
setTextColors(int senderTextColor, int messageTextColor)483     public void setTextColors(int senderTextColor, int messageTextColor) {
484         mTextColor = messageTextColor;
485         mSendingTextColor = calculateSendingTextColor();
486         updateMessageColor();
487         mSenderView.setTextColor(senderTextColor);
488     }
489 
setLayoutColor(int layoutColor)490     public void setLayoutColor(int layoutColor) {
491         if (layoutColor != mLayoutColor){
492             mLayoutColor = layoutColor;
493             mSendingSpinner.setIndeterminateTintList(ColorStateList.valueOf(mLayoutColor));
494         }
495     }
496 
updateMessageColor()497     private void updateMessageColor() {
498         if (mMessages != null) {
499             int color = mSendingSpinnerContainer.getVisibility() == View.VISIBLE
500                     ? mSendingTextColor : mTextColor;
501             for (MessagingMessage message : mMessages) {
502                 final boolean isRemoteInputHistory =
503                         message.getMessage() != null && message.getMessage().isRemoteInputHistory();
504                 message.setColor(isRemoteInputHistory ? color : mTextColor);
505             }
506         }
507     }
508 
setMessages(List<MessagingMessage> group)509     public void setMessages(List<MessagingMessage> group) {
510         // Let's now make sure all children are added and in the correct order
511         int textMessageIndex = 0;
512         MessagingImageMessage isolatedMessage = null;
513         for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) {
514             MessagingMessage message = group.get(messageIndex);
515             if (message.getGroup() != this) {
516                 message.setMessagingGroup(this);
517                 mAddedMessages.add(message);
518             }
519             boolean isImage = message instanceof MessagingImageMessage;
520             if (mImageDisplayLocation != IMAGE_DISPLAY_LOCATION_INLINE && isImage) {
521                 isolatedMessage = (MessagingImageMessage) message;
522             } else {
523                 if (removeFromParentIfDifferent(message, mMessageContainer)) {
524                     ViewGroup.LayoutParams layoutParams = message.getView().getLayoutParams();
525                     if (layoutParams != null
526                             && !(layoutParams instanceof MessagingLinearLayout.LayoutParams)) {
527                         message.getView().setLayoutParams(
528                                 mMessageContainer.generateDefaultLayoutParams());
529                     }
530                     mMessageContainer.addView(message.getView(), textMessageIndex);
531                 }
532                 if (isImage) {
533                     ((MessagingImageMessage) message).setIsolated(false);
534                 }
535                 // Let's sort them properly
536                 if (textMessageIndex != mMessageContainer.indexOfChild(message.getView())) {
537                     mMessageContainer.removeView(message.getView());
538                     mMessageContainer.addView(message.getView(), textMessageIndex);
539                 }
540                 textMessageIndex++;
541             }
542         }
543         if (isolatedMessage != null) {
544             if (mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_AT_END
545                     && removeFromParentIfDifferent(isolatedMessage, mImageContainer)) {
546                 mImageContainer.removeAllViews();
547                 mImageContainer.addView(isolatedMessage.getView());
548             } else if (mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_EXTERNAL) {
549                 mImageContainer.removeAllViews();
550             }
551             isolatedMessage.setIsolated(true);
552         } else if (mIsolatedMessage != null) {
553             mImageContainer.removeAllViews();
554         }
555         mIsolatedMessage = isolatedMessage;
556         updateImageContainerVisibility();
557         mMessages = group;
558         updateMessageColor();
559     }
560 
updateImageContainerVisibility()561     private void updateImageContainerVisibility() {
562         mImageContainer.setVisibility(mIsolatedMessage != null
563                 && mImageDisplayLocation == IMAGE_DISPLAY_LOCATION_AT_END
564                 ? View.VISIBLE : View.GONE);
565     }
566 
567     /**
568      * Remove the message from the parent if the parent isn't the one provided
569      * @return whether the message was removed
570      */
removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent)571     private boolean removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent) {
572         ViewParent parent = message.getView().getParent();
573         if (parent != newParent) {
574             if (parent instanceof ViewGroup) {
575                 ((ViewGroup) parent).removeView(message.getView());
576             }
577             return true;
578         }
579         return false;
580     }
581 
582     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)583     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
584         super.onLayout(changed, left, top, right, bottom);
585         if (!mAddedMessages.isEmpty()) {
586             final boolean firstLayout = mFirstLayout;
587             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
588                 @Override
589                 public boolean onPreDraw() {
590                     for (MessagingMessage message : mAddedMessages) {
591                         if (!message.getView().isShown()) {
592                             continue;
593                         }
594                         MessagingPropertyAnimator.fadeIn(message.getView());
595                         if (!firstLayout) {
596                             MessagingPropertyAnimator.startLocalTranslationFrom(message.getView(),
597                                     message.getView().getHeight(),
598                                     MessagingLayout.LINEAR_OUT_SLOW_IN);
599                         }
600                     }
601                     mAddedMessages.clear();
602                     getViewTreeObserver().removeOnPreDrawListener(this);
603                     return true;
604                 }
605             });
606         }
607         mFirstLayout = false;
608         updateClipRect();
609     }
610 
611     /**
612      * Calculates the group compatibility between this and another group.
613      *
614      * @param otherGroup the other group to compare it with
615      *
616      * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if
617      *         they match.
618      */
calculateGroupCompatibility(MessagingGroup otherGroup)619     public int calculateGroupCompatibility(MessagingGroup otherGroup) {
620         if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) {
621             int result = 1;
622             for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) {
623                 MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i);
624                 MessagingMessage otherMessage = otherGroup.mMessages.get(
625                         otherGroup.mMessages.size() - 1 - i);
626                 if (!ownMessage.sameAs(otherMessage)) {
627                     return result;
628                 }
629                 result++;
630             }
631             return result;
632         }
633         return 0;
634     }
635 
getSenderView()636     public TextView getSenderView() {
637         return mSenderView;
638     }
639 
getAvatar()640     public View getAvatar() {
641         return mAvatarView;
642     }
643 
getAvatarIcon()644     public Icon getAvatarIcon() {
645         return mAvatarIcon;
646     }
647 
getMessageContainer()648     public MessagingLinearLayout getMessageContainer() {
649         return mMessageContainer;
650     }
651 
getIsolatedMessage()652     public MessagingImageMessage getIsolatedMessage() {
653         return mIsolatedMessage;
654     }
655 
needsGeneratedAvatar()656     public boolean needsGeneratedAvatar() {
657         return mNeedsGeneratedAvatar;
658     }
659 
getSender()660     public Person getSender() {
661         return mSender;
662     }
663 
setClippingDisabled(boolean disabled)664     public void setClippingDisabled(boolean disabled) {
665         mClippingDisabled = disabled;
666     }
667 
setImageDisplayLocation(@mageDisplayLocation int displayLocation)668     public void setImageDisplayLocation(@ImageDisplayLocation int displayLocation) {
669         if (mImageDisplayLocation != displayLocation) {
670             mImageDisplayLocation = displayLocation;
671             updateImageContainerVisibility();
672         }
673     }
674 
getMessages()675     public List<MessagingMessage> getMessages() {
676         return mMessages;
677     }
678 
679     /**
680      * Set this layout to be single line and therefore displaying both the sender and the text on
681      * the same line.
682      *
683      * @param singleLine should be layout be single line
684      */
setSingleLine(boolean singleLine)685     public void setSingleLine(boolean singleLine) {
686         if (singleLine != mSingleLine) {
687             mSingleLine = singleLine;
688             MarginLayoutParams p = (MarginLayoutParams) mMessageContainer.getLayoutParams();
689             p.topMargin = singleLine ? 0 : mNotificationTextMarginTop;
690             mMessageContainer.setLayoutParams(p);
691             mContentContainer.setOrientation(
692                     singleLine ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
693             MarginLayoutParams layoutParams = (MarginLayoutParams) mSenderView.getLayoutParams();
694             layoutParams.setMarginEnd(singleLine ? mSenderTextPaddingSingleLine : 0);
695             mSenderView.setSingleLine(singleLine);
696             updateMaxDisplayedLines();
697             updateClipRect();
698             updateSenderVisibility();
699         }
700     }
701 
isSingleLine()702     public boolean isSingleLine() {
703         return mSingleLine;
704     }
705 
706     /**
707      * Set this group to be displayed in a conversation and adjust the visual appearance
708      *
709      * @param isInConversation is this in a conversation
710      */
setIsInConversation(boolean isInConversation)711     public void setIsInConversation(boolean isInConversation) {
712         if (mIsInConversation != isInConversation) {
713             mIsInConversation = isInConversation;
714             MarginLayoutParams layoutParams =
715                     (MarginLayoutParams) mMessagingIconContainer.getLayoutParams();
716             layoutParams.width = mIsInConversation
717                     ? mConversationContentStart
718                     : mNonConversationContentStart;
719             mMessagingIconContainer.setLayoutParams(layoutParams);
720             int imagePaddingStart = isInConversation ? 0 : mNonConversationPaddingStart;
721             mMessagingIconContainer.setPaddingRelative(imagePaddingStart, 0, 0, 0);
722 
723             ViewGroup.LayoutParams avatarLayoutParams = mAvatarView.getLayoutParams();
724             int size = mIsInConversation ? mConversationAvatarSize : mNonConversationAvatarSize;
725             avatarLayoutParams.height = size;
726             avatarLayoutParams.width = size;
727             mAvatarView.setLayoutParams(avatarLayoutParams);
728         }
729     }
730 
731     @IntDef(prefix = {"IMAGE_DISPLAY_LOCATION_"}, value = {
732             IMAGE_DISPLAY_LOCATION_INLINE,
733             IMAGE_DISPLAY_LOCATION_AT_END,
734             IMAGE_DISPLAY_LOCATION_EXTERNAL
735     })
736     @Retention(RetentionPolicy.SOURCE)
737     private @interface ImageDisplayLocation {
738     }
739 }
740