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.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.StyleRes;
23 import android.app.Person;
24 import android.content.Context;
25 import android.content.res.ColorStateList;
26 import android.graphics.Color;
27 import android.graphics.Point;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Icon;
30 import android.text.TextUtils;
31 import android.util.AttributeSet;
32 import android.util.DisplayMetrics;
33 import android.util.Pools;
34 import android.util.TypedValue;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.ViewParent;
39 import android.view.ViewTreeObserver;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.ProgressBar;
43 import android.widget.RemoteViews;
44 
45 import com.android.internal.R;
46 
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * A message of a {@link MessagingLayout}.
52  */
53 @RemoteViews.RemoteView
54 public class MessagingGroup extends LinearLayout implements MessagingLinearLayout.MessagingChild {
55     private static Pools.SimplePool<MessagingGroup> sInstancePool
56             = new Pools.SynchronizedPool<>(10);
57     private MessagingLinearLayout mMessageContainer;
58     private ImageFloatingTextView mSenderName;
59     private ImageView mAvatarView;
60     private String mAvatarSymbol = "";
61     private int mLayoutColor;
62     private CharSequence mAvatarName = "";
63     private Icon mAvatarIcon;
64     private int mTextColor;
65     private int mSendingTextColor;
66     private List<MessagingMessage> mMessages;
67     private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>();
68     private boolean mFirstLayout;
69     private boolean mIsHidingAnimated;
70     private boolean mNeedsGeneratedAvatar;
71     private Person mSender;
72     private boolean mImagesAtEnd;
73     private ViewGroup mImageContainer;
74     private MessagingImageMessage mIsolatedMessage;
75     private boolean mTransformingImages;
76     private Point mDisplaySize = new Point();
77     private ProgressBar mSendingSpinner;
78     private View mSendingSpinnerContainer;
79 
MessagingGroup(@onNull Context context)80     public MessagingGroup(@NonNull Context context) {
81         super(context);
82     }
83 
MessagingGroup(@onNull Context context, @Nullable AttributeSet attrs)84     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
85         super(context, attrs);
86     }
87 
MessagingGroup(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)88     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
89             @AttrRes int defStyleAttr) {
90         super(context, attrs, defStyleAttr);
91     }
92 
MessagingGroup(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)93     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
94             @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
95         super(context, attrs, defStyleAttr, defStyleRes);
96     }
97 
98     @Override
onFinishInflate()99     protected void onFinishInflate() {
100         super.onFinishInflate();
101         mMessageContainer = findViewById(R.id.group_message_container);
102         mSenderName = findViewById(R.id.message_name);
103         mAvatarView = findViewById(R.id.message_icon);
104         mImageContainer = findViewById(R.id.messaging_group_icon_container);
105         mSendingSpinner = findViewById(R.id.messaging_group_sending_progress);
106         mSendingSpinnerContainer = findViewById(R.id.messaging_group_sending_progress_container);
107         DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
108         mDisplaySize.x = displayMetrics.widthPixels;
109         mDisplaySize.y = displayMetrics.heightPixels;
110     }
111 
updateClipRect()112     public void updateClipRect() {
113         // We want to clip to the senderName if it's available, otherwise our images will come
114         // from a weird position
115         Rect clipRect;
116         if (mSenderName.getVisibility() != View.GONE && !mTransformingImages) {
117             ViewGroup parent = (ViewGroup) mSenderName.getParent();
118             int top = getDistanceFromParent(mSenderName, parent) - getDistanceFromParent(
119                     mMessageContainer, parent) + mSenderName.getHeight();
120             int size = Math.max(mDisplaySize.x, mDisplaySize.y);
121             clipRect = new Rect(0, top, size, size);
122         } else {
123             clipRect = null;
124         }
125         mMessageContainer.setClipBounds(clipRect);
126     }
127 
getDistanceFromParent(View searchedView, ViewGroup parent)128     private int getDistanceFromParent(View searchedView, ViewGroup parent) {
129         int position = 0;
130         View view = searchedView;
131         while(view != parent) {
132             position += view.getTop() + view.getTranslationY();
133             view = (View) view.getParent();
134         }
135         return position;
136     }
137 
setSender(Person sender, CharSequence nameOverride)138     public void setSender(Person sender, CharSequence nameOverride) {
139         mSender = sender;
140         if (nameOverride == null) {
141             nameOverride = sender.getName();
142         }
143         mSenderName.setText(nameOverride);
144         mNeedsGeneratedAvatar = sender.getIcon() == null;
145         if (!mNeedsGeneratedAvatar) {
146             setAvatar(sender.getIcon());
147         }
148         mAvatarView.setVisibility(VISIBLE);
149         mSenderName.setVisibility(TextUtils.isEmpty(nameOverride) ? GONE : VISIBLE);
150     }
151 
setSending(boolean sending)152     public void setSending(boolean sending) {
153         int visibility = sending ? View.VISIBLE : View.GONE;
154         if (mSendingSpinnerContainer.getVisibility() != visibility) {
155             mSendingSpinnerContainer.setVisibility(visibility);
156             updateMessageColor();
157         }
158     }
159 
calculateSendingTextColor()160     private int calculateSendingTextColor() {
161         TypedValue alphaValue = new TypedValue();
162         mContext.getResources().getValue(
163                 R.dimen.notification_secondary_text_disabled_alpha, alphaValue, true);
164         float alpha = alphaValue.getFloat();
165         return Color.valueOf(
166                 Color.red(mTextColor),
167                 Color.green(mTextColor),
168                 Color.blue(mTextColor),
169                 alpha).toArgb();
170     }
171 
setAvatar(Icon icon)172     public void setAvatar(Icon icon) {
173         mAvatarIcon = icon;
174         mAvatarView.setImageIcon(icon);
175         mAvatarSymbol = "";
176         mAvatarName = "";
177     }
178 
createGroup(MessagingLinearLayout layout)179     static MessagingGroup createGroup(MessagingLinearLayout layout) {;
180         MessagingGroup createdGroup = sInstancePool.acquire();
181         if (createdGroup == null) {
182             createdGroup = (MessagingGroup) LayoutInflater.from(layout.getContext()).inflate(
183                     R.layout.notification_template_messaging_group, layout,
184                     false);
185             createdGroup.addOnLayoutChangeListener(MessagingLayout.MESSAGING_PROPERTY_ANIMATOR);
186         }
187         layout.addView(createdGroup);
188         return createdGroup;
189     }
190 
removeMessage(MessagingMessage messagingMessage)191     public void removeMessage(MessagingMessage messagingMessage) {
192         View view = messagingMessage.getView();
193         boolean wasShown = view.isShown();
194         ViewGroup messageParent = (ViewGroup) view.getParent();
195         if (messageParent == null) {
196             return;
197         }
198         messageParent.removeView(view);
199         Runnable recycleRunnable = () -> {
200             messageParent.removeTransientView(view);
201             messagingMessage.recycle();
202         };
203         if (wasShown && !MessagingLinearLayout.isGone(view)) {
204             messageParent.addTransientView(view, 0);
205             performRemoveAnimation(view, recycleRunnable);
206         } else {
207             recycleRunnable.run();
208         }
209     }
210 
recycle()211     public void recycle() {
212         if (mIsolatedMessage != null) {
213             mImageContainer.removeView(mIsolatedMessage);
214         }
215         for (int i = 0; i < mMessages.size(); i++) {
216             MessagingMessage message = mMessages.get(i);
217             mMessageContainer.removeView(message.getView());
218             message.recycle();
219         }
220         setAvatar(null);
221         mAvatarView.setAlpha(1.0f);
222         mAvatarView.setTranslationY(0.0f);
223         mSenderName.setAlpha(1.0f);
224         mSenderName.setTranslationY(0.0f);
225         setAlpha(1.0f);
226         mIsolatedMessage = null;
227         mMessages = null;
228         mAddedMessages.clear();
229         mFirstLayout = true;
230         MessagingPropertyAnimator.recycle(this);
231         sInstancePool.release(MessagingGroup.this);
232     }
233 
removeGroupAnimated(Runnable endAction)234     public void removeGroupAnimated(Runnable endAction) {
235         performRemoveAnimation(this, () -> {
236             setAlpha(1.0f);
237             MessagingPropertyAnimator.setToLaidOutPosition(this);
238             if (endAction != null) {
239                 endAction.run();
240             }
241         });
242     }
243 
performRemoveAnimation(View message, Runnable endAction)244     public void performRemoveAnimation(View message, Runnable endAction) {
245         performRemoveAnimation(message, -message.getHeight(), endAction);
246     }
247 
performRemoveAnimation(View view, int disappearTranslation, Runnable endAction)248     private void performRemoveAnimation(View view, int disappearTranslation, Runnable endAction) {
249         MessagingPropertyAnimator.startLocalTranslationTo(view, disappearTranslation,
250                 MessagingLayout.FAST_OUT_LINEAR_IN);
251         MessagingPropertyAnimator.fadeOut(view, endAction);
252     }
253 
getSenderName()254     public CharSequence getSenderName() {
255         return mSenderName.getText();
256     }
257 
dropCache()258     public static void dropCache() {
259         sInstancePool = new Pools.SynchronizedPool<>(10);
260     }
261 
262     @Override
getMeasuredType()263     public int getMeasuredType() {
264         if (mIsolatedMessage != null) {
265             // We only want to show one group if we have an inline image, so let's return shortened
266             // to avoid displaying the other ones.
267             return MEASURED_SHORTENED;
268         }
269         boolean hasNormal = false;
270         for (int i = mMessageContainer.getChildCount() - 1; i >= 0; i--) {
271             View child = mMessageContainer.getChildAt(i);
272             if (child.getVisibility() == GONE) {
273                 continue;
274             }
275             if (child instanceof MessagingLinearLayout.MessagingChild) {
276                 int type = ((MessagingLinearLayout.MessagingChild) child).getMeasuredType();
277                 boolean tooSmall = type == MEASURED_TOO_SMALL;
278                 final MessagingLinearLayout.LayoutParams lp =
279                         (MessagingLinearLayout.LayoutParams) child.getLayoutParams();
280                 tooSmall |= lp.hide;
281                 if (tooSmall) {
282                     if (hasNormal) {
283                         return MEASURED_SHORTENED;
284                     } else {
285                         return MEASURED_TOO_SMALL;
286                     }
287                 } else if (type == MEASURED_SHORTENED) {
288                     return MEASURED_SHORTENED;
289                 } else {
290                     hasNormal = true;
291                 }
292             }
293         }
294         return MEASURED_NORMAL;
295     }
296 
297     @Override
getConsumedLines()298     public int getConsumedLines() {
299         int result = 0;
300         for (int i = 0; i < mMessageContainer.getChildCount(); i++) {
301             View child = mMessageContainer.getChildAt(i);
302             if (child instanceof MessagingLinearLayout.MessagingChild) {
303                 result += ((MessagingLinearLayout.MessagingChild) child).getConsumedLines();
304             }
305         }
306         result = mIsolatedMessage != null ? Math.max(result, 1) : result;
307         // A group is usually taking up quite some space with the padding and the name, let's add 1
308         return result + 1;
309     }
310 
311     @Override
setMaxDisplayedLines(int lines)312     public void setMaxDisplayedLines(int lines) {
313         mMessageContainer.setMaxDisplayedLines(lines);
314     }
315 
316     @Override
hideAnimated()317     public void hideAnimated() {
318         setIsHidingAnimated(true);
319         removeGroupAnimated(() -> setIsHidingAnimated(false));
320     }
321 
322     @Override
isHidingAnimated()323     public boolean isHidingAnimated() {
324         return mIsHidingAnimated;
325     }
326 
setIsHidingAnimated(boolean isHiding)327     private void setIsHidingAnimated(boolean isHiding) {
328         ViewParent parent = getParent();
329         mIsHidingAnimated = isHiding;
330         invalidate();
331         if (parent instanceof ViewGroup) {
332             ((ViewGroup) parent).invalidate();
333         }
334     }
335 
336     @Override
hasOverlappingRendering()337     public boolean hasOverlappingRendering() {
338         return false;
339     }
340 
getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol, int layoutColor)341     public Icon getAvatarSymbolIfMatching(CharSequence avatarName, String avatarSymbol,
342             int layoutColor) {
343         if (mAvatarName.equals(avatarName) && mAvatarSymbol.equals(avatarSymbol)
344                 && layoutColor == mLayoutColor) {
345             return mAvatarIcon;
346         }
347         return null;
348     }
349 
setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol, int layoutColor)350     public void setCreatedAvatar(Icon cachedIcon, CharSequence avatarName, String avatarSymbol,
351             int layoutColor) {
352         if (!mAvatarName.equals(avatarName) || !mAvatarSymbol.equals(avatarSymbol)
353                 || layoutColor != mLayoutColor) {
354             setAvatar(cachedIcon);
355             mAvatarSymbol = avatarSymbol;
356             setLayoutColor(layoutColor);
357             mAvatarName = avatarName;
358         }
359     }
360 
setTextColors(int senderTextColor, int messageTextColor)361     public void setTextColors(int senderTextColor, int messageTextColor) {
362         mTextColor = messageTextColor;
363         mSendingTextColor = calculateSendingTextColor();
364         updateMessageColor();
365         mSenderName.setTextColor(senderTextColor);
366     }
367 
setLayoutColor(int layoutColor)368     public void setLayoutColor(int layoutColor) {
369         if (layoutColor != mLayoutColor){
370             mLayoutColor = layoutColor;
371             mSendingSpinner.setIndeterminateTintList(ColorStateList.valueOf(mLayoutColor));
372         }
373     }
374 
updateMessageColor()375     private void updateMessageColor() {
376         if (mMessages != null) {
377             int color = mSendingSpinnerContainer.getVisibility() == View.VISIBLE
378                     ? mSendingTextColor : mTextColor;
379             for (MessagingMessage message : mMessages) {
380                 message.setColor(message.getMessage().isRemoteInputHistory() ? color : mTextColor);
381             }
382         }
383     }
384 
setMessages(List<MessagingMessage> group)385     public void setMessages(List<MessagingMessage> group) {
386         // Let's now make sure all children are added and in the correct order
387         int textMessageIndex = 0;
388         MessagingImageMessage isolatedMessage = null;
389         for (int messageIndex = 0; messageIndex < group.size(); messageIndex++) {
390             MessagingMessage message = group.get(messageIndex);
391             if (message.getGroup() != this) {
392                 message.setMessagingGroup(this);
393                 mAddedMessages.add(message);
394             }
395             boolean isImage = message instanceof MessagingImageMessage;
396             if (mImagesAtEnd && isImage) {
397                 isolatedMessage = (MessagingImageMessage) message;
398             } else {
399                 if (removeFromParentIfDifferent(message, mMessageContainer)) {
400                     ViewGroup.LayoutParams layoutParams = message.getView().getLayoutParams();
401                     if (layoutParams != null
402                             && !(layoutParams instanceof MessagingLinearLayout.LayoutParams)) {
403                         message.getView().setLayoutParams(
404                                 mMessageContainer.generateDefaultLayoutParams());
405                     }
406                     mMessageContainer.addView(message.getView(), textMessageIndex);
407                 }
408                 if (isImage) {
409                     ((MessagingImageMessage) message).setIsolated(false);
410                 }
411                 // Let's sort them properly
412                 if (textMessageIndex != mMessageContainer.indexOfChild(message.getView())) {
413                     mMessageContainer.removeView(message.getView());
414                     mMessageContainer.addView(message.getView(), textMessageIndex);
415                 }
416                 textMessageIndex++;
417             }
418         }
419         if (isolatedMessage != null) {
420             if (removeFromParentIfDifferent(isolatedMessage, mImageContainer)) {
421                 mImageContainer.removeAllViews();
422                 mImageContainer.addView(isolatedMessage.getView());
423             }
424             isolatedMessage.setIsolated(true);
425         } else if (mIsolatedMessage != null) {
426             mImageContainer.removeAllViews();
427         }
428         mIsolatedMessage = isolatedMessage;
429         updateImageContainerVisibility();
430         mMessages = group;
431         updateMessageColor();
432     }
433 
updateImageContainerVisibility()434     private void updateImageContainerVisibility() {
435         mImageContainer.setVisibility(mIsolatedMessage != null && mImagesAtEnd
436                 ? View.VISIBLE : View.GONE);
437     }
438 
439     /**
440      * Remove the message from the parent if the parent isn't the one provided
441      * @return whether the message was removed
442      */
removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent)443     private boolean removeFromParentIfDifferent(MessagingMessage message, ViewGroup newParent) {
444         ViewParent parent = message.getView().getParent();
445         if (parent != newParent) {
446             if (parent instanceof ViewGroup) {
447                 ((ViewGroup) parent).removeView(message.getView());
448             }
449             return true;
450         }
451         return false;
452     }
453 
454     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)455     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
456         super.onLayout(changed, left, top, right, bottom);
457         if (!mAddedMessages.isEmpty()) {
458             final boolean firstLayout = mFirstLayout;
459             getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
460                 @Override
461                 public boolean onPreDraw() {
462                     for (MessagingMessage message : mAddedMessages) {
463                         if (!message.getView().isShown()) {
464                             continue;
465                         }
466                         MessagingPropertyAnimator.fadeIn(message.getView());
467                         if (!firstLayout) {
468                             MessagingPropertyAnimator.startLocalTranslationFrom(message.getView(),
469                                     message.getView().getHeight(),
470                                     MessagingLayout.LINEAR_OUT_SLOW_IN);
471                         }
472                     }
473                     mAddedMessages.clear();
474                     getViewTreeObserver().removeOnPreDrawListener(this);
475                     return true;
476                 }
477             });
478         }
479         mFirstLayout = false;
480         updateClipRect();
481     }
482 
483     /**
484      * Calculates the group compatibility between this and another group.
485      *
486      * @param otherGroup the other group to compare it with
487      *
488      * @return 0 if the groups are totally incompatible or 1 + the number of matching messages if
489      *         they match.
490      */
calculateGroupCompatibility(MessagingGroup otherGroup)491     public int calculateGroupCompatibility(MessagingGroup otherGroup) {
492         if (TextUtils.equals(getSenderName(),otherGroup.getSenderName())) {
493             int result = 1;
494             for (int i = 0; i < mMessages.size() && i < otherGroup.mMessages.size(); i++) {
495                 MessagingMessage ownMessage = mMessages.get(mMessages.size() - 1 - i);
496                 MessagingMessage otherMessage = otherGroup.mMessages.get(
497                         otherGroup.mMessages.size() - 1 - i);
498                 if (!ownMessage.sameAs(otherMessage)) {
499                     return result;
500                 }
501                 result++;
502             }
503             return result;
504         }
505         return 0;
506     }
507 
getSenderView()508     public View getSenderView() {
509         return mSenderName;
510     }
511 
getAvatar()512     public View getAvatar() {
513         return mAvatarView;
514     }
515 
getMessageContainer()516     public MessagingLinearLayout getMessageContainer() {
517         return mMessageContainer;
518     }
519 
getIsolatedMessage()520     public MessagingImageMessage getIsolatedMessage() {
521         return mIsolatedMessage;
522     }
523 
needsGeneratedAvatar()524     public boolean needsGeneratedAvatar() {
525         return mNeedsGeneratedAvatar;
526     }
527 
getSender()528     public Person getSender() {
529         return mSender;
530     }
531 
setTransformingImages(boolean transformingImages)532     public void setTransformingImages(boolean transformingImages) {
533         mTransformingImages = transformingImages;
534     }
535 
setDisplayImagesAtEnd(boolean atEnd)536     public void setDisplayImagesAtEnd(boolean atEnd) {
537         if (mImagesAtEnd != atEnd) {
538             mImagesAtEnd = atEnd;
539             updateImageContainerVisibility();
540         }
541     }
542 
getMessages()543     public List<MessagingMessage> getMessages() {
544         return mMessages;
545     }
546 }
547