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