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 android.app.Flags.notificationsRedesignTemplates; 20 import static android.widget.flags.Flags.conversationLayoutUseMaximumChildHeight; 21 22 import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL; 23 import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.AnimatorSet; 28 import android.animation.ValueAnimator; 29 import android.annotation.AttrRes; 30 import android.annotation.NonNull; 31 import android.annotation.Nullable; 32 import android.annotation.StyleRes; 33 import android.app.Notification; 34 import android.app.Person; 35 import android.app.RemoteInputHistoryItem; 36 import android.content.Context; 37 import android.content.res.ColorStateList; 38 import android.graphics.Rect; 39 import android.graphics.Typeface; 40 import android.graphics.drawable.Drawable; 41 import android.graphics.drawable.GradientDrawable; 42 import android.graphics.drawable.Icon; 43 import android.os.Bundle; 44 import android.os.Parcelable; 45 import android.text.Spannable; 46 import android.text.SpannableString; 47 import android.text.TextUtils; 48 import android.text.style.StyleSpan; 49 import android.util.ArrayMap; 50 import android.util.AttributeSet; 51 import android.util.DisplayMetrics; 52 import android.view.Gravity; 53 import android.view.MotionEvent; 54 import android.view.RemotableViewMethod; 55 import android.view.TouchDelegate; 56 import android.view.View; 57 import android.view.ViewGroup; 58 import android.view.ViewTreeObserver; 59 import android.view.animation.Interpolator; 60 import android.view.animation.PathInterpolator; 61 import android.widget.FrameLayout; 62 import android.widget.ImageView; 63 import android.widget.LinearLayout; 64 import android.widget.RemoteViews; 65 import android.widget.TextView; 66 import android.widget.flags.Flags; 67 68 import com.android.internal.R; 69 import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData; 70 import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData; 71 72 import java.util.ArrayList; 73 import java.util.List; 74 import java.util.Map; 75 import java.util.Objects; 76 77 /** 78 * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal 79 * messages and adapts the layout accordingly. 80 */ 81 @RemoteViews.RemoteView 82 public class ConversationLayout extends FrameLayout 83 implements ImageMessageConsumer, IMessagingLayout { 84 85 public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); 86 public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); 87 public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f); 88 public static final Interpolator OVERSHOOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f); 89 private static final int MAX_SUMMARIZATION_LINES = 3; 90 public static final int IMPORTANCE_ANIM_GROW_DURATION = 250; 91 public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200; 92 public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25; 93 private final PeopleHelper mPeopleHelper = new PeopleHelper(); 94 private List<MessagingMessage> mMessages = new ArrayList<>(); 95 private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); 96 private MessagingLinearLayout mMessagingLinearLayout; 97 private boolean mShowHistoricMessages; 98 private final ArrayList<MessagingGroup> mGroups = new ArrayList<>(); 99 private int mLayoutColor; 100 private int mSenderTextColor; 101 private int mMessageTextColor; 102 private Icon mAvatarReplacement; 103 private boolean mIsOneToOne; 104 private final ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); 105 private Person mUser; 106 private CharSequence mNameReplacement; 107 private CharSequence mSummarizedContent; 108 private boolean mIsCollapsed; 109 private ImageResolver mImageResolver; 110 private CachingIconView mConversationIconView; 111 private View mConversationIconContainer; 112 private int mConversationIconTopPaddingExpandedGroup; 113 private int mConversationIconTopPadding; 114 private int mExpandedGroupMessagePadding; 115 // TODO (b/217799515) Currently, mConversationText shows the conversation title, the actual 116 // conversation text is inside of mMessagingLinearLayout, which is misleading, we should rename 117 // this to mConversationTitleView 118 private TextView mConversationText; 119 private View mConversationIconBadge; 120 private CachingIconView mConversationIconBadgeBg; 121 private Icon mLargeIcon; 122 private View mExpandButtonContainer; 123 private ViewGroup mExpandButtonAndContentContainer; 124 private ViewGroup mExpandButtonContainerA11yContainer; 125 private NotificationExpandButton mExpandButton; 126 private MessagingLinearLayout mImageMessageContainer; 127 private int mBadgeProtrusion; 128 private int mConversationAvatarSize; 129 private int mConversationAvatarSizeExpanded; 130 private CachingIconView mIcon; 131 private CachingIconView mImportanceRingView; 132 private int mExpandedGroupBadgeProtrusion; 133 private int mExpandedGroupBadgeProtrusionFacePile; 134 private View mConversationFacePile; 135 private int mNotificationBackgroundColor; 136 private CharSequence mFallbackChatName; 137 private CharSequence mFallbackGroupChatName; 138 //TODO (b/217799515) Currently, Notification.MessagingStyle, ConversationLayout, and 139 // HybridConversationNotificationView, each has their own definition of "ConversationTitle". 140 // What make things worse is that the term of "ConversationTitle" often confuses with 141 // "ConversationText". 142 // We need to unify them or differentiate the namings. 143 private CharSequence mConversationTitle; 144 private int mMessageSpacingStandard; 145 private int mMessageSpacingGroup; 146 private int mNotificationHeaderExpandedPadding; 147 private View mConversationHeader; 148 private View mContentContainer; 149 private boolean mExpandable = true; 150 private int mContentMarginEnd; 151 private Rect mMessagingClipRect; 152 private ObservableTextView mAppName; 153 private NotificationActionListLayout mActions; 154 private boolean mAppNameGone; 155 private int mFacePileAvatarSize; 156 private int mFacePileAvatarSizeExpandedGroup; 157 private int mFacePileProtectionWidth; 158 private int mFacePileProtectionWidthExpanded; 159 private boolean mImportantConversation; 160 private View mFeedbackIcon; 161 private float mMinTouchSize; 162 private Icon mConversationIcon; 163 private Icon mShortcutIcon; 164 private View mAppNameDivider; 165 private final TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this); 166 private final ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); 167 private boolean mPrecomputedTextEnabled = false; 168 @Nullable 169 private ConversationHeaderData mConversationHeaderData; 170 ConversationLayout(@onNull Context context)171 public ConversationLayout(@NonNull Context context) { 172 super(context); 173 } 174 ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs)175 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs) { 176 super(context, attrs); 177 } 178 ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr)179 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, 180 @AttrRes int defStyleAttr) { 181 super(context, attrs, defStyleAttr); 182 } 183 ConversationLayout(@onNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes)184 public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, 185 @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { 186 super(context, attrs, defStyleAttr, defStyleRes); 187 } 188 189 @Override onFinishInflate()190 protected void onFinishInflate() { 191 super.onFinishInflate(); 192 mPeopleHelper.init(getContext()); 193 mMessagingLinearLayout = findViewById(R.id.notification_messaging); 194 mActions = findViewById(R.id.actions); 195 mImageMessageContainer = findViewById(R.id.conversation_image_message_container); 196 // We still want to clip, but only on the top, since views can temporarily out of bounds 197 // during transitions. 198 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 199 int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); 200 mMessagingClipRect = new Rect(0, 0, size, size); 201 setMessagingClippingDisabled(false); 202 mConversationIconView = findViewById(R.id.conversation_icon); 203 mConversationIconContainer = findViewById(R.id.conversation_icon_container); 204 mIcon = findViewById(R.id.icon); 205 mFeedbackIcon = findViewById(com.android.internal.R.id.feedback); 206 mMinTouchSize = 48 * getResources().getDisplayMetrics().density; 207 mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring); 208 mConversationIconBadge = findViewById(R.id.conversation_icon_badge); 209 mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg); 210 mIcon.setOnVisibilityChangedListener((visibility) -> { 211 212 // Let's hide the background directly or in an animated way 213 boolean isGone = visibility == GONE; 214 int oldVisibility = mConversationIconBadgeBg.getVisibility(); 215 boolean wasGone = oldVisibility == GONE; 216 if (wasGone != isGone) { 217 // Keep the badge gone state in sync with the icon. This is necessary in cases 218 // Where the icon is being hidden externally like in group children. 219 mConversationIconBadgeBg.animate().cancel(); 220 mConversationIconBadgeBg.setVisibility(visibility); 221 } 222 223 // Let's handle the importance ring which can also be be gone normally 224 oldVisibility = mImportanceRingView.getVisibility(); 225 wasGone = oldVisibility == GONE; 226 visibility = !mImportantConversation ? GONE : visibility; 227 boolean isRingGone = visibility == GONE; 228 if (wasGone != isRingGone) { 229 // Keep the badge visibility in sync with the icon. This is necessary in cases 230 // Where the icon is being hidden externally like in group children. 231 mImportanceRingView.animate().cancel(); 232 mImportanceRingView.setVisibility(visibility); 233 } 234 235 oldVisibility = mConversationIconBadge.getVisibility(); 236 wasGone = oldVisibility == GONE; 237 if (wasGone != isGone) { 238 mConversationIconBadge.animate().cancel(); 239 mConversationIconBadge.setVisibility(visibility); 240 } 241 }); 242 // When the small icon is gone, hide the rest of the badge 243 mIcon.setOnForceHiddenChangedListener((forceHidden) -> { 244 mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); 245 mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); 246 }); 247 248 // When the conversation icon is gone, hide the whole badge 249 mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> { 250 mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); 251 mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); 252 mPeopleHelper.animateViewForceHidden(mIcon, forceHidden); 253 }); 254 mConversationText = findViewById(notificationsRedesignTemplates() 255 ? R.id.title : R.id.conversation_text); 256 mExpandButtonContainer = findViewById(R.id.expand_button_container); 257 mExpandButtonContainerA11yContainer = 258 findViewById(R.id.expand_button_a11y_container); 259 mConversationHeader = findViewById(R.id.conversation_header); 260 mContentContainer = findViewById(R.id.notification_action_list_margin_target); 261 mExpandButtonAndContentContainer = findViewById(R.id.expand_button_and_content_container); 262 mExpandButton = findViewById(R.id.expand_button); 263 mMessageSpacingStandard = getResources().getDimensionPixelSize( 264 R.dimen.notification_messaging_spacing); 265 mMessageSpacingGroup = getResources().getDimensionPixelSize( 266 R.dimen.notification_messaging_spacing_conversation_group); 267 mNotificationHeaderExpandedPadding = getResources().getDimensionPixelSize( 268 R.dimen.conversation_header_expanded_padding_end); 269 mContentMarginEnd = getResources().getDimensionPixelSize( 270 R.dimen.notification_content_margin_end); 271 mBadgeProtrusion = getResources().getDimensionPixelSize( 272 R.dimen.conversation_badge_protrusion); 273 mConversationAvatarSize = getResources().getDimensionPixelSize( 274 R.dimen.conversation_avatar_size); 275 mConversationAvatarSizeExpanded = getResources().getDimensionPixelSize( 276 R.dimen.conversation_avatar_size_group_expanded); 277 mConversationIconTopPaddingExpandedGroup = getResources().getDimensionPixelSize( 278 R.dimen.conversation_icon_container_top_padding_small_avatar); 279 mConversationIconTopPadding = getResources().getDimensionPixelSize( 280 R.dimen.conversation_icon_container_top_padding); 281 mExpandedGroupMessagePadding = getResources().getDimensionPixelSize( 282 R.dimen.expanded_group_conversation_message_padding); 283 mExpandedGroupBadgeProtrusion = getResources().getDimensionPixelSize( 284 R.dimen.conversation_badge_protrusion_group_expanded); 285 mExpandedGroupBadgeProtrusionFacePile = getResources().getDimensionPixelSize( 286 R.dimen.conversation_badge_protrusion_group_expanded_face_pile); 287 mConversationFacePile = findViewById(R.id.conversation_face_pile); 288 mFacePileAvatarSize = getResources().getDimensionPixelSize( 289 R.dimen.conversation_face_pile_avatar_size); 290 mFacePileAvatarSizeExpandedGroup = getResources().getDimensionPixelSize( 291 R.dimen.conversation_face_pile_avatar_size_group_expanded); 292 mFacePileProtectionWidth = getResources().getDimensionPixelSize( 293 R.dimen.conversation_face_pile_protection_width); 294 mFacePileProtectionWidthExpanded = getResources().getDimensionPixelSize( 295 R.dimen.conversation_face_pile_protection_width_expanded); 296 mFallbackChatName = getResources().getString( 297 R.string.conversation_title_fallback_one_to_one); 298 mFallbackGroupChatName = getResources().getString( 299 R.string.conversation_title_fallback_group_chat); 300 mAppName = findViewById(R.id.app_name_text); 301 mAppNameDivider = findViewById(R.id.app_name_divider); 302 mAppNameGone = mAppName.getVisibility() == GONE; 303 mAppName.setOnVisibilityChangedListener((visibility) -> { 304 onAppNameVisibilityChanged(); 305 }); 306 } 307 308 @RemotableViewMethod setAvatarReplacement(Icon icon)309 public void setAvatarReplacement(Icon icon) { 310 mAvatarReplacement = icon; 311 } 312 313 @RemotableViewMethod setNameReplacement(CharSequence nameReplacement)314 public void setNameReplacement(CharSequence nameReplacement) { 315 mNameReplacement = nameReplacement; 316 } 317 318 /** 319 * Sets this conversation as "important", adding some additional UI treatment. 320 */ 321 @RemotableViewMethod setIsImportantConversation(boolean isImportantConversation)322 public void setIsImportantConversation(boolean isImportantConversation) { 323 setIsImportantConversation(isImportantConversation, false); 324 } 325 326 /** 327 * @hide 328 **/ setIsImportantConversation(boolean isImportantConversation, boolean animate)329 public void setIsImportantConversation(boolean isImportantConversation, boolean animate) { 330 mImportantConversation = isImportantConversation; 331 mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE 332 ? VISIBLE : GONE); 333 334 if (animate && isImportantConversation) { 335 GradientDrawable ring = (GradientDrawable) mImportanceRingView.getDrawable(); 336 ring.mutate(); 337 GradientDrawable bg = (GradientDrawable) mConversationIconBadgeBg.getDrawable(); 338 bg.mutate(); 339 int ringColor = getResources() 340 .getColor(R.color.conversation_important_highlight); 341 int standardThickness = getResources() 342 .getDimensionPixelSize(R.dimen.importance_ring_stroke_width); 343 int largeThickness = getResources() 344 .getDimensionPixelSize(R.dimen.importance_ring_anim_max_stroke_width); 345 int standardSize = getResources().getDimensionPixelSize( 346 R.dimen.importance_ring_size); 347 int baseSize = standardSize - standardThickness * 2; 348 int bgSize = getResources() 349 .getDimensionPixelSize(R.dimen.conversation_icon_size_badged); 350 351 ValueAnimator.AnimatorUpdateListener animatorUpdateListener = animation -> { 352 int strokeWidth = Math.round((float) animation.getAnimatedValue()); 353 ring.setStroke(strokeWidth, ringColor); 354 int newSize = baseSize + strokeWidth * 2; 355 ring.setSize(newSize, newSize); 356 mImportanceRingView.invalidate(); 357 }; 358 359 ValueAnimator growAnimation = ValueAnimator.ofFloat(0, largeThickness); 360 growAnimation.setInterpolator(LINEAR_OUT_SLOW_IN); 361 growAnimation.setDuration(IMPORTANCE_ANIM_GROW_DURATION); 362 growAnimation.addUpdateListener(animatorUpdateListener); 363 364 ValueAnimator shrinkAnimation = 365 ValueAnimator.ofFloat(largeThickness, standardThickness); 366 shrinkAnimation.setDuration(IMPORTANCE_ANIM_SHRINK_DURATION); 367 shrinkAnimation.setStartDelay(IMPORTANCE_ANIM_SHRINK_DELAY); 368 shrinkAnimation.setInterpolator(OVERSHOOT); 369 shrinkAnimation.addUpdateListener(animatorUpdateListener); 370 shrinkAnimation.addListener(new AnimatorListenerAdapter() { 371 @Override 372 public void onAnimationStart(Animator animation) { 373 // Shrink the badge bg so that it doesn't peek behind the animation 374 bg.setSize(baseSize, baseSize); 375 mConversationIconBadgeBg.invalidate(); 376 } 377 378 @Override 379 public void onAnimationEnd(Animator animation) { 380 // Reset bg back to normal size 381 bg.setSize(bgSize, bgSize); 382 mConversationIconBadgeBg.invalidate(); 383 } 384 }); 385 386 AnimatorSet anims = new AnimatorSet(); 387 anims.playSequentially(growAnimation, shrinkAnimation); 388 anims.start(); 389 } 390 } 391 isImportantConversation()392 public boolean isImportantConversation() { 393 return mImportantConversation; 394 } 395 396 /** 397 * Set this layout to show the collapsed representation. 398 * 399 * @param isCollapsed is it collapsed 400 */ 401 @RemotableViewMethod(asyncImpl = "setIsCollapsedAsync") setIsCollapsed(boolean isCollapsed)402 public void setIsCollapsed(boolean isCollapsed) { 403 mIsCollapsed = isCollapsed; 404 int maxLines = Integer.MAX_VALUE; 405 if (isCollapsed) { 406 if (!TextUtils.isEmpty(mSummarizedContent)) { 407 maxLines = MAX_SUMMARIZATION_LINES; 408 } else { 409 if (android.app.Flags.nmCollapsedLines()) { 410 maxLines = 2; 411 } else { 412 maxLines = 1; 413 } 414 } 415 } 416 mMessagingLinearLayout.setMaxDisplayedLines(maxLines); 417 updateExpandButton(); 418 updateContentEndPaddings(); 419 } 420 421 /** 422 * setDataAsync needs to do different stuff for the collapsed vs expanded view, so store the 423 * collapsed state early. 424 */ setIsCollapsedAsync(boolean isCollapsed)425 public Runnable setIsCollapsedAsync(boolean isCollapsed) { 426 mIsCollapsed = isCollapsed; 427 return () -> setIsCollapsed(isCollapsed); 428 } 429 430 /** 431 * Set conversation data 432 * 433 * @param extras Bundle contains conversation data 434 */ 435 @RemotableViewMethod(asyncImpl = "setDataAsync") setData(Bundle extras)436 public void setData(Bundle extras) { 437 bind(parseMessagingData(extras, 438 /* usePrecomputedText= */ false, 439 /*includeConversationIcon= */false)); 440 } 441 442 @NonNull parseMessagingData(Bundle extras, boolean usePrecomputedText, boolean includeConversationIcon)443 private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText, 444 boolean includeConversationIcon) { 445 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 446 List<Notification.MessagingStyle.Message> newMessages = 447 Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); 448 Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); 449 List<Notification.MessagingStyle.Message> newHistoricMessages = 450 Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); 451 452 // mUser now set (would be nice to avoid the side effect but WHATEVER) 453 final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); 454 // Append remote input history to newMessages (again, side effect is lame but WHATEVS) 455 RemoteInputHistoryItem[] history = extras.getParcelableArray( 456 Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS, RemoteInputHistoryItem.class); 457 addRemoteInputHistoryToMessages(newMessages, history); 458 459 boolean showSpinner = 460 extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false); 461 int unreadCount = extras.getInt(Notification.EXTRA_CONVERSATION_UNREAD_MESSAGE_COUNT); 462 463 List<MessagingMessage> newMessagingMessages; 464 mSummarizedContent = extras.getCharSequence(Notification.EXTRA_SUMMARIZED_CONTENT); 465 if (!TextUtils.isEmpty(mSummarizedContent) && mIsCollapsed) { 466 Notification.MessagingStyle.Message summary = 467 new Notification.MessagingStyle.Message(mSummarizedContent, 0, ""); 468 newMessagingMessages = createMessages(List.of(summary), false, usePrecomputedText); 469 } else { 470 newMessagingMessages = 471 createMessages(newMessages, /* isHistoric= */false, usePrecomputedText); 472 } 473 final List<MessagingMessage> newHistoricMessagingMessages = 474 createMessages(newHistoricMessages, /* isHistoric= */true, usePrecomputedText); 475 476 // Add our new MessagingMessages to groups 477 List<List<MessagingMessage>> groups = new ArrayList<>(); 478 List<Person> senders = new ArrayList<>(); 479 // Lets first find the groups (populate `groups` and `senders`) 480 findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders); 481 482 // load conversation header data, avatar and title. 483 final ConversationHeaderData conversationHeaderData; 484 if (includeConversationIcon && Flags.conversationStyleSetAvatarAsync()) { 485 conversationHeaderData = loadConversationHeaderData(mIsOneToOne, 486 mConversationTitle, 487 mShortcutIcon, 488 mLargeIcon, newMessagingMessages, user, groups, mLayoutColor); 489 } else { 490 conversationHeaderData = null; 491 } 492 493 return new MessagingData(user, showSpinner, unreadCount, 494 newHistoricMessagingMessages, newMessagingMessages, groups, senders, 495 conversationHeaderData, mSummarizedContent); 496 } 497 498 /** 499 * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. 500 * This should be called on a background thread, and returns a Runnable which is then must be 501 * called on the main thread to complete the operation and set text. 502 * 503 * @param extras Bundle contains conversation data 504 * @hide 505 */ 506 @NonNull setDataAsync(Bundle extras)507 public Runnable setDataAsync(Bundle extras) { 508 if (!mPrecomputedTextEnabled) { 509 return () -> setData(extras); 510 } 511 512 final MessagingData messagingData = 513 parseMessagingData(extras, 514 /* usePrecomputedText= */ true, 515 /*includeConversationIcon=*/true); 516 517 return () -> { 518 finalizeInflate(messagingData.getHistoricMessagingMessages()); 519 finalizeInflate(messagingData.getNewMessagingMessages()); 520 521 bind(messagingData); 522 }; 523 } 524 525 /** 526 * enable/disable precomputed text usage 527 * 528 * @hide 529 */ setPrecomputedTextEnabled(boolean precomputedTextEnabled)530 public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) { 531 mPrecomputedTextEnabled = precomputedTextEnabled; 532 } 533 finalizeInflate(List<MessagingMessage> historicMessagingMessages)534 private void finalizeInflate(List<MessagingMessage> historicMessagingMessages) { 535 for (MessagingMessage messagingMessage : historicMessagingMessages) { 536 messagingMessage.finalizeInflate(); 537 } 538 } 539 540 @Override setImageResolver(ImageResolver resolver)541 public void setImageResolver(ImageResolver resolver) { 542 mImageResolver = resolver; 543 } 544 545 /** 546 * @hide 547 */ setUnreadCount(int unreadCount)548 public void setUnreadCount(int unreadCount) { 549 mExpandButton.setNumber(unreadCount); 550 } 551 addRemoteInputHistoryToMessages( List<Notification.MessagingStyle.Message> newMessages, RemoteInputHistoryItem[] remoteInputHistory)552 private void addRemoteInputHistoryToMessages( 553 List<Notification.MessagingStyle.Message> newMessages, 554 RemoteInputHistoryItem[] remoteInputHistory) { 555 if (remoteInputHistory == null || remoteInputHistory.length == 0) { 556 return; 557 } 558 for (int i = remoteInputHistory.length - 1; i >= 0; i--) { 559 RemoteInputHistoryItem historyMessage = remoteInputHistory[i]; 560 Notification.MessagingStyle.Message message = new Notification.MessagingStyle.Message( 561 historyMessage.getText(), 0, null, true /* remoteHistory */); 562 if (historyMessage.getUri() != null) { 563 message.setData(historyMessage.getMimeType(), historyMessage.getUri()); 564 } 565 newMessages.add(message); 566 } 567 } 568 bind(MessagingData messagingData)569 private void bind(MessagingData messagingData) { 570 setUser(messagingData.getUser()); 571 setUnreadCount(messagingData.getUnreadCount()); 572 573 // Copy our groups, before they get clobbered 574 ArrayList<MessagingGroup> oldGroups = new ArrayList<>(mGroups); 575 576 // Let's now create the views and reorder them accordingly 577 // side-effect: updates mGroups, mAddedGroups 578 createGroupViews(messagingData.getGroups(), messagingData.getSenders(), 579 messagingData.getShowSpinner()); 580 581 // Let's first check which groups were removed altogether and remove them in one animation 582 removeGroups(oldGroups); 583 584 // Let's remove the remaining messages 585 for (MessagingMessage message : mMessages) { 586 message.removeMessage(mToRecycle); 587 } 588 for (MessagingMessage historicMessage : mHistoricMessages) { 589 historicMessage.removeMessage(mToRecycle); 590 } 591 592 mMessages = messagingData.getNewMessagingMessages(); 593 mHistoricMessages = messagingData.getHistoricMessagingMessages(); 594 updateHistoricMessageVisibility(); 595 updateTitleAndNamesDisplay(); 596 597 updateConversationLayout(messagingData); 598 599 // Recycle everything at the end of the update, now that we know it's no longer needed. 600 for (MessagingLinearLayout.MessagingChild child : mToRecycle) { 601 child.recycle(); 602 } 603 mToRecycle.clear(); 604 } 605 606 /** 607 * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc); 608 */ updateConversationLayout(MessagingData messagingData)609 private void updateConversationLayout(MessagingData messagingData) { 610 if (!Flags.conversationStyleSetAvatarAsync()) { 611 computeAndSetConversationAvatarAndName(); 612 } else { 613 ConversationHeaderData conversationHeaderData = 614 messagingData.getConversationHeaderData(); 615 if (conversationHeaderData == null) { 616 conversationHeaderData = loadConversationHeaderData(mIsOneToOne, 617 mConversationTitle, mShortcutIcon, mLargeIcon, mMessages, mUser, 618 messagingData.getGroups(), 619 mLayoutColor); 620 } 621 setConversationAvatarAndNameFromData(conversationHeaderData); 622 } 623 624 updateAppName(); 625 updateIconPositionAndSize(); 626 updateImageMessages(); 627 updatePaddingsBasedOnContentAvailability(); 628 updateActionListPadding(); 629 updateAppNameDividerVisibility(); 630 } 631 632 @Deprecated computeAndSetConversationAvatarAndName()633 private void computeAndSetConversationAvatarAndName() { 634 // Set avatar and name 635 CharSequence conversationText = mConversationTitle; 636 mConversationIcon = mShortcutIcon; 637 if (mIsOneToOne) { 638 // Let's resolve the icon / text from the last sender 639 CharSequence userKey = getKey(mUser); 640 for (int i = mGroups.size() - 1; i >= 0; i--) { 641 MessagingGroup messagingGroup = mGroups.get(i); 642 Person messageSender = messagingGroup.getSender(); 643 if ((messageSender != null && !TextUtils.equals(userKey, getKey(messageSender))) 644 || i == 0) { 645 if (TextUtils.isEmpty(conversationText)) { 646 // We use the sendername as header text if no conversation title is provided 647 // (This usually happens for most 1:1 conversations) 648 conversationText = messagingGroup.getSenderName(); 649 } 650 if (mConversationIcon == null) { 651 Icon avatarIcon = messagingGroup.getAvatarIcon(); 652 if (avatarIcon == null) { 653 avatarIcon = mPeopleHelper.createAvatarSymbol(conversationText, "", 654 mLayoutColor); 655 } 656 mConversationIcon = avatarIcon; 657 } 658 break; 659 } 660 } 661 } 662 if (mConversationIcon == null) { 663 mConversationIcon = mLargeIcon; 664 } 665 if (mIsOneToOne || mConversationIcon != null) { 666 mConversationIconView.setVisibility(VISIBLE); 667 mConversationFacePile.setVisibility(GONE); 668 mConversationIconView.setImageIcon(mConversationIcon); 669 } else { 670 mConversationIconView.setVisibility(GONE); 671 // This will also inflate it! 672 mConversationFacePile.setVisibility(VISIBLE); 673 // rebind the value to the inflated view instead of the stub 674 mConversationFacePile = findViewById(R.id.conversation_face_pile); 675 bindFacePile(); 676 } 677 if (TextUtils.isEmpty(conversationText)) { 678 conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName; 679 } 680 mConversationText.setText(conversationText); 681 // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) 682 // This needs to happen after all of the above o update all of the groups 683 mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); 684 } 685 setConversationAvatarAndNameFromData( ConversationHeaderData conversationHeaderData)686 private void setConversationAvatarAndNameFromData( 687 ConversationHeaderData conversationHeaderData) { 688 mConversationHeaderData = conversationHeaderData; 689 final OneToOneConversationAvatarData oneToOneConversationDrawable; 690 final GroupConversationAvatarData groupConversationAvatarData; 691 final ConversationAvatarData conversationAvatar = 692 conversationHeaderData.getConversationAvatar(); 693 if (conversationAvatar instanceof OneToOneConversationAvatarData) { 694 oneToOneConversationDrawable = 695 ((OneToOneConversationAvatarData) conversationAvatar); 696 groupConversationAvatarData = null; 697 } else { 698 oneToOneConversationDrawable = null; 699 groupConversationAvatarData = ((GroupConversationAvatarData) conversationAvatar); 700 } 701 702 if (oneToOneConversationDrawable != null) { 703 mConversationIconView.setVisibility(VISIBLE); 704 mConversationFacePile.setVisibility(GONE); 705 mConversationIconView.setImageDrawable(oneToOneConversationDrawable.mDrawable); 706 } else { 707 mConversationIconView.setVisibility(GONE); 708 // This will also inflate it! 709 mConversationFacePile.setVisibility(VISIBLE); 710 // rebind the value to the inflated view instead of the stub 711 mConversationFacePile = findViewById(R.id.conversation_face_pile); 712 bindFacePile(groupConversationAvatarData); 713 } 714 CharSequence conversationText = conversationHeaderData.getConversationText(); 715 if (TextUtils.isEmpty(conversationText)) { 716 conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName; 717 } 718 mConversationText.setText(conversationText); 719 // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) 720 // This needs to happen after all of the above o update all of the groups 721 mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); 722 } 723 updateActionListPadding()724 private void updateActionListPadding() { 725 if (!notificationsRedesignTemplates() && mActions != null) { 726 mActions.setCollapsibleIndentDimen(R.dimen.call_notification_collapsible_indent); 727 } 728 } 729 updateImageMessages()730 private void updateImageMessages() { 731 if (mImageMessageContainer == null) { 732 return; 733 } 734 View newMessage = getNewImageMessage(); 735 // Remove all messages that don't belong into the image layout 736 View previousMessage = mImageMessageContainer.getChildAt(0); 737 if (previousMessage != newMessage) { 738 mImageMessageContainer.removeView(previousMessage); 739 if (newMessage != null) { 740 mImageMessageContainer.addView(newMessage); 741 } 742 } 743 mImageMessageContainer.setVisibility(newMessage != null ? VISIBLE : GONE); 744 } 745 746 @Nullable getNewImageMessage()747 private View getNewImageMessage() { 748 if (mIsCollapsed && !mGroups.isEmpty()) { 749 // When collapsed, we're displaying the image message in a dedicated container 750 // on the right of the layout instead of inline. Let's add the isolated image there 751 MessagingGroup messagingGroup = mGroups.getLast(); 752 MessagingImageMessage isolatedMessage = messagingGroup.getIsolatedMessage(); 753 if (isolatedMessage != null) { 754 return isolatedMessage.getView(); 755 } 756 } 757 return null; 758 } 759 bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView)760 public void bindFacePile(ImageView bottomBackground, ImageView bottomView, ImageView topView) { 761 applyNotificationBackgroundColor(bottomBackground); 762 // Let's find the two last conversations: 763 Icon secondLastIcon = null; 764 CharSequence lastKey = null; 765 Icon lastIcon = null; 766 CharSequence userKey = getKey(mUser); 767 for (int i = mGroups.size() - 1; i >= 0; i--) { 768 MessagingGroup messagingGroup = mGroups.get(i); 769 Person messageSender = messagingGroup.getSender(); 770 boolean notUser = messageSender != null 771 && !TextUtils.equals(userKey, getKey(messageSender)); 772 boolean notIncluded = messageSender != null 773 && !TextUtils.equals(lastKey, getKey(messageSender)); 774 if ((notUser && notIncluded) 775 || (i == 0 && lastKey == null)) { 776 if (lastIcon == null) { 777 lastIcon = messagingGroup.getAvatarIcon(); 778 lastKey = getKey(messageSender); 779 } else { 780 secondLastIcon = messagingGroup.getAvatarIcon(); 781 break; 782 } 783 } 784 } 785 if (lastIcon == null) { 786 lastIcon = mPeopleHelper.createAvatarSymbol(" ", "", mLayoutColor); 787 } 788 bottomView.setImageIcon(lastIcon); 789 if (secondLastIcon == null) { 790 secondLastIcon = mPeopleHelper.createAvatarSymbol("", "", mLayoutColor); 791 } 792 topView.setImageIcon(secondLastIcon); 793 } 794 795 @Deprecated bindFacePile()796 private void bindFacePile() { 797 bindFacePile(null); 798 } 799 bindFacePile(@ullable GroupConversationAvatarData groupConversationAvatarData)800 private void bindFacePile(@Nullable GroupConversationAvatarData groupConversationAvatarData) { 801 ImageView bottomBackground = mConversationFacePile.findViewById( 802 R.id.conversation_face_pile_bottom_background); 803 ImageView bottomView = mConversationFacePile.findViewById( 804 R.id.conversation_face_pile_bottom); 805 ImageView topView = mConversationFacePile.findViewById( 806 R.id.conversation_face_pile_top); 807 808 if (groupConversationAvatarData == null) { 809 bindFacePile(bottomBackground, bottomView, topView); 810 } else { 811 bindFacePileWithDrawable(bottomBackground, bottomView, topView, 812 groupConversationAvatarData); 813 814 } 815 816 if (!notificationsRedesignTemplates()) { 817 // We no longer need to update the size based on expansion state. 818 int conversationAvatarSize; 819 int facepileAvatarSize; 820 int facePileBackgroundSize; 821 if (mIsCollapsed) { 822 conversationAvatarSize = mConversationAvatarSize; 823 facepileAvatarSize = mFacePileAvatarSize; 824 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidth; 825 } else { 826 conversationAvatarSize = mConversationAvatarSizeExpanded; 827 facepileAvatarSize = mFacePileAvatarSizeExpandedGroup; 828 facePileBackgroundSize = facepileAvatarSize + 2 * mFacePileProtectionWidthExpanded; 829 } 830 LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams(); 831 layoutParams.width = conversationAvatarSize; 832 layoutParams.height = conversationAvatarSize; 833 mConversationFacePile.setLayoutParams(layoutParams); 834 835 layoutParams = (LayoutParams) bottomView.getLayoutParams(); 836 layoutParams.width = facepileAvatarSize; 837 layoutParams.height = facepileAvatarSize; 838 bottomView.setLayoutParams(layoutParams); 839 840 layoutParams = (LayoutParams) topView.getLayoutParams(); 841 layoutParams.width = facepileAvatarSize; 842 layoutParams.height = facepileAvatarSize; 843 topView.setLayoutParams(layoutParams); 844 845 layoutParams = (LayoutParams) bottomBackground.getLayoutParams(); 846 layoutParams.width = facePileBackgroundSize; 847 layoutParams.height = facePileBackgroundSize; 848 bottomBackground.setLayoutParams(layoutParams); 849 } 850 } 851 852 /** 853 * Binds group avatar drawables to face pile. 854 */ bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, ImageView topView, GroupConversationAvatarData groupConversationAvatarData)855 public void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, 856 ImageView topView, GroupConversationAvatarData groupConversationAvatarData) { 857 applyNotificationBackgroundColor(bottomBackground); 858 bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon); 859 topView.setImageDrawable(groupConversationAvatarData.mSecondLastIcon); 860 } 861 updateAppName()862 private void updateAppName() { 863 if (notificationsRedesignTemplates()) { 864 return; 865 } 866 867 mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE); 868 } 869 shouldHideAppName()870 public boolean shouldHideAppName() { 871 return mIsCollapsed; 872 } 873 874 /** 875 * update the icon position and sizing 876 */ updateIconPositionAndSize()877 private void updateIconPositionAndSize() { 878 if (notificationsRedesignTemplates()) { 879 // Icon size is fixed in the redesign. 880 return; 881 } 882 883 int badgeProtrusion; 884 int conversationAvatarSize; 885 if (mIsOneToOne || mIsCollapsed) { 886 badgeProtrusion = mBadgeProtrusion; 887 conversationAvatarSize = mConversationAvatarSize; 888 } else { 889 badgeProtrusion = mConversationFacePile.getVisibility() == VISIBLE 890 ? mExpandedGroupBadgeProtrusionFacePile 891 : mExpandedGroupBadgeProtrusion; 892 conversationAvatarSize = mConversationAvatarSizeExpanded; 893 } 894 895 if (mConversationIconView.getVisibility() == VISIBLE) { 896 LayoutParams layoutParams = (LayoutParams) mConversationIconView.getLayoutParams(); 897 layoutParams.width = conversationAvatarSize; 898 layoutParams.height = conversationAvatarSize; 899 layoutParams.leftMargin = badgeProtrusion; 900 layoutParams.rightMargin = badgeProtrusion; 901 layoutParams.bottomMargin = badgeProtrusion; 902 mConversationIconView.setLayoutParams(layoutParams); 903 } 904 905 if (mConversationFacePile.getVisibility() == VISIBLE) { 906 LayoutParams layoutParams = (LayoutParams) mConversationFacePile.getLayoutParams(); 907 layoutParams.leftMargin = badgeProtrusion; 908 layoutParams.rightMargin = badgeProtrusion; 909 layoutParams.bottomMargin = badgeProtrusion; 910 mConversationFacePile.setLayoutParams(layoutParams); 911 } 912 } 913 updatePaddingsBasedOnContentAvailability()914 private void updatePaddingsBasedOnContentAvailability() { 915 if (notificationsRedesignTemplates()) { 916 // group icons have the same size as 1:1 conversations 917 return; 918 } 919 920 // groups have avatars that need more spacing 921 mMessagingLinearLayout.setSpacing( 922 mIsOneToOne ? mMessageSpacingStandard : mMessageSpacingGroup); 923 924 int messagingPadding = mIsOneToOne || mIsCollapsed 925 ? 0 926 // Add some extra padding to the messages, since otherwise it will overlap with the 927 // group 928 : mExpandedGroupMessagePadding; 929 930 int iconPadding = mIsOneToOne || mIsCollapsed 931 ? mConversationIconTopPadding 932 : mConversationIconTopPaddingExpandedGroup; 933 934 mConversationIconContainer.setPaddingRelative( 935 mConversationIconContainer.getPaddingStart(), 936 iconPadding, 937 mConversationIconContainer.getPaddingEnd(), 938 mConversationIconContainer.getPaddingBottom()); 939 940 mMessagingLinearLayout.setPaddingRelative( 941 mMessagingLinearLayout.getPaddingStart(), 942 messagingPadding, 943 mMessagingLinearLayout.getPaddingEnd(), 944 mMessagingLinearLayout.getPaddingBottom()); 945 } 946 947 /** 948 * async version of {@link ConversationLayout#setLargeIcon} 949 */ 950 @RemotableViewMethod setLargeIconAsync(Icon largeIcon)951 public Runnable setLargeIconAsync(Icon largeIcon) { 952 if (!Flags.conversationStyleSetAvatarAsync()) { 953 return () -> setLargeIcon(largeIcon); 954 } 955 956 mLargeIcon = largeIcon; 957 return NotificationRunnables.NOOP; 958 } 959 960 @RemotableViewMethod(asyncImpl = "setLargeIconAsync") setLargeIcon(Icon largeIcon)961 public void setLargeIcon(Icon largeIcon) { 962 mLargeIcon = largeIcon; 963 } 964 965 /** 966 * async version of {@link ConversationLayout#setShortcutIcon} 967 */ 968 @RemotableViewMethod setShortcutIconAsync(Icon shortcutIcon)969 public Runnable setShortcutIconAsync(Icon shortcutIcon) { 970 if (!Flags.conversationStyleSetAvatarAsync()) { 971 return () -> setShortcutIcon(shortcutIcon); 972 } 973 974 mShortcutIcon = shortcutIcon; 975 return NotificationRunnables.NOOP; 976 } 977 978 @RemotableViewMethod(asyncImpl = "setShortcutIconAsync") setShortcutIcon(Icon shortcutIcon)979 public void setShortcutIcon(Icon shortcutIcon) { 980 mShortcutIcon = shortcutIcon; 981 } 982 983 /** 984 * async version of {@link ConversationLayout#setConversationTitle} 985 */ 986 @RemotableViewMethod setConversationTitleAsync(CharSequence conversationTitle)987 public Runnable setConversationTitleAsync(CharSequence conversationTitle) { 988 if (!Flags.conversationStyleSetAvatarAsync()) { 989 return () -> setConversationTitle(conversationTitle); 990 } 991 992 // Remove formatting from the title. 993 mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; 994 return NotificationRunnables.NOOP; 995 } 996 997 /** 998 * Sets the conversation title of this conversation. 999 * 1000 * @param conversationTitle the conversation title 1001 */ 1002 @RemotableViewMethod(asyncImpl = "setConversationTitleAsync") setConversationTitle(CharSequence conversationTitle)1003 public void setConversationTitle(CharSequence conversationTitle) { 1004 // Remove formatting from the title. 1005 mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; 1006 } 1007 1008 // TODO (b/217799515) getConversationTitle is not consistent with setConversationTitle 1009 // if you call getConversationTitle() immediately after setConversationTitle(), the result 1010 // will not correctly reflect the new change without calling updateConversationLayout, for 1011 // example. getConversationTitle()1012 public CharSequence getConversationTitle() { 1013 return mConversationText.getText(); 1014 } 1015 removeGroups(ArrayList<MessagingGroup> oldGroups)1016 private void removeGroups(ArrayList<MessagingGroup> oldGroups) { 1017 int size = oldGroups.size(); 1018 for (int i = 0; i < size; i++) { 1019 MessagingGroup group = oldGroups.get(i); 1020 if (!mGroups.contains(group)) { 1021 List<MessagingMessage> messages = group.getMessages(); 1022 boolean wasShown = group.isShown(); 1023 mMessagingLinearLayout.removeView(group); 1024 if (wasShown && !MessagingLinearLayout.isGone(group)) { 1025 mMessagingLinearLayout.addTransientView(group, 0); 1026 group.removeGroupAnimated(() -> { 1027 mMessagingLinearLayout.removeTransientView(group); 1028 group.recycle(); 1029 }); 1030 } else { 1031 // Defer recycling until after the update is done, since we may still need the 1032 // old group around to perform other updates. 1033 mToRecycle.add(group); 1034 } 1035 mMessages.removeAll(messages); 1036 mHistoricMessages.removeAll(messages); 1037 } 1038 } 1039 } 1040 updateTitleAndNamesDisplay()1041 private void updateTitleAndNamesDisplay() { 1042 // Map of unique names to their prefix 1043 Map<CharSequence, String> uniqueNames = mPeopleHelper.mapUniqueNamesToPrefix(mGroups); 1044 1045 // Now that we have the correct symbols, let's look what we have cached 1046 ArrayMap<CharSequence, Icon> cachedAvatars = new ArrayMap<>(); 1047 for (int i = 0; i < mGroups.size(); i++) { 1048 // Let's now set the avatars 1049 MessagingGroup group = mGroups.get(i); 1050 boolean isOwnMessage = group.getSender() == mUser; 1051 CharSequence senderName = group.getSenderName(); 1052 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName) 1053 || (mIsOneToOne && mAvatarReplacement != null && !isOwnMessage)) { 1054 continue; 1055 } 1056 String symbol = uniqueNames.get(senderName); 1057 Icon cachedIcon = group.getAvatarSymbolIfMatching(senderName, 1058 symbol, mLayoutColor); 1059 if (cachedIcon != null) { 1060 cachedAvatars.put(senderName, cachedIcon); 1061 } 1062 } 1063 1064 for (int i = 0; i < mGroups.size(); i++) { 1065 // Let's now set the avatars 1066 MessagingGroup group = mGroups.get(i); 1067 CharSequence senderName = group.getSenderName(); 1068 if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) { 1069 continue; 1070 } 1071 if (mIsOneToOne && mAvatarReplacement != null && group.getSender() != mUser) { 1072 group.setAvatar(mAvatarReplacement); 1073 } else { 1074 Icon cachedIcon = cachedAvatars.get(senderName); 1075 if (cachedIcon == null) { 1076 cachedIcon = mPeopleHelper.createAvatarSymbol(senderName, 1077 uniqueNames.get(senderName), mLayoutColor); 1078 cachedAvatars.put(senderName, cachedIcon); 1079 } 1080 group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), 1081 mLayoutColor); 1082 } 1083 } 1084 } 1085 1086 /** 1087 * async version of {@link ConversationLayout#setLayoutColor} 1088 */ 1089 @RemotableViewMethod setLayoutColorAsync(int color)1090 public Runnable setLayoutColorAsync(int color) { 1091 if (!Flags.conversationStyleSetAvatarAsync()) { 1092 return () -> setLayoutColor(color); 1093 } 1094 1095 mLayoutColor = color; 1096 return NotificationRunnables.NOOP; 1097 } 1098 1099 @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") setLayoutColor(int color)1100 public void setLayoutColor(int color) { 1101 mLayoutColor = color; 1102 } 1103 1104 /** 1105 * async version of {@link ConversationLayout#setIsOneToOne} 1106 */ 1107 @RemotableViewMethod setIsOneToOneAsync(boolean oneToOne)1108 public Runnable setIsOneToOneAsync(boolean oneToOne) { 1109 if (!Flags.conversationStyleSetAvatarAsync()) { 1110 return () -> setIsOneToOne(oneToOne); 1111 } 1112 mIsOneToOne = oneToOne; 1113 return NotificationRunnables.NOOP; 1114 } 1115 1116 @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync") setIsOneToOne(boolean oneToOne)1117 public void setIsOneToOne(boolean oneToOne) { 1118 mIsOneToOne = oneToOne; 1119 } 1120 1121 @RemotableViewMethod setSenderTextColor(int color)1122 public void setSenderTextColor(int color) { 1123 mSenderTextColor = color; 1124 mConversationText.setTextColor(color); 1125 } 1126 1127 /** 1128 * @param color the color of the notification background 1129 */ 1130 @RemotableViewMethod setNotificationBackgroundColor(int color)1131 public void setNotificationBackgroundColor(int color) { 1132 mNotificationBackgroundColor = color; 1133 applyNotificationBackgroundColor(mConversationIconBadgeBg); 1134 } 1135 applyNotificationBackgroundColor(ImageView view)1136 private void applyNotificationBackgroundColor(ImageView view) { 1137 view.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor)); 1138 } 1139 1140 @RemotableViewMethod setMessageTextColor(int color)1141 public void setMessageTextColor(int color) { 1142 mMessageTextColor = color; 1143 } 1144 setUser(Person user)1145 private void setUser(Person user) { 1146 mUser = user; 1147 if (mUser.getIcon() == null) { 1148 Icon userIcon = Icon.createWithResource(getContext(), 1149 R.drawable.messaging_user); 1150 userIcon.setTint(mLayoutColor); 1151 mUser = mUser.toBuilder().setIcon(userIcon).build(); 1152 } 1153 } 1154 createGroupViews(List<List<MessagingMessage>> groups, List<Person> senders, boolean showSpinner)1155 private void createGroupViews(List<List<MessagingMessage>> groups, 1156 List<Person> senders, boolean showSpinner) { 1157 mGroups.clear(); 1158 for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { 1159 List<MessagingMessage> group = groups.get(groupIndex); 1160 MessagingGroup newGroup = null; 1161 // we'll just take the first group that exists or create one there is none 1162 for (int messageIndex = group.size() - 1; messageIndex >= 0; messageIndex--) { 1163 MessagingMessage message = group.get(messageIndex); 1164 newGroup = message.getGroup(); 1165 if (newGroup != null) { 1166 break; 1167 } 1168 } 1169 // Create a new group, adding it to the linear layout as well 1170 if (newGroup == null) { 1171 newGroup = MessagingGroup.createGroup(mMessagingLinearLayout); 1172 mAddedGroups.add(newGroup); 1173 } else if (newGroup.getParent() != mMessagingLinearLayout) { 1174 throw new IllegalStateException( 1175 "group parent was " + newGroup.getParent() + " but expected " 1176 + mMessagingLinearLayout); 1177 } 1178 newGroup.setImageDisplayLocation(mIsCollapsed 1179 ? IMAGE_DISPLAY_LOCATION_EXTERNAL 1180 : IMAGE_DISPLAY_LOCATION_INLINE); 1181 newGroup.setIsInConversation(true); 1182 newGroup.setLayoutColor(mLayoutColor); 1183 newGroup.setTextColors(mSenderTextColor, mMessageTextColor); 1184 Person sender = senders.get(groupIndex); 1185 CharSequence nameOverride = null; 1186 if (sender != mUser && mNameReplacement != null) { 1187 nameOverride = mNameReplacement; 1188 } 1189 newGroup.setShowingAvatar(!mIsOneToOne && !mIsCollapsed); 1190 newGroup.setSingleLine(mIsCollapsed 1191 ? !android.app.Flags.nmCollapsedLines() && TextUtils.isEmpty(mSummarizedContent) 1192 : false); 1193 newGroup.setIsCollapsed(mIsCollapsed); 1194 newGroup.setSender(sender, nameOverride); 1195 newGroup.setSending(groupIndex == (groups.size() - 1) && showSpinner); 1196 mGroups.add(newGroup); 1197 1198 // Reposition to the correct place (if we're re-using a group) 1199 if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) { 1200 mMessagingLinearLayout.removeView(newGroup); 1201 mMessagingLinearLayout.addView(newGroup, groupIndex); 1202 } 1203 newGroup.setMessages(group); 1204 } 1205 1206 if (Flags.dropNonExistingMessages()) { 1207 // remove groups from mAddedGroups when they are no longer in mGroups. 1208 mAddedGroups.removeIf( 1209 messagingGroup -> !mGroups.contains(messagingGroup)); 1210 } 1211 } 1212 1213 /** 1214 * Finds groups and senders from the given messaging messages and fills outGroups and outSenders 1215 */ findGroups(List<MessagingMessage> historicMessages, List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups, List<Person> outSenders)1216 private void findGroups(List<MessagingMessage> historicMessages, 1217 List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> outGroups, 1218 List<Person> outSenders) { 1219 CharSequence currentSenderKey = null; 1220 List<MessagingMessage> currentGroup = null; 1221 int histSize = historicMessages.size(); 1222 for (int i = 0; i < histSize + messages.size(); i++) { 1223 MessagingMessage message; 1224 if (i < histSize) { 1225 message = historicMessages.get(i); 1226 } else { 1227 message = messages.get(i - histSize); 1228 } 1229 boolean isNewGroup = currentGroup == null; 1230 Person sender = 1231 message.getMessage() == null ? null : message.getMessage().getSenderPerson(); 1232 CharSequence key = getKey(sender); 1233 isNewGroup |= !TextUtils.equals(key, currentSenderKey); 1234 if (isNewGroup) { 1235 currentGroup = new ArrayList<>(); 1236 outGroups.add(currentGroup); 1237 if (sender == null) { 1238 sender = user; 1239 } else { 1240 // Remove all formatting from the sender name 1241 sender = sender.toBuilder().setName(Objects.toString(sender.getName())).build(); 1242 } 1243 outSenders.add(sender); 1244 currentSenderKey = key; 1245 } 1246 currentGroup.add(message); 1247 } 1248 } 1249 getKey(Person person)1250 private CharSequence getKey(Person person) { 1251 return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); 1252 } 1253 loadConversationHeaderData(boolean isOneToOne, CharSequence conversationTitle, Icon shortcutIcon, Icon largeIcon, List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> groups, int layoutColor)1254 private ConversationHeaderData loadConversationHeaderData(boolean isOneToOne, 1255 CharSequence conversationTitle, Icon shortcutIcon, Icon largeIcon, 1256 List<MessagingMessage> messages, 1257 Person user, 1258 List<List<MessagingMessage>> groups, int layoutColor) { 1259 Icon conversationIcon = shortcutIcon; 1260 CharSequence conversationText = conversationTitle; 1261 final CharSequence userKey = getKey(user); 1262 if (isOneToOne) { 1263 for (int i = messages.size() - 1; i >= 0; i--) { 1264 final Notification.MessagingStyle.Message message = messages.get(i).getMessage(); 1265 final Person sender = message.getSenderPerson(); 1266 final CharSequence senderKey = getKey(sender); 1267 if ((sender != null && senderKey != userKey) || i == 0) { 1268 if (conversationText == null || conversationText.isEmpty()) { 1269 conversationText = sender != null ? sender.getName() : ""; 1270 } 1271 if (conversationIcon == null) { 1272 conversationIcon = sender != null ? sender.getIcon() 1273 : mPeopleHelper.createAvatarSymbol(conversationText, "", 1274 layoutColor); 1275 } 1276 break; 1277 } 1278 } 1279 } 1280 if (android.app.Flags.cleanUpSpansAndNewLines() && conversationText != null) { 1281 // remove formatting from title. 1282 conversationText = conversationText.toString(); 1283 } 1284 1285 if (conversationIcon == null) { 1286 conversationIcon = largeIcon; 1287 } 1288 1289 if (isOneToOne || conversationIcon != null) { 1290 return new ConversationHeaderData( 1291 conversationText, 1292 new OneToOneConversationAvatarData( 1293 resolveAvatarImageForOneToOne(conversationIcon))); 1294 } 1295 1296 final List<List<Notification.MessagingStyle.Message>> groupMessages = new ArrayList<>(); 1297 for (int i = 0; i < groups.size(); i++) { 1298 final List<Notification.MessagingStyle.Message> groupMessage = new ArrayList<>(); 1299 for (int j = 0; j < groups.get(i).size(); j++) { 1300 groupMessage.add(groups.get(i).get(j).getMessage()); 1301 } 1302 groupMessages.add(groupMessage); 1303 } 1304 1305 final PeopleHelper.NameToPrefixMap nameToPrefixMap = 1306 mPeopleHelper.mapUniqueNamesToPrefixWithGroupList(groupMessages); 1307 1308 Icon lastIcon = null; 1309 Icon secondLastIcon = null; 1310 1311 CharSequence lastKey = null; 1312 1313 for (int i = groups.size() - 1; i >= 0; i--) { 1314 final Notification.MessagingStyle.Message message = groups.get(i).get(0).getMessage(); 1315 final Person sender = 1316 message.getSenderPerson() != null ? message.getSenderPerson() : user; 1317 final CharSequence senderKey = getKey(sender); 1318 final boolean notUser = senderKey != userKey; 1319 final boolean notIncluded = senderKey != lastKey; 1320 1321 if ((notUser && notIncluded) || (i == 0 && lastKey == null)) { 1322 if (lastIcon == null) { 1323 if (sender.getIcon() != null) { 1324 lastIcon = sender.getIcon(); 1325 } else { 1326 final CharSequence senderName = 1327 sender.getName() != null ? sender.getName() : ""; 1328 lastIcon = mPeopleHelper.createAvatarSymbol( 1329 senderName, nameToPrefixMap.getPrefix(senderName), 1330 layoutColor); 1331 } 1332 lastKey = senderKey; 1333 } else { 1334 if (sender.getIcon() != null) { 1335 secondLastIcon = sender.getIcon(); 1336 } else { 1337 final CharSequence senderName = 1338 sender.getName() != null ? sender.getName() : ""; 1339 secondLastIcon = mPeopleHelper.createAvatarSymbol( 1340 senderName, nameToPrefixMap.getPrefix(senderName), 1341 layoutColor); 1342 } 1343 break; 1344 } 1345 } 1346 } 1347 1348 if (lastIcon == null) { 1349 lastIcon = mPeopleHelper.createAvatarSymbol( 1350 "", "", layoutColor); 1351 } 1352 1353 if (secondLastIcon == null) { 1354 secondLastIcon = mPeopleHelper.createAvatarSymbol( 1355 "", "", layoutColor); 1356 } 1357 1358 return new ConversationHeaderData( 1359 conversationText, 1360 new GroupConversationAvatarData(resolveAvatarImageForFacePile(lastIcon), 1361 resolveAvatarImageForFacePile(secondLastIcon))); 1362 } 1363 1364 /** 1365 * One To One Conversation Avatars is loaded by CachingIconView(conversation icon view). 1366 */ 1367 @Nullable resolveAvatarImageForOneToOne(Icon conversationIcon)1368 private Drawable resolveAvatarImageForOneToOne(Icon conversationIcon) { 1369 final Drawable conversationIconDrawable = 1370 tryLoadingSizeRestrictedIconForOneToOne(conversationIcon); 1371 if (conversationIconDrawable != null) { 1372 return conversationIconDrawable; 1373 } 1374 // when size restricted icon loading fails, we fallback to icons load drawable. 1375 return loadDrawableFromIcon(conversationIcon); 1376 } 1377 1378 @Nullable tryLoadingSizeRestrictedIconForOneToOne(Icon conversationIcon)1379 private Drawable tryLoadingSizeRestrictedIconForOneToOne(Icon conversationIcon) { 1380 try { 1381 return mConversationIconView.loadSizeRestrictedIcon(conversationIcon); 1382 } catch (Exception ex) { 1383 return null; 1384 } 1385 } 1386 1387 /** 1388 * Group Avatar drawables are loaded by Icon. 1389 */ 1390 @Nullable resolveAvatarImageForFacePile(Icon conversationIcon)1391 private Drawable resolveAvatarImageForFacePile(Icon conversationIcon) { 1392 return loadDrawableFromIcon(conversationIcon); 1393 } 1394 1395 @Nullable loadDrawableFromIcon(Icon conversationIcon)1396 private Drawable loadDrawableFromIcon(Icon conversationIcon) { 1397 try { 1398 return conversationIcon.loadDrawable(getContext()); 1399 } catch (Exception ex) { 1400 return null; 1401 } 1402 } 1403 1404 /** 1405 * Creates new messages, reusing existing ones if they are available. 1406 * 1407 * @param newMessages the messages to parse. 1408 */ createMessages( List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, boolean usePrecomputedText)1409 private List<MessagingMessage> createMessages( 1410 List<Notification.MessagingStyle.Message> newMessages, boolean isHistoric, 1411 boolean usePrecomputedText) { 1412 List<MessagingMessage> result = new ArrayList<>(); 1413 for (int i = 0; i < newMessages.size(); i++) { 1414 Notification.MessagingStyle.Message m = newMessages.get(i); 1415 MessagingMessage message = findAndRemoveMatchingMessage(m); 1416 if (message == null) { 1417 message = MessagingMessage.createMessage(this, m, 1418 mImageResolver, usePrecomputedText); 1419 } 1420 message.setIsHistoric(isHistoric); 1421 result.add(message); 1422 } 1423 return result; 1424 } 1425 findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m)1426 private MessagingMessage findAndRemoveMatchingMessage(Notification.MessagingStyle.Message m) { 1427 for (int i = 0; i < mMessages.size(); i++) { 1428 MessagingMessage existing = mMessages.get(i); 1429 if (existing.sameAs(m)) { 1430 mMessages.remove(i); 1431 return existing; 1432 } 1433 } 1434 for (int i = 0; i < mHistoricMessages.size(); i++) { 1435 MessagingMessage existing = mHistoricMessages.get(i); 1436 if (existing.sameAs(m)) { 1437 mHistoricMessages.remove(i); 1438 return existing; 1439 } 1440 } 1441 return null; 1442 } 1443 showHistoricMessages(boolean show)1444 public void showHistoricMessages(boolean show) { 1445 mShowHistoricMessages = show; 1446 updateHistoricMessageVisibility(); 1447 } 1448 updateHistoricMessageVisibility()1449 private void updateHistoricMessageVisibility() { 1450 int numHistoric = mHistoricMessages.size(); 1451 for (int i = 0; i < numHistoric; i++) { 1452 MessagingMessage existing = mHistoricMessages.get(i); 1453 existing.setVisibility(mShowHistoricMessages ? VISIBLE : GONE); 1454 } 1455 int numGroups = mGroups.size(); 1456 for (int i = 0; i < numGroups; i++) { 1457 MessagingGroup group = mGroups.get(i); 1458 int visibleChildren = 0; 1459 List<MessagingMessage> messages = group.getMessages(); 1460 int numGroupMessages = messages.size(); 1461 for (int j = 0; j < numGroupMessages; j++) { 1462 MessagingMessage message = messages.get(j); 1463 if (message.getVisibility() != GONE) { 1464 visibleChildren++; 1465 } 1466 } 1467 if (visibleChildren > 0 && group.getVisibility() == GONE) { 1468 group.setVisibility(VISIBLE); 1469 } else if (visibleChildren == 0 && group.getVisibility() != GONE) { 1470 group.setVisibility(GONE); 1471 } 1472 } 1473 } 1474 1475 1476 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)1477 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1478 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1479 1480 // ConversationLayout needs to set its height to its biggest child to show the content 1481 // properly. 1482 // FrameLayout measures its match_parent children twice when any of FLs dimension is not 1483 // specified. However, its sets its own dimensions before the second measurement pass. 1484 // Content CutOff happens when children have bigger height on its second measurement. 1485 if (conversationLayoutUseMaximumChildHeight()) { 1486 int maxHeight = getMeasuredHeight(); 1487 final int count = getChildCount(); 1488 1489 for (int i = 0; i < count; i++) { 1490 final View child = getChildAt(i); 1491 if (child == null || child.getVisibility() == GONE) { 1492 continue; 1493 } 1494 1495 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1496 maxHeight = Math.max(maxHeight, 1497 child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); 1498 } 1499 maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); 1500 if (maxHeight != getMeasuredHeight()) { 1501 setMeasuredDimension(getMeasuredWidth(), maxHeight); 1502 } 1503 } 1504 } 1505 1506 @Override onLayout(boolean changed, int left, int top, int right, int bottom)1507 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 1508 super.onLayout(changed, left, top, right, bottom); 1509 if (!mAddedGroups.isEmpty()) { 1510 getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 1511 @Override 1512 public boolean onPreDraw() { 1513 for (MessagingGroup group : mAddedGroups) { 1514 if (!group.isShown()) { 1515 continue; 1516 } 1517 MessagingPropertyAnimator.fadeIn(group.getAvatar()); 1518 MessagingPropertyAnimator.fadeIn(group.getSenderView()); 1519 MessagingPropertyAnimator.startLocalTranslationFrom(group, 1520 group.getHeight(), LINEAR_OUT_SLOW_IN); 1521 } 1522 mAddedGroups.clear(); 1523 getViewTreeObserver().removeOnPreDrawListener(this); 1524 return true; 1525 } 1526 }); 1527 } 1528 mTouchDelegate.clear(); 1529 if (mFeedbackIcon.getVisibility() == VISIBLE) { 1530 float width = Math.max(mMinTouchSize, mFeedbackIcon.getWidth()); 1531 float height = Math.max(mMinTouchSize, mFeedbackIcon.getHeight()); 1532 final Rect feedbackTouchRect = new Rect(); 1533 feedbackTouchRect.left = (int) ((mFeedbackIcon.getLeft() + mFeedbackIcon.getRight()) 1534 / 2.0f - width / 2.0f); 1535 feedbackTouchRect.top = (int) ((mFeedbackIcon.getTop() + mFeedbackIcon.getBottom()) 1536 / 2.0f - height / 2.0f); 1537 feedbackTouchRect.bottom = (int) (feedbackTouchRect.top + height); 1538 feedbackTouchRect.right = (int) (feedbackTouchRect.left + width); 1539 1540 getRelativeTouchRect(feedbackTouchRect, mFeedbackIcon); 1541 mTouchDelegate.add(new TouchDelegate(feedbackTouchRect, mFeedbackIcon)); 1542 } 1543 1544 setTouchDelegate(mTouchDelegate); 1545 } 1546 getRelativeTouchRect(Rect touchRect, View view)1547 private void getRelativeTouchRect(Rect touchRect, View view) { 1548 ViewGroup viewGroup = (ViewGroup) view.getParent(); 1549 while (viewGroup != this) { 1550 touchRect.offset(viewGroup.getLeft(), viewGroup.getTop()); 1551 viewGroup = (ViewGroup) viewGroup.getParent(); 1552 } 1553 } 1554 getMessagingLinearLayout()1555 public MessagingLinearLayout getMessagingLinearLayout() { 1556 return mMessagingLinearLayout; 1557 } 1558 getImageMessageContainer()1559 public @NonNull ViewGroup getImageMessageContainer() { 1560 return mImageMessageContainer; 1561 } 1562 getMessagingGroups()1563 public ArrayList<MessagingGroup> getMessagingGroups() { 1564 return mGroups; 1565 } 1566 updateExpandButton()1567 private void updateExpandButton() { 1568 if (notificationsRedesignTemplates()) { 1569 return; 1570 } 1571 1572 int buttonGravity; 1573 ViewGroup newContainer; 1574 if (mIsCollapsed) { 1575 buttonGravity = Gravity.CENTER; 1576 // NOTE(b/182474419): In order for the touch target of the expand button to be the full 1577 // height of the notification, we would want the mExpandButtonContainer's height to be 1578 // set to WRAP_CONTENT (or 88dp) when in the collapsed state. Unfortunately, that 1579 // causes an unstable remeasuring infinite loop when the unread count is visible, 1580 // causing the layout to occasionally hide the messages. As an aside, that naive 1581 // solution also causes an undesirably large gap between content and smart replies. 1582 newContainer = mExpandButtonAndContentContainer; 1583 } else { 1584 buttonGravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP; 1585 newContainer = mExpandButtonContainerA11yContainer; 1586 } 1587 mExpandButton.setExpanded(!mIsCollapsed); 1588 1589 // We need to make sure that the expand button is in the linearlayout pushing over the 1590 // content when collapsed, but allows the content to flow under it when expanded. 1591 if (newContainer != mExpandButtonContainer.getParent()) { 1592 ((ViewGroup) mExpandButtonContainer.getParent()).removeView(mExpandButtonContainer); 1593 newContainer.addView(mExpandButtonContainer); 1594 } 1595 1596 // update if the expand button is centered 1597 LinearLayout.LayoutParams layoutParams = 1598 (LinearLayout.LayoutParams) mExpandButton.getLayoutParams(); 1599 layoutParams.gravity = buttonGravity; 1600 mExpandButton.setLayoutParams(layoutParams); 1601 } 1602 updateContentEndPaddings()1603 private void updateContentEndPaddings() { 1604 if (notificationsRedesignTemplates()) { 1605 return; 1606 } 1607 1608 // Let's make sure the conversation header can't run into the expand button when we're 1609 // collapsed and update the paddings of the content 1610 int headerPaddingEnd; 1611 int contentPaddingEnd; 1612 if (!mExpandable) { 1613 headerPaddingEnd = 0; 1614 contentPaddingEnd = mContentMarginEnd; 1615 } else if (mIsCollapsed) { 1616 headerPaddingEnd = 0; 1617 contentPaddingEnd = 0; 1618 } else { 1619 headerPaddingEnd = mNotificationHeaderExpandedPadding; 1620 contentPaddingEnd = mContentMarginEnd; 1621 } 1622 mConversationHeader.setPaddingRelative( 1623 mConversationHeader.getPaddingStart(), 1624 mConversationHeader.getPaddingTop(), 1625 headerPaddingEnd, 1626 mConversationHeader.getPaddingBottom()); 1627 1628 mContentContainer.setPaddingRelative( 1629 mContentContainer.getPaddingStart(), 1630 mContentContainer.getPaddingTop(), 1631 contentPaddingEnd, 1632 mContentContainer.getPaddingBottom()); 1633 } 1634 onAppNameVisibilityChanged()1635 private void onAppNameVisibilityChanged() { 1636 if (notificationsRedesignTemplates()) { 1637 return; 1638 } 1639 1640 boolean appNameGone = mAppName.getVisibility() == GONE; 1641 if (appNameGone != mAppNameGone) { 1642 mAppNameGone = appNameGone; 1643 updateAppNameDividerVisibility(); 1644 } 1645 } 1646 updateAppNameDividerVisibility()1647 private void updateAppNameDividerVisibility() { 1648 if (notificationsRedesignTemplates()) { 1649 return; 1650 } 1651 1652 mAppNameDivider.setVisibility(mAppNameGone ? GONE : VISIBLE); 1653 } 1654 updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener)1655 public void updateExpandability(boolean expandable, @Nullable OnClickListener onClickListener) { 1656 if (notificationsRedesignTemplates()) { 1657 return; 1658 } 1659 1660 mExpandable = expandable; 1661 if (expandable) { 1662 mExpandButtonContainer.setVisibility(VISIBLE); 1663 mExpandButton.setOnClickListener(onClickListener); 1664 mConversationIconContainer.setOnClickListener(onClickListener); 1665 } else { 1666 mExpandButtonContainer.setVisibility(GONE); 1667 mConversationIconContainer.setOnClickListener(null); 1668 } 1669 mExpandButton.setVisibility(VISIBLE); 1670 updateContentEndPaddings(); 1671 } 1672 1673 @Override setMessagingClippingDisabled(boolean clippingDisabled)1674 public void setMessagingClippingDisabled(boolean clippingDisabled) { 1675 mMessagingLinearLayout.setClipBounds(clippingDisabled ? null : mMessagingClipRect); 1676 } 1677 1678 @Nullable getConversationSenderName()1679 public CharSequence getConversationSenderName() { 1680 if (mGroups.isEmpty()) { 1681 return null; 1682 } 1683 final CharSequence name = mGroups.get(mGroups.size() - 1).getSenderName(); 1684 return getResources().getString(R.string.conversation_single_line_name_display, name); 1685 } 1686 isOneToOne()1687 public boolean isOneToOne() { 1688 return mIsOneToOne; 1689 } 1690 1691 @Nullable getConversationText()1692 public CharSequence getConversationText() { 1693 if (mSummarizedContent != null) { 1694 return mSummarizedContent; 1695 } 1696 if (mMessages.isEmpty()) { 1697 return null; 1698 } 1699 final MessagingMessage messagingMessage = mMessages.get(mMessages.size() - 1); 1700 final CharSequence text = messagingMessage.getMessage() == null ? null 1701 : messagingMessage.getMessage().getText(); 1702 if (text == null && messagingMessage instanceof MessagingImageMessage) { 1703 final String unformatted = 1704 getResources().getString(R.string.conversation_single_line_image_placeholder); 1705 SpannableString spannableString = new SpannableString(unformatted); 1706 spannableString.setSpan( 1707 new StyleSpan(Typeface.ITALIC), 1708 0, 1709 spannableString.length(), 1710 Spannable.SPAN_INCLUSIVE_EXCLUSIVE); 1711 return spannableString; 1712 } 1713 return text; 1714 } 1715 1716 @Nullable getConversationIcon()1717 public Icon getConversationIcon() { 1718 return mConversationIcon; 1719 } 1720 1721 @Nullable getConversationHeaderData()1722 public ConversationHeaderData getConversationHeaderData() { 1723 return mConversationHeaderData; 1724 } 1725 1726 private static class TouchDelegateComposite extends TouchDelegate { 1727 private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>(); 1728 TouchDelegateComposite(View view)1729 private TouchDelegateComposite(View view) { 1730 super(new Rect(), view); 1731 } 1732 add(TouchDelegate delegate)1733 public void add(TouchDelegate delegate) { 1734 mDelegates.add(delegate); 1735 } 1736 clear()1737 public void clear() { 1738 mDelegates.clear(); 1739 } 1740 1741 @Override onTouchEvent(MotionEvent event)1742 public boolean onTouchEvent(MotionEvent event) { 1743 float x = event.getX(); 1744 float y = event.getY(); 1745 for (TouchDelegate delegate : mDelegates) { 1746 event.setLocation(x, y); 1747 if (delegate.onTouchEvent(event)) { 1748 return true; 1749 } 1750 } 1751 return false; 1752 } 1753 } 1754 } 1755