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