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.Notification; 24 import android.app.Person; 25 import android.content.Context; 26 import android.graphics.Bitmap; 27 import android.graphics.Canvas; 28 import android.graphics.Color; 29 import android.graphics.Paint; 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.ViewTreeObserver; 40 import android.view.animation.Interpolator; 41 import android.view.animation.PathInterpolator; 42 import android.widget.FrameLayout; 43 import android.widget.RemoteViews; 44 import android.widget.TextView; 45 46 import com.android.internal.R; 47 import com.android.internal.graphics.ColorUtils; 48 import com.android.internal.util.ContrastColorUtil; 49 50 import java.util.ArrayList; 51 import java.util.List; 52 import java.util.function.Consumer; 53 import java.util.regex.Pattern; 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 implements ImageMessageConsumer { 61 62 private static final float COLOR_SHIFT_AMOUNT = 60; 63 /** 64 * Pattren for filter some ingonable characters. 65 * p{Z} for any kind of whitespace or invisible separator. 66 * p{C} for any kind of punctuation character. 67 */ 68 private static final Pattern IGNORABLE_CHAR_PATTERN 69 = Pattern.compile("[\\p{C}\\p{Z}]"); 70 private static final Pattern SPECIAL_CHAR_PATTERN 71 = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]"); 72 private static final Consumer<MessagingMessage> REMOVE_MESSAGE 73 = MessagingMessage::removeMessage; 74 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); 75 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 76 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 77 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR 78 = new MessagingPropertyAnimator(); 79 private List<MessagingMessage> mMessages = new ArrayList<>(); 80 private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); 81 private MessagingLinearLayout mMessagingLinearLayout; 82 private boolean mShowHistoricMessages; 83 private ArrayList<MessagingGroup> mGroups = new ArrayList<>(); 84 private TextView mTitleView; 85 private int mLayoutColor; 86 private int mSenderTextColor; 87 private int mMessageTextColor; 88 private int mAvatarSize; 89 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 90 private Paint mTextPaint = new Paint(); 91 private CharSequence mConversationTitle; 92 private Icon mAvatarReplacement; 93 private boolean mIsOneToOne; 94 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); 95 private Person mUser; 96 private CharSequence mNameReplacement; 97 private boolean mDisplayImagesAtEnd; 98 private ImageResolver mImageResolver; 99 MessagingLayout(@onNull Context context)100 public MessagingLayout(@NonNull Context context) { 101 super(context); 102 } 103 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs)104 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 105 super(context, attrs); 106 } 107 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)108 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 109 @AttrRes int defStyleAttr) { 110 super(context, attrs, defStyleAttr); 111 } 112 MessagingLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)113 public MessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, 114 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 115 super(context, attrs, defStyleAttr, defStyleRes); 116 } 117 118 @Override onFinishInflate()119 protected void onFinishInflate() { 120 super.onFinishInflate(); 121 mMessagingLinearLayout = findViewById(R.id.notification_messaging); 122 mMessagingLinearLayout.setMessagingLayout(this); 123 // We still want to clip, but only on the top, since views can temporarily out of bounds 124 // during transitions. 125 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 126 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); 127 Rect rect = new Rect(0, 0, size, size); 128 mMessagingLinearLayout.setClipBounds(rect); 129 mTitleView = findViewById(R.id.title); 130 mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size); 131 mTextPaint.setTextAlign(Paint.Align.CENTER); 132 mTextPaint.setAntiAlias(true); 133 } 134 135 @RemotableViewMethod setAvatarReplacement(Icon icon)136 public void setAvatarReplacement(Icon icon) { 137 mAvatarReplacement = icon; 138 } 139 140 @RemotableViewMethod setNameReplacement(CharSequence nameReplacement)141 public void setNameReplacement(CharSequence nameReplacement) { 142 mNameReplacement = nameReplacement; 143 } 144 145 @RemotableViewMethod setDisplayImagesAtEnd(boolean atEnd)146 public void setDisplayImagesAtEnd(boolean atEnd) { 147 mDisplayImagesAtEnd = atEnd; 148 } 149 150 @RemotableViewMethod setData(Bundle extras)151 public void setData(Bundle extras) { 152 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 153 List<Notification.MessagingStyle.Message> newMessages 154 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 155 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); 156 List<Notification.MessagingStyle.Message> newHistoricMessages 157 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); 158 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON)); 159 mConversationTitle = null; 160 TextView headerText = findViewById(R.id.header_text); 161 if (headerText != null) { 162 mConversationTitle = headerText.getText(); 163 } 164 addRemoteInputHistoryToMessages(newMessages, 165 extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY)); 166 boolean showSpinner = 167 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); 168 bind(newMessages, newHistoricMessages, showSpinner); 169 } 170 171 @Override setImageResolver(ImageResolver resolver)172 public void setImageResolver(ImageResolver resolver) { 173 mImageResolver = resolver; 174 } 175 addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, CharSequence[] remoteInputHistory)176 private void addRemoteInputHistoryToMessages( 177 List<Notification.MessagingStyle.Message> newMessages, 178 CharSequence[] remoteInputHistory) { 179 if (remoteInputHistory == null || remoteInputHistory.length == 0) { 180 return; 181 } 182 for (int i = remoteInputHistory.length - 1; i >= 0; i--) { 183 CharSequence message = remoteInputHistory[i]; 184 newMessages.add(new Notification.MessagingStyle.Message( 185 message, 0, (Person) null, true /* remoteHistory */)); 186 } 187 } 188 bind(List<Notification.MessagingStyle.Message> newMessages, List<Notification.MessagingStyle.Message> newHistoricMessages, boolean showSpinner)189 private void bind(List<Notification.MessagingStyle.Message> newMessages, 190 List<Notification.MessagingStyle.Message> newHistoricMessages, 191 boolean showSpinner) { 192 193 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages, 194 true /* isHistoric */); 195 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */); 196 197 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); 198 addMessagesToGroups(historicMessages, messages, showSpinner); 199 200 // Let's first check which groups were removed altogether and remove them in one animation 201 removeGroups(oldGroups); 202 203 // Let's remove the remaining messages 204 mMessages.forEach(REMOVE_MESSAGE); 205 mHistoricMessages.forEach(REMOVE_MESSAGE); 206 207 mMessages = messages; 208 mHistoricMessages = historicMessages; 209 210 updateHistoricMessageVisibility(); 211 updateTitleAndNamesDisplay(); 212 } 213 removeGroups(ArrayList<MessagingGroup> oldGroups)214 private void removeGroups(ArrayList<MessagingGroup> oldGroups) { 215 int size = oldGroups.size(); 216 for (int i = 0; i < size; i++) { 217 MessagingGroup group = oldGroups.get(i); 218 if (!mGroups.contains(group)) { 219 List<MessagingMessage> messages = group.getMessages(); 220 Runnable endRunnable = () -> { 221 mMessagingLinearLayout.removeTransientView(group); 222 group.recycle(); 223 }; 224 225 boolean wasShown = group.isShown(); 226 mMessagingLinearLayout.removeView(group); 227 if (wasShown && !MessagingLinearLayout.isGone(group)) { 228 mMessagingLinearLayout.addTransientView(group, 0); 229 group.removeGroupAnimated(endRunnable); 230 } else { 231 endRunnable.run(); 232 } 233 mMessages.removeAll(messages); 234 mHistoricMessages.removeAll(messages); 235 } 236 } 237 } 238 updateTitleAndNamesDisplay()239 private void updateTitleAndNamesDisplay() { 240 ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>(); 241 ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>(); 242 for (int i = 0; i < mGroups.size(); i++) { 243 MessagingGroup group = mGroups.get(i); 244 CharSequence senderName = group.getSenderName(); 245 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 246 continue; 247 } 248 if (!uniqueNames.containsKey(senderName)) { 249 // Only use visible characters to get uniqueNames 250 String pureSenderName = IGNORABLE_CHAR_PATTERN 251 .matcher(senderName).replaceAll("" /* replacement */); 252 char c = pureSenderName.charAt(0); 253 if (uniqueCharacters.containsKey(c)) { 254 // this character was already used, lets make it more unique. We first need to 255 // resolve the existing character if it exists 256 CharSequence existingName = uniqueCharacters.get(c); 257 if (existingName != null) { 258 uniqueNames.put(existingName, findNameSplit((String) existingName)); 259 uniqueCharacters.put(c, null); 260 } 261 uniqueNames.put(senderName, findNameSplit((String) senderName)); 262 } else { 263 uniqueNames.put(senderName, Character.toString(c)); 264 uniqueCharacters.put(c, pureSenderName); 265 } 266 } 267 } 268 269 // Now that we have the correct symbols, let's look what we have cached 270 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); 271 for (int i = 0; i < mGroups.size(); i++) { 272 // Let's now set the avatars 273 MessagingGroup group = mGroups.get(i); 274 boolean isOwnMessage = group.getSender() == mUser; 275 CharSequence senderName = group.getSenderName(); 276 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName) 277 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) { 278 continue; 279 } 280 String symbol = uniqueNames.get(senderName); 281 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, 282 symbol, mLayoutColor); 283 if (cachedIcon != null) { 284 cachedAvatars.put(senderName, cachedIcon); 285 } 286 } 287 288 for (int i = 0; i < mGroups.size(); i++) { 289 // Let's now set the avatars 290 MessagingGroup group = mGroups.get(i); 291 CharSequence senderName = group.getSenderName(); 292 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 293 continue; 294 } 295 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) { 296 group.setAvatar(mAvatarReplacement); 297 } else { 298 Icon cachedIcon = cachedAvatars.get(senderName); 299 if (cachedIcon == null) { 300 cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName), 301 mLayoutColor); 302 cachedAvatars.put(senderName, cachedIcon); 303 } 304 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), 305 mLayoutColor); 306 } 307 } 308 } 309 createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor)310 public Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { 311 if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) || 312 SPECIAL_CHAR_PATTERN.matcher(symbol).find()) { 313 Icon avatarIcon = Icon.createWithResource(getContext(), 314 com.android.internal.R.drawable.messaging_user); 315 avatarIcon.setTint(findColor(senderName, layoutColor)); 316 return avatarIcon; 317 } else { 318 Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888); 319 Canvas canvas = new Canvas(bitmap); 320 float radius = mAvatarSize / 2.0f; 321 int color = findColor(senderName, layoutColor); 322 mPaint.setColor(color); 323 canvas.drawCircle(radius, radius, radius, mPaint); 324 boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f; 325 mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE); 326 mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f); 327 int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)); 328 canvas.drawText(symbol, radius, yPos, mTextPaint); 329 return Icon.createWithBitmap(bitmap); 330 } 331 } 332 findColor(CharSequence senderName, int layoutColor)333 private int findColor(CharSequence senderName, int layoutColor) { 334 double luminance = ContrastColorUtil.calculateLuminance(layoutColor); 335 float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f; 336 337 // we need to offset the range if the luminance is too close to the borders 338 shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0); 339 shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0); 340 return ContrastColorUtil.getShiftedColor(layoutColor, 341 (int) (shift * COLOR_SHIFT_AMOUNT)); 342 } 343 findNameSplit(String existingName)344 private String findNameSplit(String existingName) { 345 String[] split = existingName.split(" "); 346 if (split.length > 1) { 347 return Character.toString(split[0].charAt(0)) 348 + Character.toString(split[1].charAt(0)); 349 } 350 return existingName.substring(0, 1); 351 } 352 353 @RemotableViewMethod setLayoutColor(int color)354 public void setLayoutColor(int color) { 355 mLayoutColor = color; 356 } 357 358 @RemotableViewMethod setIsOneToOne(boolean oneToOne)359 public void setIsOneToOne(boolean oneToOne) { 360 mIsOneToOne = oneToOne; 361 } 362 363 @RemotableViewMethod setSenderTextColor(int color)364 public void setSenderTextColor(int color) { 365 mSenderTextColor = color; 366 } 367 368 @RemotableViewMethod setMessageTextColor(int color)369 public void setMessageTextColor(int color) { 370 mMessageTextColor = color; 371 } 372 setUser(Person user)373 public void setUser(Person user) { 374 mUser = user; 375 if (mUser.getIcon() == null) { 376 Icon userIcon = Icon.createWithResource(getContext(), 377 com.android.internal.R.drawable.messaging_user); 378 userIcon.setTint(mLayoutColor); 379 mUser = mUser.toBuilder().setIcon(userIcon).build(); 380 } 381 } 382 addMessagesToGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, boolean showSpinner)383 private void addMessagesToGroups(List<MessagingMessage> historicMessages, 384 List<MessagingMessage> messages, boolean showSpinner) { 385 // Let's first find our groups! 386 List<List<MessagingMessage>> groups = new ArrayList<>(); 387 List<Person> senders = new ArrayList<>(); 388 389 // Lets first find the groups 390 findGroups(historicMessages, messages, groups, senders); 391 392 // Let's now create the views and reorder them accordingly 393 createGroupViews(groups, senders, showSpinner); 394 } 395 createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)396 private void createGroupViews(List<List<MessagingMessage>> groups, 397 List<Person> senders, boolean showSpinner) { 398 mGroups.clear(); 399 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { 400 List<MessagingMessage> group = groups.get(groupIndex); 401 MessagingGroup newGroup = null; 402 // we'll just take the first group that exists or create one there is none 403 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { 404 MessagingMessage message = group.get(messageIndex); 405 newGroup = message.getGroup(); 406 if (newGroup != null) { 407 break; 408 } 409 } 410 if (newGroup == null) { 411 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); 412 mAddedGroups.add(newGroup); 413 } 414 newGroup.setDisplayImagesAtEnd(mDisplayImagesAtEnd); 415 newGroup.setLayoutColor(mLayoutColor); 416 newGroup.setTextColors(mSenderTextColor, mMessageTextColor); 417 Person sender = senders.get(groupIndex); 418 CharSequence nameOverride = null; 419 if (sender != mUser && mNameReplacement != null) { 420 nameOverride = mNameReplacement; 421 } 422 newGroup.setSender(sender, nameOverride); 423 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); 424 mGroups.add(newGroup); 425 426 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { 427 mMessagingLinearLayout.removeView(newGroup); 428 mMessagingLinearLayout.addView(newGroup, groupIndex); 429 } 430 newGroup.setMessages(group); 431 } 432 } 433 findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)434 private void findGroups(List<MessagingMessage> historicMessages, 435 List<MessagingMessage> messages, List<List<MessagingMessage>> groups, 436 List<Person> senders) { 437 CharSequence currentSenderKey = null; 438 List<MessagingMessage> currentGroup = null; 439 int histSize = historicMessages.size(); 440 for (int i = 0; i < histSize + messages.size(); i++) { 441 MessagingMessage message; 442 if (i < histSize) { 443 message = historicMessages.get(i); 444 } else { 445 message = messages.get(i - histSize); 446 } 447 boolean isNewGroup = currentGroup == null; 448 Person sender = message.getMessage().getSenderPerson(); 449 CharSequence key = sender == null ? null 450 : sender.getKey() == null ? sender.getName() : sender.getKey(); 451 isNewGroup |= !TextUtils.equals(key, currentSenderKey); 452 if (isNewGroup) { 453 currentGroup = new ArrayList<>(); 454 groups.add(currentGroup); 455 if (sender == null) { 456 sender = mUser; 457 } 458 senders.add(sender); 459 currentSenderKey = key; 460 } 461 currentGroup.add(message); 462 } 463 } 464 465 /** 466 * Creates new messages, reusing existing ones if they are available. 467 * 468 * @param newMessages the messages to parse. 469 */ createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean historic)470 private List<MessagingMessage> createMessages( 471 List<Notification.MessagingStyle.Message> newMessages, boolean historic) { 472 List<MessagingMessage> result = new ArrayList<>(); 473 for (int i = 0; i < newMessages.size(); i++) { 474 Notification.MessagingStyle.Message m = newMessages.get(i); 475 MessagingMessage message = findAndRemoveMatchingMessage(m); 476 if (message == null) { 477 message = MessagingMessage.createMessage(this, m, mImageResolver); 478 } 479 message.setIsHistoric(historic); 480 result.add(message); 481 } 482 return result; 483 } 484 findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)485 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { 486 for (int i = 0; i < mMessages.size(); i++) { 487 MessagingMessage existing = mMessages.get(i); 488 if (existing.sameAs(m)) { 489 mMessages.remove(i); 490 return existing; 491 } 492 } 493 for (int i = 0; i < mHistoricMessages.size(); i++) { 494 MessagingMessage existing = mHistoricMessages.get(i); 495 if (existing.sameAs(m)) { 496 mHistoricMessages.remove(i); 497 return existing; 498 } 499 } 500 return null; 501 } 502 showHistoricMessages(boolean show)503 public void showHistoricMessages(boolean show) { 504 mShowHistoricMessages = show; 505 updateHistoricMessageVisibility(); 506 } 507 updateHistoricMessageVisibility()508 private void updateHistoricMessageVisibility() { 509 int numHistoric = mHistoricMessages.size(); 510 for (int i = 0; i < numHistoric; i++) { 511 MessagingMessage existing = mHistoricMessages.get(i); 512 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); 513 } 514 int numGroups = mGroups.size(); 515 for (int i = 0; i < numGroups; i++) { 516 MessagingGroup group = mGroups.get(i); 517 int visibleChildren = 0; 518 List<MessagingMessage> messages = group.getMessages(); 519 int numGroupMessages = messages.size(); 520 for (int j = 0; j < numGroupMessages; j++) { 521 MessagingMessage message = messages.get(j); 522 if (message.getVisibility() != GONE) { 523 visibleChildren++; 524 } 525 } 526 if (visibleChildren > 0 && group.getVisibility() == GONE) { 527 group.setVisibility(VISIBLE); 528 } else if (visibleChildren == 0 && group.getVisibility() != GONE) { 529 group.setVisibility(GONE); 530 } 531 } 532 } 533 534 @Override onLayout(boolean changed, int left, int top, int right, int bottom)535 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 536 super.onLayout(changed, left, top, right, bottom); 537 if (!mAddedGroups.isEmpty()) { 538 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 539 @Override 540 public boolean onPreDraw() { 541 for (MessagingGroup group : mAddedGroups) { 542 if (!group.isShown()) { 543 continue; 544 } 545 MessagingPropertyAnimator.fadeIn(group.getAvatar()); 546 MessagingPropertyAnimator.fadeIn(group.getSenderView()); 547 MessagingPropertyAnimator.startLocalTranslationFrom(group, 548 group.getHeight(), LINEAR_OUT_SLOW_IN); 549 } 550 mAddedGroups.clear(); 551 getViewTreeObserver().removeOnPreDrawListener(this); 552 return true; 553 } 554 }); 555 } 556 } 557 getMessagingLinearLayout()558 public MessagingLinearLayout getMessagingLinearLayout() { 559 return mMessagingLinearLayout; 560 } 561 getMessagingGroups()562 public ArrayList<MessagingGroup> getMessagingGroups() { 563 return mGroups; 564 } 565 } 566