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