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