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