1 /* 2 * Copyright (C) 2020 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.animation.Animator; 23 import android.animation.AnimatorListenerAdapter; 24 import android.animation.AnimatorSet; 25 import android.animation.ValueAnimator; 26 import android.annotation.AttrRes; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.StyleRes; 30 import android.app.Notification; 31 import android.app.Person; 32 import android.app.RemoteInputHistoryItem; 33 import android.content.Context; 34 import android.content.res.ColorStateList; 35 import android.graphics.Rect; 36 import android.graphics.Typeface; 37 import android.graphics.drawable.GradientDrawable; 38 import android.graphics.drawable.Icon; 39 import android.os.Bundle; 40 import android.os.Parcelable; 41 import android.text.Spannable; 42 import android.text.SpannableString; 43 import android.text.TextUtils; 44 import android.text.style.StyleSpan; 45 import android.util.ArrayMap; 46 import android.util.AttributeSet; 47 import android.util.DisplayMetrics; 48 import android.view.Gravity; 49 import android.view.MotionEvent; 50 import android.view.RemotableViewMethod; 51 import android.view.TouchDelegate; 52 import android.view.View; 53 import android.view.ViewGroup; 54 import android.view.ViewTreeObserver; 55 import android.view.animation.Interpolator; 56 import android.view.animation.PathInterpolator; 57 import android.widget.FrameLayout; 58 import android.widget.ImageView; 59 import android.widget.LinearLayout; 60 import android.widget.RemoteViews; 61 import android.widget.TextView; 62 63 import com.android.internal.R; 64 65 import java.util.ArrayList; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Objects; 69 70 /** 71 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal 72 * messages and adapts the layout accordingly. 73 */ 74 @RemoteViews.RemoteView 75 public class ConversationLayout extends FrameLayout 76 implements ImageMessageConsumer, IMessagingLayout { 77 78 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); 79 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 80 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 81 public static final Interpolator OVERSHOOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f); 82 public static final OnLayoutChangeListener MESSAGING_PROPERTY_ANIMATOR 83 = new MessagingPropertyAnimator(); 84 public static final int IMPORTANCE_ANIM_GROW_DURATION = 250; 85 public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200; 86 public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25; 87 private final PeopleHelper mPeopleHelper = new PeopleHelper(); 88 private List<MessagingMessage> mMessages = new ArrayList<>(); 89 private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); 90 private MessagingLinearLayout mMessagingLinearLayout; 91 private boolean mShowHistoricMessages; 92 private ArrayList<MessagingGroup> mGroups = new ArrayList<>(); 93 private int mLayoutColor; 94 private int mSenderTextColor; 95 private int mMessageTextColor; 96 private Icon mAvatarReplacement; 97 private boolean mIsOneToOne; 98 private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); 99 private Person mUser; 100 private CharSequence mNameReplacement; 101 private boolean mIsCollapsed; 102 private ImageResolver mImageResolver; 103 private CachingIconView mConversationIconView; 104 private View mConversationIconContainer; 105 private int mConversationIconTopPaddingExpandedGroup; 106 private int mConversationIconTopPadding; 107 private int mExpandedGroupMessagePadding; 108 private TextView mConversationText; 109 private View mConversationIconBadge; 110 private CachingIconView mConversationIconBadgeBg; 111 private Icon mLargeIcon; 112 private View mExpandButtonContainer; 113 private ViewGroup mExpandButtonAndContentContainer; 114 private ViewGroup mExpandButtonContainerA11yContainer; 115 private NotificationExpandButton mExpandButton; 116 private MessagingLinearLayout mImageMessageContainer; 117 private int mBadgeProtrusion; 118 private int mConversationAvatarSize; 119 private int mConversationAvatarSizeExpanded; 120 private CachingIconView mIcon; 121 private CachingIconView mImportanceRingView; 122 private int mExpandedGroupBadgeProtrusion; 123 private int mExpandedGroupBadgeProtrusionFacePile; 124 private View mConversationFacePile; 125 private int mNotificationBackgroundColor; 126 private CharSequence mFallbackChatName; 127 private CharSequence mFallbackGroupChatName; 128 private CharSequence mConversationTitle; 129 private int mMessageSpacingStandard; 130 private int mMessageSpacingGroup; 131 private int mNotificationHeaderExpandedPadding; 132 private View mConversationHeader; 133 private View mContentContainer; 134 private boolean mExpandable = true; 135 private int mContentMarginEnd; 136 private Rect mMessagingClipRect; 137 private ObservableTextView mAppName; 138 private NotificationActionListLayout mActions; 139 private boolean mAppNameGone; 140 private int mFacePileAvatarSize; 141 private int mFacePileAvatarSizeExpandedGroup; 142 private int mFacePileProtectionWidth; 143 private int mFacePileProtectionWidthExpanded; 144 private boolean mImportantConversation; 145 private View mFeedbackIcon; 146 private float mMinTouchSize; 147 private Icon mConversationIcon; 148 private Icon mShortcutIcon; 149 private View mAppNameDivider; 150 private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this); 151 private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); 152 ConversationLayout(@onNull Context context)153 public ConversationLayout(@NonNull Context context) { 154 super(context); 155 } 156 ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs)157 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 158 super(context, attrs); 159 } 160 ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)161 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, 162 @AttrRes int defStyleAttr) { 163 super(context, attrs, defStyleAttr); 164 } 165 ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)166 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, 167 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 168 super(context, attrs, defStyleAttr, defStyleRes); 169 } 170 171 @Override onFinishInflate()172 protected void onFinishInflate() { 173 super.onFinishInflate(); 174 mPeopleHelper.init(getContext()); 175 mMessagingLinearLayout = findViewById(R.id.notification_messaging); 176 mActions = findViewById(R.id.actions); 177 mImageMessageContainer = findViewById(R.id.conversation_image_message_container); 178 // We still want to clip, but only on the top, since views can temporarily out of bounds 179 // during transitions. 180 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 181 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); 182 mMessagingClipRect = new Rect(0, 0, size, size); 183 setMessagingClippingDisabled(false); 184 mConversationIconView = findViewById(R.id.conversation_icon); 185 mConversationIconContainer = findViewById(R.id.conversation_icon_container); 186 mIcon = findViewById(R.id.icon); 187 mFeedbackIcon = findViewById(com.android.internal.R.id.feedback); 188 mMinTouchSize = 48 * getResources().getDisplayMetrics().density; 189 mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring); 190 mConversationIconBadge = findViewById(R.id.conversation_icon_badge); 191 mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg); 192 mIcon.setOnVisibilityChangedListener((visibility) -> { 193 194 // Let's hide the background directly or in an animated way 195 boolean isGone = visibility == GONE; 196 int oldVisibility = mConversationIconBadgeBg.getVisibility(); 197 boolean wasGone = oldVisibility == GONE; 198 if (wasGone != isGone) { 199 // Keep the badge gone state in sync with the icon. This is necessary in cases 200 // Where the icon is being hidden externally like in group children. 201 mConversationIconBadgeBg.animate().cancel(); 202 mConversationIconBadgeBg.setVisibility(visibility); 203 } 204 205 // Let's handle the importance ring which can also be be gone normally 206 oldVisibility = mImportanceRingView.getVisibility(); 207 wasGone = oldVisibility == GONE; 208 visibility = !mImportantConversation ? GONE : visibility; 209 boolean isRingGone = visibility == GONE; 210 if (wasGone != isRingGone) { 211 // Keep the badge visibility in sync with the icon. This is necessary in cases 212 // Where the icon is being hidden externally like in group children. 213 mImportanceRingView.animate().cancel(); 214 mImportanceRingView.setVisibility(visibility); 215 } 216 217 oldVisibility = mConversationIconBadge.getVisibility(); 218 wasGone = oldVisibility == GONE; 219 if (wasGone != isGone) { 220 mConversationIconBadge.animate().cancel(); 221 mConversationIconBadge.setVisibility(visibility); 222 } 223 }); 224 // When the small icon is gone, hide the rest of the badge 225 mIcon.setOnForceHiddenChangedListener((forceHidden) -> { 226 mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); 227 mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); 228 }); 229 230 // When the conversation icon is gone, hide the whole badge 231 mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> { 232 mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); 233 mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); 234 mPeopleHelper.animateViewForceHidden(mIcon, forceHidden); 235 }); 236 mConversationText = findViewById(R.id.conversation_text); 237 mExpandButtonContainer = findViewById(R.id.expand_button_container); 238 mExpandButtonContainerA11yContainer = 239 findViewById(R.id.expand_button_a11y_container); 240 mConversationHeader = findViewById(R.id.conversation_header); 241 mContentContainer = findViewById(R.id.notification_action_list_margin_target); 242 mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container); 243 mExpandButton = findViewById(R.id.expand_button); 244 mMessageSpacingStandard = getResources().getDimensionPixelSize( 245 R.dimen.notification_messaging_spacing); 246 mMessageSpacingGroup = getResources().getDimensionPixelSize( 247 R.dimen.notification_messaging_spacing_conversation_group); 248 mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize( 249 R.dimen.conversation_header_expanded_padding_end); 250 mContentMarginEnd = getResources().getDimensionPixelSize( 251 R.dimen.notification_content_margin_end); 252 mBadgeProtrusion = getResources().getDimensionPixelSize( 253 R.dimen.conversation_badge_protrusion); 254 mConversationAvatarSize = getResources().getDimensionPixelSize( 255 R.dimen.conversation_avatar_size); 256 mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize( 257 R.dimen.conversation_avatar_size_group_expanded); 258 mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize( 259 R.dimen.conversation_icon_container_top_padding_small_avatar); 260 mConversationIconTopPadding = getResources().getDimensionPixelSize( 261 R.dimen.conversation_icon_container_top_padding); 262 mExpandedGroupMessagePadding = getResources().getDimensionPixelSize( 263 R.dimen.expanded_group_conversation_message_padding); 264 mExpandedGroupBadgeProtrusion = getResources().getDimensionPixelSize( 265 R.dimen.conversation_badge_protrusion_group_expanded); 266 mExpandedGroupBadgeProtrusionFacePile = getResources().getDimensionPixelSize( 267 R.dimen.conversation_badge_protrusion_group_expanded_face_pile); 268 mConversationFacePile = findViewById(R.id.conversation_face_pile); 269 mFacePileAvatarSize = getResources().getDimensionPixelSize( 270 R.dimen.conversation_face_pile_avatar_size); 271 mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize( 272 R.dimen.conversation_face_pile_avatar_size_group_expanded); 273 mFacePileProtectionWidth = getResources().getDimensionPixelSize( 274 R.dimen.conversation_face_pile_protection_width); 275 mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize( 276 R.dimen.conversation_face_pile_protection_width_expanded); 277 mFallbackChatName = getResources().getString( 278 R.string.conversation_title_fallback_one_to_one); 279 mFallbackGroupChatName = getResources().getString( 280 R.string.conversation_title_fallback_group_chat); 281 mAppName = findViewById(R.id.app_name_text); 282 mAppNameDivider = findViewById(R.id.app_name_divider); 283 mAppNameGone = mAppName.getVisibility() == GONE; 284 mAppName.setOnVisibilityChangedListener((visibility) -> { 285 onAppNameVisibilityChanged(); 286 }); 287 } 288 289 @RemotableViewMethod setAvatarReplacement(Icon icon)290 public void setAvatarReplacement(Icon icon) { 291 mAvatarReplacement = icon; 292 } 293 294 @RemotableViewMethod setNameReplacement(CharSequence nameReplacement)295 public void setNameReplacement(CharSequence nameReplacement) { 296 mNameReplacement = nameReplacement; 297 } 298 299 /** Sets this conversation as "important", adding some additional UI treatment. */ 300 @RemotableViewMethod setIsImportantConversation(boolean isImportantConversation)301 public void setIsImportantConversation(boolean isImportantConversation) { 302 setIsImportantConversation(isImportantConversation, false); 303 } 304 305 /** @hide **/ setIsImportantConversation(boolean isImportantConversation, boolean animate)306 public void setIsImportantConversation(boolean isImportantConversation, boolean animate) { 307 mImportantConversation = isImportantConversation; 308 mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE 309 ? VISIBLE : GONE); 310 311 if (animate && isImportantConversation) { 312 GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable(); 313 ring.mutate(); 314 GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable(); 315 bg.mutate(); 316 int ringColor = getResources() 317 .getColor(R.color.conversation_important_highlight); 318 int standardThickness = getResources() 319 .getDimensionPixelSize(R.dimen.importance_ring_stroke_width); 320 int largeThickness = getResources() 321 .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width); 322 int standardSize = getResources().getDimensionPixelSize( 323 R.dimen.importance_ring_size); 324 int baseSize = standardSize - standardThickness * 2; 325 int bgSize = getResources() 326 .getDimensionPixelSize(R.dimen.conversation_icon_size_badged); 327 328 ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> { 329 int strokeWidth = Math.round((float) animation.getAnimatedValue()); 330 ring.setStroke(strokeWidth, ringColor); 331 int newSize = baseSize + strokeWidth * 2; 332 ring.setSize(newSize, newSize); 333 mImportanceRingView.invalidate(); 334 }; 335 336 ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness); 337 growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN); 338 growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION); 339 growAnimation.addUpdateListener(animatorUpdateListener); 340 341 ValueAnimator shrinkAnimation = 342 ValueAnimator.ofFloat(largeThickness, standardThickness); 343 shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION); 344 shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY); 345 shrinkAnimation.setInterpolator(OVERSHOOT); 346 shrinkAnimation.addUpdateListener(animatorUpdateListener); 347 shrinkAnimation.addListener(new AnimatorListenerAdapter() { 348 @Override 349 public void onAnimationStart(Animator animation) { 350 // Shrink the badge bg so that it doesn't peek behind the animation 351 bg.setSize(baseSize, baseSize); 352 mConversationIconBadgeBg.invalidate(); 353 } 354 355 @Override 356 public void onAnimationEnd(Animator animation) { 357 // Reset bg back to normal size 358 bg.setSize(bgSize, bgSize); 359 mConversationIconBadgeBg.invalidate(); 360 } 361 }); 362 363 AnimatorSet anims = new AnimatorSet(); 364 anims.playSequentially(growAnimation, shrinkAnimation); 365 anims.start(); 366 } 367 } 368 isImportantConversation()369 public boolean isImportantConversation() { 370 return mImportantConversation; 371 } 372 373 /** 374 * Set this layout to show the collapsed representation. 375 * 376 * @param isCollapsed is it collapsed 377 */ 378 @RemotableViewMethod setIsCollapsed(boolean isCollapsed)379 public void setIsCollapsed(boolean isCollapsed) { 380 mIsCollapsed = isCollapsed; 381 mMessagingLinearLayout.setMaxDisplayedLines(isCollapsed ? 1 : Integer.MAX_VALUE); 382 updateExpandButton(); 383 updateContentEndPaddings(); 384 } 385 386 @RemotableViewMethod setData(Bundle extras)387 public void setData(Bundle extras) { 388 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 389 List<Notification.MessagingStyle.Message> newMessages 390 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 391 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); 392 List<Notification.MessagingStyle.Message> newHistoricMessages 393 = Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); 394 395 // mUser now set (would be nice to avoid the side effect but WHATEVER) 396 setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON)); 397 398 // Append remote input history to newMessages (again, side effect is lame but WHATEVS) 399 RemoteInputHistoryItem[] history = (RemoteInputHistoryItem[]) 400 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); 401 addRemoteInputHistoryToMessages(newMessages, history); 402 403 boolean showSpinner = 404 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); 405 // bind it, baby 406 bind(newMessages, newHistoricMessages, showSpinner); 407 408 int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); 409 setUnreadCount(unreadCount); 410 } 411 412 @Override setImageResolver(ImageResolver resolver)413 public void setImageResolver(ImageResolver resolver) { 414 mImageResolver = resolver; 415 } 416 417 /** @hide */ setUnreadCount(int unreadCount)418 public void setUnreadCount(int unreadCount) { 419 mExpandButton.setNumber(unreadCount); 420 } 421 addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)422 private void addRemoteInputHistoryToMessages( 423 List<Notification.MessagingStyle.Message> newMessages, 424 RemoteInputHistoryItem[] remoteInputHistory) { 425 if (remoteInputHistory == null || remoteInputHistory.length == 0) { 426 return; 427 } 428 for (int i = remoteInputHistory.length - 1; i >= 0; i--) { 429 RemoteInputHistoryItem historyMessage = remoteInputHistory[i]; 430 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message( 431 historyMessage.getText(), 0, (Person) null, true /* remoteHistory */); 432 if (historyMessage.getUri() != null) { 433 message.setData(historyMessage.getMimeType(), historyMessage.getUri()); 434 } 435 newMessages.add(message); 436 } 437 } 438 bind(List<Notification.MessagingStyle.Message> newMessages, List<Notification.MessagingStyle.Message> newHistoricMessages, boolean showSpinner)439 private void bind(List<Notification.MessagingStyle.Message> newMessages, 440 List<Notification.MessagingStyle.Message> newHistoricMessages, 441 boolean showSpinner) { 442 // convert MessagingStyle.Message to MessagingMessage, re-using ones from a previous binding 443 // if they exist 444 List<MessagingMessage> historicMessages = createMessages(newHistoricMessages, 445 true /* isHistoric */); 446 List<MessagingMessage> messages = createMessages(newMessages, false /* isHistoric */); 447 448 // Copy our groups, before they get clobbered 449 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); 450 451 // Add our new MessagingMessages to groups 452 List<List<MessagingMessage>> groups = new ArrayList<>(); 453 List<Person> senders = new ArrayList<>(); 454 455 // Lets first find the groups (populate `groups` and `senders`) 456 findGroups(historicMessages, messages, groups, senders); 457 458 // Let's now create the views and reorder them accordingly 459 // side-effect: updates mGroups, mAddedGroups 460 createGroupViews(groups, senders, showSpinner); 461 462 // Let's first check which groups were removed altogether and remove them in one animation 463 removeGroups(oldGroups); 464 465 // Let's remove the remaining messages 466 for (MessagingMessage message : mMessages) { 467 message.removeMessage(mToRecycle); 468 } 469 for (MessagingMessage historicMessage : mHistoricMessages) { 470 historicMessage.removeMessage(mToRecycle); 471 } 472 473 mMessages = messages; 474 mHistoricMessages = historicMessages; 475 476 updateHistoricMessageVisibility(); 477 updateTitleAndNamesDisplay(); 478 479 updateConversationLayout(); 480 481 // Recycle everything at the end of the update, now that we know it's no longer needed. 482 for (MessagingLinearLayout.MessagingChild child : mToRecycle) { 483 child.recycle(); 484 } 485 mToRecycle.clear(); 486 } 487 488 /** 489 * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc); 490 */ updateConversationLayout()491 private void updateConversationLayout() { 492 // Set avatar and name 493 CharSequence conversationText = mConversationTitle; 494 mConversationIcon = mShortcutIcon; 495 if (mIsOneToOne) { 496 // Let's resolve the icon / text from the last sender 497 CharSequence userKey = getKey(mUser); 498 for (int i = mGroups.size() - 1; i >= 0; i--) { 499 MessagingGroup messagingGroup = mGroups.get(i); 500 Person messageSender = messagingGroup.getSender(); 501 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender))) 502 || i == 0) { 503 if (TextUtils.isEmpty(conversationText)) { 504 // We use the sendername as header text if no conversation title is provided 505 // (This usually happens for most 1:1 conversations) 506 conversationText = messagingGroup.getSenderName(); 507 } 508 if (mConversationIcon == null) { 509 Icon avatarIcon = messagingGroup.getAvatarIcon(); 510 if (avatarIcon == null) { 511 avatarIcon = mPeopleHelper.createAvatarSymbol(conversationText, "", 512 mLayoutColor); 513 } 514 mConversationIcon = avatarIcon; 515 } 516 break; 517 } 518 } 519 } 520 if (mConversationIcon == null) { 521 mConversationIcon = mLargeIcon; 522 } 523 if (mIsOneToOne || mConversationIcon != null) { 524 mConversationIconView.setVisibility(VISIBLE); 525 mConversationFacePile.setVisibility(GONE); 526 mConversationIconView.setImageIcon(mConversationIcon); 527 } else { 528 mConversationIconView.setVisibility(GONE); 529 // This will also inflate it! 530 mConversationFacePile.setVisibility(VISIBLE); 531 // rebind the value to the inflated view instead of the stub 532 mConversationFacePile = findViewById(R.id.conversation_face_pile); 533 bindFacePile(); 534 } 535 if (TextUtils.isEmpty(conversationText)) { 536 conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName; 537 } 538 mConversationText.setText(conversationText); 539 // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) 540 // This needs to happen after all of the above o update all of the groups 541 mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); 542 updateAppName(); 543 updateIconPositionAndSize(); 544 updateImageMessages(); 545 updatePaddingsBasedOnContentAvailability(); 546 updateActionListPadding(); 547 updateAppNameDividerVisibility(); 548 } 549 updateActionListPadding()550 private void updateActionListPadding() { 551 if (mActions != null) { 552 mActions.setCollapsibleIndentDimen(R.dimen.call_notification_collapsible_indent); 553 } 554 } 555 updateImageMessages()556 private void updateImageMessages() { 557 View newMessage = null; 558 if (mIsCollapsed && mGroups.size() > 0) { 559 560 // When collapsed, we're displaying the image message in a dedicated container 561 // on the right of the layout instead of inline. Let's add the isolated image there 562 MessagingGroup messagingGroup = mGroups.get(mGroups.size() -1); 563 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); 564 if (isolatedMessage != null) { 565 newMessage = isolatedMessage.getView(); 566 } 567 } 568 // Remove all messages that don't belong into the image layout 569 View previousMessage = mImageMessageContainer.getChildAt(0); 570 if (previousMessage != newMessage) { 571 mImageMessageContainer.removeView(previousMessage); 572 if (newMessage != null) { 573 mImageMessageContainer.addView(newMessage); 574 } 575 } 576 mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE); 577 } 578 bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView)579 public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) { 580 applyNotificationBackgroundColor(bottomBackground); 581 // Let's find the two last conversations: 582 Icon secondLastIcon = null; 583 CharSequence lastKey = null; 584 Icon lastIcon = null; 585 CharSequence userKey = getKey(mUser); 586 for (int i = mGroups.size() - 1; i >= 0; i--) { 587 MessagingGroup messagingGroup = mGroups.get(i); 588 Person messageSender = messagingGroup.getSender(); 589 boolean notUser = messageSender != null 590 && !TextUtils.equals(userKey, getKey(messageSender)); 591 boolean notIncluded = messageSender != null 592 && !TextUtils.equals(lastKey, getKey(messageSender)); 593 if ((notUser && notIncluded) 594 || (i == 0 && lastKey == null)) { 595 if (lastIcon == null) { 596 lastIcon = messagingGroup.getAvatarIcon(); 597 lastKey = getKey(messageSender); 598 } else { 599 secondLastIcon = messagingGroup.getAvatarIcon(); 600 break; 601 } 602 } 603 } 604 if (lastIcon == null) { 605 lastIcon = mPeopleHelper.createAvatarSymbol(" ", "", mLayoutColor); 606 } 607 bottomView.setImageIcon(lastIcon); 608 if (secondLastIcon == null) { 609 secondLastIcon = mPeopleHelper.createAvatarSymbol("", "", mLayoutColor); 610 } 611 topView.setImageIcon(secondLastIcon); 612 } 613 bindFacePile()614 private void bindFacePile() { 615 ImageView bottomBackground = mConversationFacePile.findViewById( 616 R.id.conversation_face_pile_bottom_background); 617 ImageView bottomView = mConversationFacePile.findViewById( 618 R.id.conversation_face_pile_bottom); 619 ImageView topView = mConversationFacePile.findViewById( 620 R.id.conversation_face_pile_top); 621 622 bindFacePile(bottomBackground, bottomView, topView); 623 624 int conversationAvatarSize; 625 int facepileAvatarSize; 626 int facePileBackgroundSize; 627 if (mIsCollapsed) { 628 conversationAvatarSize = mConversationAvatarSize; 629 facepileAvatarSize = mFacePileAvatarSize; 630 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth; 631 } else { 632 conversationAvatarSize = mConversationAvatarSizeExpanded; 633 facepileAvatarSize = mFacePileAvatarSizeExpandedGroup; 634 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded; 635 } 636 LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams(); 637 layoutParams.width = conversationAvatarSize; 638 layoutParams.height = conversationAvatarSize; 639 mConversationFacePile.setLayoutParams(layoutParams); 640 641 layoutParams = (LayoutParams) bottomView.getLayoutParams(); 642 layoutParams.width = facepileAvatarSize; 643 layoutParams.height = facepileAvatarSize; 644 bottomView.setLayoutParams(layoutParams); 645 646 layoutParams = (LayoutParams) topView.getLayoutParams(); 647 layoutParams.width = facepileAvatarSize; 648 layoutParams.height = facepileAvatarSize; 649 topView.setLayoutParams(layoutParams); 650 651 layoutParams = (LayoutParams) bottomBackground.getLayoutParams(); 652 layoutParams.width = facePileBackgroundSize; 653 layoutParams.height = facePileBackgroundSize; 654 bottomBackground.setLayoutParams(layoutParams); 655 } 656 updateAppName()657 private void updateAppName() { 658 mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE); 659 } 660 shouldHideAppName()661 public boolean shouldHideAppName() { 662 return mIsCollapsed; 663 } 664 665 /** 666 * update the icon position and sizing 667 */ updateIconPositionAndSize()668 private void updateIconPositionAndSize() { 669 int badgeProtrusion; 670 int conversationAvatarSize; 671 if (mIsOneToOne || mIsCollapsed) { 672 badgeProtrusion = mBadgeProtrusion; 673 conversationAvatarSize = mConversationAvatarSize; 674 } else { 675 badgeProtrusion = mConversationFacePile.getVisibility() == VISIBLE 676 ? mExpandedGroupBadgeProtrusionFacePile 677 : mExpandedGroupBadgeProtrusion; 678 conversationAvatarSize = mConversationAvatarSizeExpanded; 679 } 680 681 if (mConversationIconView.getVisibility() == VISIBLE) { 682 LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams(); 683 layoutParams.width = conversationAvatarSize; 684 layoutParams.height = conversationAvatarSize; 685 layoutParams.leftMargin = badgeProtrusion; 686 layoutParams.rightMargin = badgeProtrusion; 687 layoutParams.bottomMargin = badgeProtrusion; 688 mConversationIconView.setLayoutParams(layoutParams); 689 } 690 691 if (mConversationFacePile.getVisibility() == VISIBLE) { 692 LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams(); 693 layoutParams.leftMargin = badgeProtrusion; 694 layoutParams.rightMargin = badgeProtrusion; 695 layoutParams.bottomMargin = badgeProtrusion; 696 mConversationFacePile.setLayoutParams(layoutParams); 697 } 698 } 699 updatePaddingsBasedOnContentAvailability()700 private void updatePaddingsBasedOnContentAvailability() { 701 // groups have avatars that need more spacing 702 mMessagingLinearLayout.setSpacing( 703 mIsOneToOne ? mMessageSpacingStandard : mMessageSpacingGroup); 704 705 int messagingPadding = mIsOneToOne || mIsCollapsed 706 ? 0 707 // Add some extra padding to the messages, since otherwise it will overlap with the 708 // group 709 : mExpandedGroupMessagePadding; 710 711 int iconPadding = mIsOneToOne || mIsCollapsed 712 ? mConversationIconTopPadding 713 : mConversationIconTopPaddingExpandedGroup; 714 715 mConversationIconContainer.setPaddingRelative( 716 mConversationIconContainer.getPaddingStart(), 717 iconPadding, 718 mConversationIconContainer.getPaddingEnd(), 719 mConversationIconContainer.getPaddingBottom()); 720 721 mMessagingLinearLayout.setPaddingRelative( 722 mMessagingLinearLayout.getPaddingStart(), 723 messagingPadding, 724 mMessagingLinearLayout.getPaddingEnd(), 725 mMessagingLinearLayout.getPaddingBottom()); 726 } 727 728 @RemotableViewMethod setLargeIcon(Icon largeIcon)729 public void setLargeIcon(Icon largeIcon) { 730 mLargeIcon = largeIcon; 731 } 732 733 @RemotableViewMethod setShortcutIcon(Icon shortcutIcon)734 public void setShortcutIcon(Icon shortcutIcon) { 735 mShortcutIcon = shortcutIcon; 736 } 737 738 /** 739 * Sets the conversation title of this conversation. 740 * 741 * @param conversationTitle the conversation title 742 */ 743 @RemotableViewMethod setConversationTitle(CharSequence conversationTitle)744 public void setConversationTitle(CharSequence conversationTitle) { 745 // Remove formatting from the title. 746 mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; 747 } 748 getConversationTitle()749 public CharSequence getConversationTitle() { 750 return mConversationText.getText(); 751 } 752 removeGroups(ArrayList<MessagingGroup> oldGroups)753 private void removeGroups(ArrayList<MessagingGroup> oldGroups) { 754 int size = oldGroups.size(); 755 for (int i = 0; i < size; i++) { 756 MessagingGroup group = oldGroups.get(i); 757 if (!mGroups.contains(group)) { 758 List<MessagingMessage> messages = group.getMessages(); 759 boolean wasShown = group.isShown(); 760 mMessagingLinearLayout.removeView(group); 761 if (wasShown && !MessagingLinearLayout.isGone(group)) { 762 mMessagingLinearLayout.addTransientView(group, 0); 763 group.removeGroupAnimated(() -> { 764 mMessagingLinearLayout.removeTransientView(group); 765 group.recycle(); 766 }); 767 } else { 768 // Defer recycling until after the update is done, since we may still need the 769 // old group around to perform other updates. 770 mToRecycle.add(group); 771 } 772 mMessages.removeAll(messages); 773 mHistoricMessages.removeAll(messages); 774 } 775 } 776 } 777 updateTitleAndNamesDisplay()778 private void updateTitleAndNamesDisplay() { 779 // Map of unique names to their prefix 780 Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups); 781 782 // Now that we have the correct symbols, let's look what we have cached 783 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); 784 for (int i = 0; i < mGroups.size(); i++) { 785 // Let's now set the avatars 786 MessagingGroup group = mGroups.get(i); 787 boolean isOwnMessage = group.getSender() == mUser; 788 CharSequence senderName = group.getSenderName(); 789 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName) 790 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) { 791 continue; 792 } 793 String symbol = uniqueNames.get(senderName); 794 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, 795 symbol, mLayoutColor); 796 if (cachedIcon != null) { 797 cachedAvatars.put(senderName, cachedIcon); 798 } 799 } 800 801 for (int i = 0; i < mGroups.size(); i++) { 802 // Let's now set the avatars 803 MessagingGroup group = mGroups.get(i); 804 CharSequence senderName = group.getSenderName(); 805 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 806 continue; 807 } 808 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) { 809 group.setAvatar(mAvatarReplacement); 810 } else { 811 Icon cachedIcon = cachedAvatars.get(senderName); 812 if (cachedIcon == null) { 813 cachedIcon = mPeopleHelper.createAvatarSymbol(senderName, 814 uniqueNames.get(senderName), mLayoutColor); 815 cachedAvatars.put(senderName, cachedIcon); 816 } 817 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), 818 mLayoutColor); 819 } 820 } 821 } 822 823 @RemotableViewMethod setLayoutColor(int color)824 public void setLayoutColor(int color) { 825 mLayoutColor = color; 826 } 827 828 @RemotableViewMethod setIsOneToOne(boolean oneToOne)829 public void setIsOneToOne(boolean oneToOne) { 830 mIsOneToOne = oneToOne; 831 } 832 833 @RemotableViewMethod setSenderTextColor(int color)834 public void setSenderTextColor(int color) { 835 mSenderTextColor = color; 836 mConversationText.setTextColor(color); 837 } 838 839 /** 840 * @param color the color of the notification background 841 */ 842 @RemotableViewMethod setNotificationBackgroundColor(int color)843 public void setNotificationBackgroundColor(int color) { 844 mNotificationBackgroundColor = color; 845 applyNotificationBackgroundColor(mConversationIconBadgeBg); 846 } 847 applyNotificationBackgroundColor(ImageView view)848 private void applyNotificationBackgroundColor(ImageView view) { 849 view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor)); 850 } 851 852 @RemotableViewMethod setMessageTextColor(int color)853 public void setMessageTextColor(int color) { 854 mMessageTextColor = color; 855 } 856 setUser(Person user)857 private void setUser(Person user) { 858 mUser = user; 859 if (mUser.getIcon() == null) { 860 Icon userIcon = Icon.createWithResource(getContext(), 861 R.drawable.messaging_user); 862 userIcon.setTint(mLayoutColor); 863 mUser = mUser.toBuilder().setIcon(userIcon).build(); 864 } 865 } 866 createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)867 private void createGroupViews(List<List<MessagingMessage>> groups, 868 List<Person> senders, boolean showSpinner) { 869 mGroups.clear(); 870 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { 871 List<MessagingMessage> group = groups.get(groupIndex); 872 MessagingGroup newGroup = null; 873 // we'll just take the first group that exists or create one there is none 874 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { 875 MessagingMessage message = group.get(messageIndex); 876 newGroup = message.getGroup(); 877 if (newGroup != null) { 878 break; 879 } 880 } 881 // Create a new group, adding it to the linear layout as well 882 if (newGroup == null) { 883 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); 884 mAddedGroups.add(newGroup); 885 } else if (newGroup.getParent() != mMessagingLinearLayout) { 886 throw new IllegalStateException( 887 "group parent was " + newGroup.getParent() + " but expected " 888 + mMessagingLinearLayout); 889 } 890 newGroup.setImageDisplayLocation(mIsCollapsed 891 ? IMAGE_DISPLAY_LOCATION_EXTERNAL 892 : IMAGE_DISPLAY_LOCATION_INLINE); 893 newGroup.setIsInConversation(true); 894 newGroup.setLayoutColor(mLayoutColor); 895 newGroup.setTextColors(mSenderTextColor, mMessageTextColor); 896 Person sender = senders.get(groupIndex); 897 CharSequence nameOverride = null; 898 if (sender != mUser && mNameReplacement != null) { 899 nameOverride = mNameReplacement; 900 } 901 newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed); 902 newGroup.setSingleLine(mIsCollapsed); 903 newGroup.setSender(sender, nameOverride); 904 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); 905 mGroups.add(newGroup); 906 907 // Reposition to the correct place (if we're re-using a group) 908 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { 909 mMessagingLinearLayout.removeView(newGroup); 910 mMessagingLinearLayout.addView(newGroup, groupIndex); 911 } 912 newGroup.setMessages(group); 913 } 914 } 915 findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, List<List<MessagingMessage>> groups, List<Person> senders)916 private void findGroups(List<MessagingMessage> historicMessages, 917 List<MessagingMessage> messages, List<List<MessagingMessage>> groups, 918 List<Person> senders) { 919 CharSequence currentSenderKey = null; 920 List<MessagingMessage> currentGroup = null; 921 int histSize = historicMessages.size(); 922 for (int i = 0; i < histSize + messages.size(); i++) { 923 MessagingMessage message; 924 if (i < histSize) { 925 message = historicMessages.get(i); 926 } else { 927 message = messages.get(i - histSize); 928 } 929 boolean isNewGroup = currentGroup == null; 930 Person sender = message.getMessage().getSenderPerson(); 931 CharSequence key = getKey(sender); 932 isNewGroup |= !TextUtils.equals(key, currentSenderKey); 933 if (isNewGroup) { 934 currentGroup = new ArrayList<>(); 935 groups.add(currentGroup); 936 if (sender == null) { 937 sender = mUser; 938 } else { 939 // Remove all formatting from the sender name 940 sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build(); 941 } 942 senders.add(sender); 943 currentSenderKey = key; 944 } 945 currentGroup.add(message); 946 } 947 } 948 getKey(Person person)949 private CharSequence getKey(Person person) { 950 return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); 951 } 952 953 /** 954 * Creates new messages, reusing existing ones if they are available. 955 * 956 * @param newMessages the messages to parse. 957 */ createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean historic)958 private List<MessagingMessage> createMessages( 959 List<Notification.MessagingStyle.Message> newMessages, boolean historic) { 960 List<MessagingMessage> result = new ArrayList<>(); 961 for (int i = 0; i < newMessages.size(); i++) { 962 Notification.MessagingStyle.Message m = newMessages.get(i); 963 MessagingMessage message = findAndRemoveMatchingMessage(m); 964 if (message == null) { 965 message = MessagingMessage.createMessage(this, m, mImageResolver); 966 } 967 message.setIsHistoric(historic); 968 result.add(message); 969 } 970 return result; 971 } 972 findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)973 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { 974 for (int i = 0; i < mMessages.size(); i++) { 975 MessagingMessage existing = mMessages.get(i); 976 if (existing.sameAs(m)) { 977 mMessages.remove(i); 978 return existing; 979 } 980 } 981 for (int i = 0; i < mHistoricMessages.size(); i++) { 982 MessagingMessage existing = mHistoricMessages.get(i); 983 if (existing.sameAs(m)) { 984 mHistoricMessages.remove(i); 985 return existing; 986 } 987 } 988 return null; 989 } 990 showHistoricMessages(boolean show)991 public void showHistoricMessages(boolean show) { 992 mShowHistoricMessages = show; 993 updateHistoricMessageVisibility(); 994 } 995 updateHistoricMessageVisibility()996 private void updateHistoricMessageVisibility() { 997 int numHistoric = mHistoricMessages.size(); 998 for (int i = 0; i < numHistoric; i++) { 999 MessagingMessage existing = mHistoricMessages.get(i); 1000 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); 1001 } 1002 int numGroups = mGroups.size(); 1003 for (int i = 0; i < numGroups; i++) { 1004 MessagingGroup group = mGroups.get(i); 1005 int visibleChildren = 0; 1006 List<MessagingMessage> messages = group.getMessages(); 1007 int numGroupMessages = messages.size(); 1008 for (int j = 0; j < numGroupMessages; j++) { 1009 MessagingMessage message = messages.get(j); 1010 if (message.getVisibility() != GONE) { 1011 visibleChildren++; 1012 } 1013 } 1014 if (visibleChildren > 0 && group.getVisibility() == GONE) { 1015 group.setVisibility(VISIBLE); 1016 } else if (visibleChildren == 0 && group.getVisibility() != GONE) { 1017 group.setVisibility(GONE); 1018 } 1019 } 1020 } 1021 1022 @Override onLayout(boolean changed, int left, int top, int right, int bottom)1023 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1024 super.onLayout(changed, left, top, right, bottom); 1025 if (!mAddedGroups.isEmpty()) { 1026 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1027 @Override 1028 public boolean onPreDraw() { 1029 for (MessagingGroup group : mAddedGroups) { 1030 if (!group.isShown()) { 1031 continue; 1032 } 1033 MessagingPropertyAnimator.fadeIn(group.getAvatar()); 1034 MessagingPropertyAnimator.fadeIn(group.getSenderView()); 1035 MessagingPropertyAnimator.startLocalTranslationFrom(group, 1036 group.getHeight(), LINEAR_OUT_SLOW_IN); 1037 } 1038 mAddedGroups.clear(); 1039 getViewTreeObserver().removeOnPreDrawListener(this); 1040 return true; 1041 } 1042 }); 1043 } 1044 mTouchDelegate.clear(); 1045 if (mFeedbackIcon.getVisibility() == VISIBLE) { 1046 float width = Math.max(mMinTouchSize, mFeedbackIcon.getWidth()); 1047 float height = Math.max(mMinTouchSize, mFeedbackIcon.getHeight()); 1048 final Rect feedbackTouchRect = new Rect(); 1049 feedbackTouchRect.left = (int) ((mFeedbackIcon.getLeft() + mFeedbackIcon.getRight()) 1050 / 2.0f - width / 2.0f); 1051 feedbackTouchRect.top = (int) ((mFeedbackIcon.getTop() + mFeedbackIcon.getBottom()) 1052 / 2.0f - height / 2.0f); 1053 feedbackTouchRect.bottom = (int) (feedbackTouchRect.top + height); 1054 feedbackTouchRect.right = (int) (feedbackTouchRect.left + width); 1055 1056 getRelativeTouchRect(feedbackTouchRect, mFeedbackIcon); 1057 mTouchDelegate.add(new TouchDelegate(feedbackTouchRect, mFeedbackIcon)); 1058 } 1059 1060 setTouchDelegate(mTouchDelegate); 1061 } 1062 getRelativeTouchRect(Rect touchRect, View view)1063 private void getRelativeTouchRect(Rect touchRect, View view) { 1064 ViewGroup viewGroup = (ViewGroup) view.getParent(); 1065 while (viewGroup != this) { 1066 touchRect.offset(viewGroup.getLeft(), viewGroup.getTop()); 1067 viewGroup = (ViewGroup) viewGroup.getParent(); 1068 } 1069 } 1070 getMessagingLinearLayout()1071 public MessagingLinearLayout getMessagingLinearLayout() { 1072 return mMessagingLinearLayout; 1073 } 1074 getImageMessageContainer()1075 public @NonNull ViewGroup getImageMessageContainer() { 1076 return mImageMessageContainer; 1077 } 1078 getMessagingGroups()1079 public ArrayList<MessagingGroup> getMessagingGroups() { 1080 return mGroups; 1081 } 1082 updateExpandButton()1083 private void updateExpandButton() { 1084 int buttonGravity; 1085 ViewGroup newContainer; 1086 if (mIsCollapsed) { 1087 buttonGravity = Gravity.CENTER; 1088 // NOTE(b/182474419): In order for the touch target of the expand button to be the full 1089 // height of the notification, we would want the mExpandButtonContainer's height to be 1090 // set to WRAP_CONTENT (or 88dp) when in the collapsed state. Unfortunately, that 1091 // causes an unstable remeasuring infinite loop when the unread count is visible, 1092 // causing the layout to occasionally hide the messages. As an aside, that naive 1093 // solution also causes an undesirably large gap between content and smart replies. 1094 newContainer = mExpandButtonAndContentContainer; 1095 } else { 1096 buttonGravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; 1097 newContainer = mExpandButtonContainerA11yContainer; 1098 } 1099 mExpandButton.setExpanded(!mIsCollapsed); 1100 1101 // We need to make sure that the expand button is in the linearlayout pushing over the 1102 // content when collapsed, but allows the content to flow under it when expanded. 1103 if (newContainer != mExpandButtonContainer.getParent()) { 1104 ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer); 1105 newContainer.addView(mExpandButtonContainer); 1106 } 1107 1108 // update if the expand button is centered 1109 LinearLayout.LayoutParams layoutParams = 1110 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams(); 1111 layoutParams.gravity = buttonGravity; 1112 mExpandButton.setLayoutParams(layoutParams); 1113 } 1114 updateContentEndPaddings()1115 private void updateContentEndPaddings() { 1116 // Let's make sure the conversation header can't run into the expand button when we're 1117 // collapsed and update the paddings of the content 1118 int headerPaddingEnd; 1119 int contentPaddingEnd; 1120 if (!mExpandable) { 1121 headerPaddingEnd = 0; 1122 contentPaddingEnd = mContentMarginEnd; 1123 } else if (mIsCollapsed) { 1124 headerPaddingEnd = 0; 1125 contentPaddingEnd = 0; 1126 } else { 1127 headerPaddingEnd = mNotificationHeaderExpandedPadding; 1128 contentPaddingEnd = mContentMarginEnd; 1129 } 1130 mConversationHeader.setPaddingRelative( 1131 mConversationHeader.getPaddingStart(), 1132 mConversationHeader.getPaddingTop(), 1133 headerPaddingEnd, 1134 mConversationHeader.getPaddingBottom()); 1135 1136 mContentContainer.setPaddingRelative( 1137 mContentContainer.getPaddingStart(), 1138 mContentContainer.getPaddingTop(), 1139 contentPaddingEnd, 1140 mContentContainer.getPaddingBottom()); 1141 } 1142 onAppNameVisibilityChanged()1143 private void onAppNameVisibilityChanged() { 1144 boolean appNameGone = mAppName.getVisibility() == GONE; 1145 if (appNameGone != mAppNameGone) { 1146 mAppNameGone = appNameGone; 1147 updateAppNameDividerVisibility(); 1148 } 1149 } 1150 updateAppNameDividerVisibility()1151 private void updateAppNameDividerVisibility() { 1152 mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE); 1153 } 1154 updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener)1155 public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) { 1156 mExpandable = expandable; 1157 if (expandable) { 1158 mExpandButtonContainer.setVisibility(VISIBLE); 1159 mExpandButton.setOnClickListener(onClickListener); 1160 mConversationIconContainer.setOnClickListener(onClickListener); 1161 } else { 1162 mExpandButtonContainer.setVisibility(GONE); 1163 mConversationIconContainer.setOnClickListener(null); 1164 } 1165 mExpandButton.setVisibility(VISIBLE); 1166 updateContentEndPaddings(); 1167 } 1168 1169 @Override setMessagingClippingDisabled(boolean clippingDisabled)1170 public void setMessagingClippingDisabled(boolean clippingDisabled) { 1171 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect); 1172 } 1173 1174 @Nullable getConversationSenderName()1175 public CharSequence getConversationSenderName() { 1176 if (mGroups.isEmpty()) { 1177 return null; 1178 } 1179 final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName(); 1180 return getResources().getString(R.string.conversation_single_line_name_display, name); 1181 } 1182 isOneToOne()1183 public boolean isOneToOne() { 1184 return mIsOneToOne; 1185 } 1186 1187 @Nullable getConversationText()1188 public CharSequence getConversationText() { 1189 if (mMessages.isEmpty()) { 1190 return null; 1191 } 1192 final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1); 1193 final CharSequence text = messagingMessage.getMessage().getText(); 1194 if (text == null && messagingMessage instanceof MessagingImageMessage) { 1195 final String unformatted = 1196 getResources().getString(R.string.conversation_single_line_image_placeholder); 1197 SpannableString spannableString = new SpannableString(unformatted); 1198 spannableString.setSpan( 1199 new StyleSpan(Typeface.ITALIC), 1200 0, 1201 spannableString.length(), 1202 Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 1203 return spannableString; 1204 } 1205 return text; 1206 } 1207 1208 @Nullable getConversationIcon()1209 public Icon getConversationIcon() { 1210 return mConversationIcon; 1211 } 1212 1213 private static class TouchDelegateComposite extends TouchDelegate { 1214 private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>(); 1215 TouchDelegateComposite(View view)1216 private TouchDelegateComposite(View view) { 1217 super(new Rect(), view); 1218 } 1219 add(TouchDelegate delegate)1220 public void add(TouchDelegate delegate) { 1221 mDelegates.add(delegate); 1222 } 1223 clear()1224 public void clear() { 1225 mDelegates.clear(); 1226 } 1227 1228 @Override onTouchEvent(MotionEvent event)1229 public boolean onTouchEvent(MotionEvent event) { 1230 float x = event.getX(); 1231 float y = event.getY(); 1232 for (TouchDelegate delegate: mDelegates) { 1233 event.setLocation(x, y); 1234 if (delegate.onTouchEvent(event)) { 1235 return true; 1236 } 1237 } 1238 return false; 1239 } 1240 } 1241 } 1242