Home | History | Annotate | Download | only in widget
      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 
     80     public MessagingGroup(@NonNull Context context) {
     81         super(context);
     82     }
     83 
     84     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
     85         super(context, attrs);
     86     }
     87 
     88     public MessagingGroup(@NonNull Context context, @Nullable AttributeSet attrs,
     89             @AttrRes int defStyleAttr) {
     90         super(context, attrs, defStyleAttr);
     91     }
     92 
     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
     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 
    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 
    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 
    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 
    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 
    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 
    172     public void setAvatar(Icon icon) {
    173         mAvatarIcon = icon;
    174         mAvatarView.setImageIcon(icon);
    175         mAvatarSymbol = "";
    176         mAvatarName = "";
    177     }
    178 
    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 
    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 
    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 
    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 
    244     public void performRemoveAnimation(View message, Runnable endAction) {
    245         performRemoveAnimation(message, -message.getHeight(), endAction);
    246     }
    247 
    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 
    254     public CharSequence getSenderName() {
    255         return mSenderName.getText();
    256     }
    257 
    258     public static void dropCache() {
    259         sInstancePool = new Pools.SynchronizedPool<>(10);
    260     }
    261 
    262     @Override
    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
    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
    312     public void setMaxDisplayedLines(int lines) {
    313         mMessageContainer.setMaxDisplayedLines(lines);
    314     }
    315 
    316     @Override
    317     public void hideAnimated() {
    318         setIsHidingAnimated(true);
    319         removeGroupAnimated(() -> setIsHidingAnimated(false));
    320     }
    321 
    322     @Override
    323     public boolean isHidingAnimated() {
    324         return mIsHidingAnimated;
    325     }
    326 
    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
    337     public boolean hasOverlappingRendering() {
    338         return false;
    339     }
    340 
    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 
    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 
    361     public void setTextColors(int senderTextColor, int messageTextColor) {
    362         mTextColor = messageTextColor;
    363         mSendingTextColor = calculateSendingTextColor();
    364         updateMessageColor();
    365         mSenderName.setTextColor(senderTextColor);
    366     }
    367 
    368     public void setLayoutColor(int layoutColor) {
    369         if (layoutColor != mLayoutColor){
    370             mLayoutColor = layoutColor;
    371             mSendingSpinner.setIndeterminateTintList(ColorStateList.valueOf(mLayoutColor));
    372         }
    373     }
    374 
    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 
    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 
    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      */
    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
    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      */
    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 
    508     public View getSenderView() {
    509         return mSenderName;
    510     }
    511 
    512     public View getAvatar() {
    513         return mAvatarView;
    514     }
    515 
    516     public MessagingLinearLayout getMessageContainer() {
    517         return mMessageContainer;
    518     }
    519 
    520     public MessagingImageMessage getIsolatedMessage() {
    521         return mIsolatedMessage;
    522     }
    523 
    524     public boolean needsGeneratedAvatar() {
    525         return mNeedsGeneratedAvatar;
    526     }
    527 
    528     public Person getSender() {
    529         return mSender;
    530     }
    531 
    532     public void setTransformingImages(boolean transformingImages) {
    533         mTransformingImages = transformingImages;
    534     }
    535 
    536     public void setDisplayImagesAtEnd(boolean atEnd) {
    537         if (mImagesAtEnd != atEnd) {
    538             mImagesAtEnd = atEnd;
    539             updateImageContainerVisibility();
    540         }
    541     }
    542 
    543     public List<MessagingMessage> getMessages() {
    544         return mMessages;
    545     }
    546 }
    547