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 static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL; 20 import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE; 21 22 import android.annotation.AttrRes; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.StyleRes; 26 import android.app.Notification; 27 import android.app.Person; 28 import android.app.RemoteInputHistoryItem; 29 import android.content.Context; 30 import android.graphics.Rect; 31 import android.graphics.drawable.Icon; 32 import android.os.Bundle; 33 import android.os.Parcelable; 34 import android.text.TextUtils; 35 import android.util.ArrayMap; 36 import android.util.AttributeSet; 37 import android.util.DisplayMetrics; 38 import android.view.RemotableViewMethod; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.ViewTreeObserver; 42 import android.view.animation.Interpolator; 43 import android.view.animation.PathInterpolator; 44 import android.widget.FrameLayout; 45 import android.widget.ImageView; 46 import android.widget.RemoteViews; 47 import android.widget.flags.Flags; 48 49 import com.android.internal.R; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Map; 54 55 /** 56 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal 57 * messages and adapts the layout accordingly. 58 */ 59 @RemoteViews.RemoteView 60 public class MessagingLayout extends FrameLayout 61 implements ImageMessageConsumer, IMessagingLayout { 62 63 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); 64 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 65 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 66 private static final int MAX_SUMMARIZATION_LINES = 3; 67 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR 68 = new MessagingPropertyAnimator(); 69 private final PeopleHelper mPeopleHelper = new PeopleHelper(); 70 private List<MessagingMessage> mMessages = new ArrayList<>(); 71 private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); 72 private MessagingLinearLayout mMessagingLinearLayout; 73 private boolean mShowHistoricMessages; 74 private final ArrayList<MessagingGroup> mGroups = new ArrayList<>(); 75 private MessagingLinearLayout mImageMessageContainer; 76 private ImageView mRightIconView; 77 private Rect mMessagingClipRect; 78 private int mLayoutColor; 79 private int mSenderTextColor; 80 private int mMessageTextColor; 81 private Icon mAvatarReplacement; 82 private boolean mIsOneToOne; 83 private final ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); 84 private Person mUser; 85 private CharSequence mNameReplacement; 86 private boolean mIsCollapsed; 87 private ImageResolver mImageResolver; 88 private CharSequence mConversationTitle; 89 private final ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); 90 private boolean mPrecomputedTextEnabled = false; 91 private CharSequence mSummarizedContent; 92 MessagingLayout(@onNull Context context)93 public MessagingLayout(@NonNull Context context) { 94 super(context); 95 } 96 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs)97 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 98 super(context, attrs); 99 } 100 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)101 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 102 @AttrRes int defStyleAttr) { 103 super(context, attrs, defStyleAttr); 104 } 105 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)106 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 107 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 108 super(context, attrs, defStyleAttr, defStyleRes); 109 } 110 111 @Override onFinishInflate()112 protected void onFinishInflate() { 113 super.onFinishInflate(); 114 mPeopleHelper.init(getContext()); 115 mMessagingLinearLayout = findViewById(R.id.notification_messaging); 116 mImageMessageContainer = findViewById(R.id.conversation_image_message_container); 117 mRightIconView = findViewById(R.id.right_icon); 118 // We still want to clip, but only on the top, since views can temporarily out of bounds 119 // during transitions. 120 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 121 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); 122 mMessagingClipRect = new Rect(0, 0, size, size); 123 setMessagingClippingDisabled(false); 124 } 125 126 @RemotableViewMethod(asyncImpl = "setAvatarReplacementAsync") setAvatarReplacement(Icon icon)127 public void setAvatarReplacement(Icon icon) { 128 mAvatarReplacement = icon; 129 } 130 131 /** 132 * @hide 133 */ setAvatarReplacementAsync(Icon icon)134 public Runnable setAvatarReplacementAsync(Icon icon) { 135 mAvatarReplacement = icon; 136 return () -> {}; 137 } 138 139 @RemotableViewMethod(asyncImpl = "setNameReplacementAsync") setNameReplacement(CharSequence nameReplacement)140 public void setNameReplacement(CharSequence nameReplacement) { 141 mNameReplacement = nameReplacement; 142 } 143 144 /** 145 * @hide 146 */ setNameReplacementAsync(CharSequence nameReplacement)147 public Runnable setNameReplacementAsync(CharSequence nameReplacement) { 148 mNameReplacement = nameReplacement; 149 return () -> {}; 150 } 151 152 /** 153 * Set this layout to show the collapsed representation. 154 * 155 * @param isCollapsed is it collapsed 156 */ 157 @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") setIsCollapsed(boolean isCollapsed)158 public void setIsCollapsed(boolean isCollapsed) { 159 mIsCollapsed = isCollapsed; 160 if (mIsCollapsed) { 161 mMessagingLinearLayout.setMaxDisplayedLines( 162 android.app.Flags.nmCollapsedLines() ? 2 : 1); 163 } 164 } 165 166 /** 167 * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the 168 * collapsed state early. 169 */ setIsCollapsedAsync(boolean isCollapsed)170 public Runnable setIsCollapsedAsync(boolean isCollapsed) { 171 mIsCollapsed = isCollapsed; 172 return () -> {}; 173 } 174 175 @RemotableViewMethod setLargeIcon(Icon largeIcon)176 public void setLargeIcon(Icon largeIcon) { 177 // Unused 178 } 179 180 /** 181 * Sets the conversation title of this conversation. 182 * 183 * @param conversationTitle the conversation title 184 */ 185 @RemotableViewMethod(asyncImpl = "setConversationTitleAsync") setConversationTitle(CharSequence conversationTitle)186 public void setConversationTitle(CharSequence conversationTitle) { 187 mConversationTitle = conversationTitle; 188 } 189 190 /** 191 * @hide 192 */ setConversationTitleAsync(CharSequence conversationTitle)193 public Runnable setConversationTitleAsync(CharSequence conversationTitle) { 194 mConversationTitle = conversationTitle; 195 return ()->{}; 196 } 197 198 /** 199 * Set Messaging data 200 * @param extras Bundle contains messaging data 201 */ 202 @RemotableViewMethod(asyncImpl = "setDataAsync") setData(Bundle extras)203 public void setData(Bundle extras) { 204 bind(parseMessagingData(extras, /* usePrecomputedText= */false)); 205 } 206 207 @NonNull parseMessagingData(Bundle extras, boolean usePrecomputedText)208 private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) { 209 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 210 List<Notification.MessagingStyle.Message> newMessages = 211 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 212 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); 213 List<Notification.MessagingStyle.Message> newHistoricMessages = 214 Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); 215 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, 216 Person.class)); 217 RemoteInputHistoryItem[] history = extras.getParcelableArray( 218 Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, RemoteInputHistoryItem.class); 219 addRemoteInputHistoryToMessages(newMessages, history); 220 221 final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); 222 boolean showSpinner = 223 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); 224 225 226 final List<MessagingMessage> historicMessagingMessages = createMessages(newHistoricMessages, 227 /* isHistoric= */true, usePrecomputedText); 228 List<MessagingMessage> newMessagingMessages; 229 mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); 230 if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) { 231 mMessagingLinearLayout.setMaxDisplayedLines(MAX_SUMMARIZATION_LINES); 232 Notification.MessagingStyle.Message summary = 233 new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); 234 newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); 235 } else { 236 newMessagingMessages = 237 createMessages(newMessages, /* isHistoric= */false, usePrecomputedText); 238 } 239 240 // Let's first find our groups! 241 List<List<MessagingMessage>> groups = new ArrayList<>(); 242 List<Person> senders = new ArrayList<>(); 243 244 // Lets first find the groups 245 findGroups(historicMessagingMessages, newMessagingMessages, groups, senders); 246 247 return new MessagingData(user, showSpinner, historicMessagingMessages, newMessagingMessages, 248 groups, senders, mSummarizedContent); 249 } 250 251 /** 252 * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. 253 * This should be called on a background thread, and returns a Runnable which is then must be 254 * called on the main thread to complete the operation and set text. 255 * @param extras Bundle contains messaging data 256 * @hide 257 */ 258 @NonNull setDataAsync(Bundle extras)259 public Runnable setDataAsync(Bundle extras) { 260 if (!mPrecomputedTextEnabled) { 261 return () -> setData(extras); 262 } 263 264 final MessagingData messagingData = 265 parseMessagingData(extras, /* usePrecomputedText= */true); 266 267 return () -> { 268 finalizeInflate(messagingData.getHistoricMessagingMessages()); 269 finalizeInflate(messagingData.getNewMessagingMessages()); 270 bind(messagingData); 271 }; 272 } 273 274 /** 275 * enable/disable precomputed text usage 276 * @hide 277 */ setPrecomputedTextEnabled(boolean precomputedTextEnabled)278 public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) { 279 mPrecomputedTextEnabled = precomputedTextEnabled; 280 } 281 finalizeInflate(List<MessagingMessage> historicMessagingMessages)282 private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) { 283 for (MessagingMessage messagingMessage: historicMessagingMessages) { 284 messagingMessage.finalizeInflate(); 285 } 286 } 287 288 @Override setImageResolver(ImageResolver resolver)289 public void setImageResolver(ImageResolver resolver) { 290 mImageResolver = resolver; 291 } 292 addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)293 private void addRemoteInputHistoryToMessages( 294 List<Notification.MessagingStyle.Message> newMessages, 295 RemoteInputHistoryItem[] remoteInputHistory) { 296 if (remoteInputHistory == null || remoteInputHistory.length == 0) { 297 return; 298 } 299 for (int i = remoteInputHistory.length - 1; i >= 0; i--) { 300 RemoteInputHistoryItem historyMessage = remoteInputHistory[i]; 301 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message( 302 historyMessage.getText(), 0, null, true /* remoteHistory */); 303 if (historyMessage.getUri() != null) { 304 message.setData(historyMessage.getMimeType(), historyMessage.getUri()); 305 } 306 newMessages.add(message); 307 } 308 } 309 bind(MessagingData messagingData)310 private void bind(MessagingData messagingData) { 311 setUser(messagingData.getUser()); 312 313 // Let's now create the views and reorder them accordingly 314 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); 315 createGroupViews(messagingData.getGroups(), messagingData.getSenders(), 316 messagingData.getShowSpinner()); 317 318 // Let's first check which groups were removed altogether and remove them in one animation 319 removeGroups(oldGroups); 320 321 // Let's remove the remaining messages 322 for (MessagingMessage message : mMessages) { 323 message.removeMessage(mToRecycle); 324 } 325 for (MessagingMessage historicMessage : mHistoricMessages) { 326 historicMessage.removeMessage(mToRecycle); 327 } 328 329 mMessages = messagingData.getNewMessagingMessages(); 330 mHistoricMessages = messagingData.getHistoricMessagingMessages(); 331 332 updateHistoricMessageVisibility(); 333 updateTitleAndNamesDisplay(); 334 // after groups are finalized, hide the first sender name if it's showing as the title 335 mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, mConversationTitle); 336 updateImageMessages(); 337 338 // Recycle everything at the end of the update, now that we know it's no longer needed. 339 for (MessagingLinearLayout.MessagingChild child : mToRecycle) { 340 child.recycle(); 341 } 342 mToRecycle.clear(); 343 } 344 updateImageMessages()345 private void updateImageMessages() { 346 if (mImageMessageContainer == null) { 347 return; 348 } 349 View newMessage = getNewImageMessage(); 350 // Remove all messages that don't belong into the image layout 351 View previousMessage = mImageMessageContainer.getChildAt(0); 352 if (previousMessage != newMessage) { 353 mImageMessageContainer.removeView(previousMessage); 354 if (newMessage != null) { 355 mImageMessageContainer.addView(newMessage); 356 } 357 } 358 mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE); 359 360 // When showing an image message, do not show the large icon. Removing the drawable 361 // prevents it from being shown in the left_icon view (by the grouping util). 362 if (newMessage != null && mRightIconView != null && mRightIconView.getDrawable() != null) { 363 mRightIconView.setImageDrawable(null); 364 mRightIconView.setVisibility(GONE); 365 } 366 } 367 368 @Nullable getNewImageMessage()369 private View getNewImageMessage() { 370 if (mIsCollapsed && !mGroups.isEmpty()) { 371 // When collapsed, we're displaying the image message in a dedicated container 372 // on the right of the layout instead of inline. Let's add the isolated image there 373 MessagingGroup messagingGroup = mGroups.getLast(); 374 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); 375 if (isolatedMessage != null) { 376 return isolatedMessage.getView(); 377 } 378 } 379 return null; 380 } 381 removeGroups(ArrayList<MessagingGroup> oldGroups)382 private void removeGroups(ArrayList<MessagingGroup> oldGroups) { 383 int size = oldGroups.size(); 384 for (int i = 0; i < size; i++) { 385 MessagingGroup group = oldGroups.get(i); 386 if (!mGroups.contains(group)) { 387 List<MessagingMessage> messages = group.getMessages(); 388 389 boolean wasShown = group.isShown(); 390 mMessagingLinearLayout.removeView(group); 391 if (wasShown && !MessagingLinearLayout.isGone(group)) { 392 mMessagingLinearLayout.addTransientView(group, 0); 393 group.removeGroupAnimated(() -> { 394 mMessagingLinearLayout.removeTransientView(group); 395 group.recycle(); 396 }); 397 } else { 398 mToRecycle.add(group); 399 } 400 mMessages.removeAll(messages); 401 mHistoricMessages.removeAll(messages); 402 } 403 } 404 } 405 updateTitleAndNamesDisplay()406 private void updateTitleAndNamesDisplay() { 407 Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups); 408 409 // Now that we have the correct symbols, let's look what we have cached 410 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); 411 for (int i = 0; i < mGroups.size(); i++) { 412 // Let's now set the avatars 413 MessagingGroup group = mGroups.get(i); 414 boolean isOwnMessage = group.getSender() == mUser; 415 CharSequence senderName = group.getSenderName(); 416 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName) 417 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) { 418 continue; 419 } 420 String symbol = uniqueNames.get(senderName); 421 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, 422 symbol, mLayoutColor); 423 if (cachedIcon != null) { 424 cachedAvatars.put(senderName, cachedIcon); 425 } 426 } 427 428 for (int i = 0; i < mGroups.size(); i++) { 429 // Let's now set the avatars 430 MessagingGroup group = mGroups.get(i); 431 CharSequence senderName = group.getSenderName(); 432 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 433 continue; 434 } 435 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) { 436 group.setAvatar(mAvatarReplacement); 437 } else { 438 Icon cachedIcon = cachedAvatars.get(senderName); 439 if (cachedIcon == null) { 440 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName), 441 mLayoutColor); 442 cachedAvatars.put(senderName, cachedIcon); 443 } 444 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), 445 mLayoutColor); 446 } 447 } 448 } 449 createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)450 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { 451 return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor); 452 } 453 454 @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") setLayoutColor(int color)455 public void setLayoutColor(int color) { 456 mLayoutColor = color; 457 } 458 459 /** 460 * @hide 461 */ setLayoutColorAsync(int color)462 public Runnable setLayoutColorAsync(int color) { 463 mLayoutColor = color; 464 return () -> {}; 465 } 466 467 @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync") setIsOneToOne(boolean oneToOne)468 public void setIsOneToOne(boolean oneToOne) { 469 mIsOneToOne = oneToOne; 470 } 471 472 /** 473 * @hide 474 */ setIsOneToOneAsync(boolean oneToOne)475 public Runnable setIsOneToOneAsync(boolean oneToOne) { 476 mIsOneToOne = oneToOne; 477 return () -> {}; 478 } 479 480 @RemotableViewMethod(asyncImpl = "setSenderTextColorAsync") setSenderTextColor(int color)481 public void setSenderTextColor(int color) { 482 mSenderTextColor = color; 483 } 484 485 /** 486 * @hide 487 */ setSenderTextColorAsync(int color)488 public Runnable setSenderTextColorAsync(int color) { 489 mSenderTextColor = color; 490 return () -> {}; 491 } 492 /** 493 * @param color the color of the notification background 494 */ 495 @RemotableViewMethod setNotificationBackgroundColor(int color)496 public void setNotificationBackgroundColor(int color) { 497 // Nothing to do with this 498 } 499 500 @RemotableViewMethod(asyncImpl = "setMessageTextColorAsync") setMessageTextColor(int color)501 public void setMessageTextColor(int color) { 502 mMessageTextColor = color; 503 } 504 505 /** 506 * @hide 507 */ setMessageTextColorAsync(int color)508 public Runnable setMessageTextColorAsync(int color) { 509 mMessageTextColor = color; 510 return () -> {}; 511 } 512 setUser(Person user)513 public void setUser(Person user) { 514 mUser = user; 515 if (mUser.getIcon() == null) { 516 Icon userIcon = Icon.createWithResource(getContext(), 517 com.android.internal.R.drawable.messaging_user); 518 userIcon.setTint(mLayoutColor); 519 mUser = mUser.toBuilder().setIcon(userIcon).build(); 520 } 521 } 522 createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)523 private void createGroupViews(List<List<MessagingMessage>> groups, 524 List<Person> senders, boolean showSpinner) { 525 mGroups.clear(); 526 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { 527 List<MessagingMessage> group = groups.get(groupIndex); 528 MessagingGroup newGroup = null; 529 // we'll just take the first group that exists or create one there is none 530 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { 531 MessagingMessage message = group.get(messageIndex); 532 newGroup = message.getGroup(); 533 if (newGroup != null) { 534 break; 535 } 536 } 537 if (newGroup == null) { 538 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); 539 mAddedGroups.add(newGroup); 540 } else if (newGroup.getParent() != mMessagingLinearLayout) { 541 throw new IllegalStateException( 542 "group parent was " + newGroup.getParent() + " but expected " 543 + mMessagingLinearLayout); 544 } 545 newGroup.setImageDisplayLocation(mIsCollapsed 546 ? IMAGE_DISPLAY_LOCATION_EXTERNAL 547 : IMAGE_DISPLAY_LOCATION_INLINE); 548 newGroup.setIsInConversation(false); 549 newGroup.setLayoutColor(mLayoutColor); 550 newGroup.setTextColors(mSenderTextColor, mMessageTextColor); 551 Person sender = senders.get(groupIndex); 552 CharSequence nameOverride = null; 553 if (sender != mUser && mNameReplacement != null) { 554 nameOverride = mNameReplacement; 555 } 556 newGroup.setSingleLine(mIsCollapsed 557 ? !android.app.Flags.nmCollapsedLines() && TextUtils.isEmpty(mSummarizedContent) 558 : false); 559 newGroup.setShowingAvatar(!mIsCollapsed); 560 newGroup.setIsCollapsed(mIsCollapsed); 561 newGroup.setSender(sender, nameOverride); 562 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); 563 mGroups.add(newGroup); 564 565 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { 566 mMessagingLinearLayout.removeView(newGroup); 567 mMessagingLinearLayout.addView(newGroup, groupIndex); 568 } 569 newGroup.setMessages(group); 570 } 571 572 if (Flags.dropNonExistingMessages()) { 573 // remove groups from mAddedGroups when they are no longer in mGroups. 574 mAddedGroups.removeIf( 575 messagingGroup -> !mGroups.contains(messagingGroup)); 576 } 577 } 578 findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)579 private void findGroups(List<MessagingMessage> historicMessages, 580 List<MessagingMessage> messages, List<List<MessagingMessage>> groups, 581 List<Person> senders) { 582 CharSequence currentSenderKey = null; 583 List<MessagingMessage> currentGroup = null; 584 int histSize = historicMessages.size(); 585 for (int i = 0; i < histSize + messages.size(); i++) { 586 MessagingMessage message; 587 if (i < histSize) { 588 message = historicMessages.get(i); 589 } else { 590 message = messages.get(i - histSize); 591 } 592 boolean isNewGroup = currentGroup == null; 593 Person sender = 594 message.getMessage() == null ? null : message.getMessage().getSenderPerson(); 595 CharSequence key = sender == null ? null 596 : sender.getKey() == null ? sender.getName() : sender.getKey(); 597 isNewGroup |= !TextUtils.equals(key, currentSenderKey); 598 if (isNewGroup) { 599 currentGroup = new ArrayList<>(); 600 groups.add(currentGroup); 601 if (sender == null) { 602 sender = mUser; 603 } 604 senders.add(sender); 605 currentSenderKey = key; 606 } 607 currentGroup.add(message); 608 } 609 } 610 611 /** 612 * Creates new messages, reusing existing ones if they are available. 613 * 614 * @param newMessages the messages to parse. 615 */ createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, boolean usePrecomputedText)616 private List<MessagingMessage> createMessages( 617 List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, 618 boolean usePrecomputedText) { 619 List<MessagingMessage> result = new ArrayList<>(); 620 for (int i = 0; i < newMessages.size(); i++) { 621 Notification.MessagingStyle.Message m = newMessages.get(i); 622 MessagingMessage message = findAndRemoveMatchingMessage(m); 623 if (message == null) { 624 message = MessagingMessage.createMessage(this, m, 625 mImageResolver, usePrecomputedText); 626 } 627 message.setIsHistoric(isHistoric); 628 result.add(message); 629 } 630 return result; 631 } 632 findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)633 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { 634 for (int i = 0; i < mMessages.size(); i++) { 635 MessagingMessage existing = mMessages.get(i); 636 if (existing.sameAs(m)) { 637 mMessages.remove(i); 638 return existing; 639 } 640 } 641 for (int i = 0; i < mHistoricMessages.size(); i++) { 642 MessagingMessage existing = mHistoricMessages.get(i); 643 if (existing.sameAs(m)) { 644 mHistoricMessages.remove(i); 645 return existing; 646 } 647 } 648 return null; 649 } 650 showHistoricMessages(boolean show)651 public void showHistoricMessages(boolean show) { 652 mShowHistoricMessages = show; 653 updateHistoricMessageVisibility(); 654 } 655 updateHistoricMessageVisibility()656 private void updateHistoricMessageVisibility() { 657 int numHistoric = mHistoricMessages.size(); 658 for (int i = 0; i < numHistoric; i++) { 659 MessagingMessage existing = mHistoricMessages.get(i); 660 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); 661 } 662 int numGroups = mGroups.size(); 663 for (int i = 0; i < numGroups; i++) { 664 MessagingGroup group = mGroups.get(i); 665 int visibleChildren = 0; 666 List<MessagingMessage> messages = group.getMessages(); 667 int numGroupMessages = messages.size(); 668 for (int j = 0; j < numGroupMessages; j++) { 669 MessagingMessage message = messages.get(j); 670 if (message.getVisibility() != GONE) { 671 visibleChildren++; 672 } 673 } 674 if (visibleChildren > 0 && group.getVisibility() == GONE) { 675 group.setVisibility(VISIBLE); 676 } else if (visibleChildren == 0 && group.getVisibility() != GONE) { 677 group.setVisibility(GONE); 678 } 679 } 680 } 681 682 @Override onLayout(boolean changed, int left, int top, int right, int bottom)683 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 684 super.onLayout(changed, left, top, right, bottom); 685 if (!mAddedGroups.isEmpty()) { 686 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 687 @Override 688 public boolean onPreDraw() { 689 for (MessagingGroup group : mAddedGroups) { 690 if (!group.isShown()) { 691 continue; 692 } 693 MessagingPropertyAnimator.fadeIn(group.getAvatar()); 694 MessagingPropertyAnimator.fadeIn(group.getSenderView()); 695 MessagingPropertyAnimator.startLocalTranslationFrom(group, 696 group.getHeight(), LINEAR_OUT_SLOW_IN); 697 } 698 mAddedGroups.clear(); 699 getViewTreeObserver().removeOnPreDrawListener(this); 700 return true; 701 } 702 }); 703 } 704 } 705 getMessagingLinearLayout()706 public MessagingLinearLayout getMessagingLinearLayout() { 707 return mMessagingLinearLayout; 708 } 709 710 @Nullable getImageMessageContainer()711 public ViewGroup getImageMessageContainer() { 712 return mImageMessageContainer; 713 } 714 getMessagingGroups()715 public ArrayList<MessagingGroup> getMessagingGroups() { 716 return mGroups; 717 } 718 719 @Override setMessagingClippingDisabled(boolean clippingDisabled)720 public void setMessagingClippingDisabled(boolean clippingDisabled) { 721 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect); 722 } 723 } 724