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