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