Loading core/java/android/widget/flags/notification_widget_flags.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -16,3 +16,13 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "conversation_style_set_avatar_async" namespace: "systemui" description: "Offloads conversation avatar drawable loading to the background thread" bug: "305540309" metadata { purpose: PURPOSE_BUGFIX } } No newline at end of file core/java/com/android/internal/widget/ConversationAvatarData.java 0 → 100644 +42 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.graphics.drawable.Drawable; /** * @hide */ interface ConversationAvatarData { final class OneToOneConversationAvatarData implements ConversationAvatarData { final Drawable mDrawable; OneToOneConversationAvatarData(Drawable drawable) { mDrawable = drawable; } } final class GroupConversationAvatarData implements ConversationAvatarData { final Drawable mLastIcon; final Drawable mSecondLastIcon; GroupConversationAvatarData(Drawable lastIcon, Drawable secondLastIcon) { mLastIcon = lastIcon; mSecondLastIcon = secondLastIcon; } } } core/java/com/android/internal/widget/ConversationHeaderData.java 0 → 100644 +44 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.annotation.Nullable; /** * @hide */ final class ConversationHeaderData { private final CharSequence mConversationText; private final ConversationAvatarData mConversationAvatarData; ConversationHeaderData(CharSequence conversationText, ConversationAvatarData conversationAvatarData) { mConversationText = conversationText; mConversationAvatarData = conversationAvatarData; } @Nullable CharSequence getConversationText() { return mConversationText; } @Nullable ConversationAvatarData getConversationAvatar() { return mConversationAvatarData; } } core/java/com/android/internal/widget/ConversationLayout.java +293 −15 Original line number Diff line number Diff line Loading @@ -34,8 +34,10 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.text.Spannable; Loading @@ -59,8 +61,11 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RemoteViews; import android.widget.TextView; import android.widget.flags.Flags; import com.android.internal.R; import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData; import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData; import java.util.ArrayList; import java.util.List; Loading Loading @@ -403,11 +408,14 @@ public class ConversationLayout extends FrameLayout */ @RemotableViewMethod(asyncImpl = "setDataAsync") public void setData(Bundle extras) { bind(parseMessagingData(extras, /* usePrecomputedText= */ false)); bind(parseMessagingData(extras, /* usePrecomputedText= */ false, /*includeConversationIcon= */false)); } @NonNull private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) { private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText, boolean includeConversationIcon) { Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); List<Notification.MessagingStyle.Message> newMessages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); Loading Loading @@ -438,8 +446,20 @@ public class ConversationLayout extends FrameLayout // Lets first find the groups (populate `groups` and `senders`) findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders); // load conversation header data, avatar and title. final ConversationHeaderData conversationHeaderData; if (includeConversationIcon && Flags.conversationStyleSetAvatarAsync()) { conversationHeaderData = loadConversationHeaderData(mIsOneToOne, mConversationTitle, mShortcutIcon, mLargeIcon, newMessagingMessages, user, groups, mLayoutColor); } else { conversationHeaderData = null; } return new MessagingData(user, showSpinner, unreadCount, newHistoricMessagingMessages, newMessagingMessages, groups, senders); newHistoricMessagingMessages, newMessagingMessages, groups, senders, conversationHeaderData); } /** Loading @@ -457,7 +477,9 @@ public class ConversationLayout extends FrameLayout } final MessagingData messagingData = parseMessagingData(extras, /* usePrecomputedText= */ true); parseMessagingData(extras, /* usePrecomputedText= */ true, /*includeConversationIcon=*/true); return () -> { finalizeInflate(messagingData.getHistoricMessagingMessages()); Loading Loading @@ -536,11 +558,10 @@ public class ConversationLayout extends FrameLayout mMessages = messagingData.getNewMessagingMessages(); mHistoricMessages = messagingData.getHistoricMessagingMessages(); updateHistoricMessageVisibility(); updateTitleAndNamesDisplay(); updateConversationLayout(); updateConversationLayout(messagingData); // Recycle everything at the end of the update, now that we know it's no longer needed. for (MessagingLinearLayout.MessagingChild child : mToRecycle) { Loading @@ -552,7 +573,31 @@ public class ConversationLayout extends FrameLayout /** * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc); */ private void updateConversationLayout() { private void updateConversationLayout(MessagingData messagingData) { if (!Flags.conversationStyleSetAvatarAsync()) { computeAndSetConversationAvatarAndName(); } else { ConversationHeaderData conversationHeaderData = messagingData.getConversationHeaderData(); if (conversationHeaderData == null) { conversationHeaderData = loadConversationHeaderData(mIsOneToOne, mConversationTitle, mShortcutIcon, mLargeIcon, mMessages, mUser, messagingData.getGroups(), mLayoutColor); } setConversationAvatarAndNameFromData(conversationHeaderData); } updateAppName(); updateIconPositionAndSize(); updateImageMessages(); updatePaddingsBasedOnContentAvailability(); updateActionListPadding(); updateAppNameDividerVisibility(); } @Deprecated private void computeAndSetConversationAvatarAndName() { // Set avatar and name CharSequence conversationText = mConversationTitle; mConversationIcon = mShortcutIcon; Loading Loading @@ -603,12 +648,43 @@ public class ConversationLayout extends FrameLayout // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) // This needs to happen after all of the above o update all of the groups mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); updateAppName(); updateIconPositionAndSize(); updateImageMessages(); updatePaddingsBasedOnContentAvailability(); updateActionListPadding(); updateAppNameDividerVisibility(); } private void setConversationAvatarAndNameFromData( ConversationHeaderData conversationHeaderData) { final OneToOneConversationAvatarData oneToOneConversationDrawable; final GroupConversationAvatarData groupConversationAvatarData; final ConversationAvatarData conversationAvatar = conversationHeaderData.getConversationAvatar(); if (conversationAvatar instanceof OneToOneConversationAvatarData) { oneToOneConversationDrawable = ((OneToOneConversationAvatarData) conversationAvatar); groupConversationAvatarData = null; } else { oneToOneConversationDrawable = null; groupConversationAvatarData = ((GroupConversationAvatarData) conversationAvatar); } if (oneToOneConversationDrawable != null) { mConversationIconView.setVisibility(VISIBLE); mConversationFacePile.setVisibility(GONE); mConversationIconView.setImageDrawable(oneToOneConversationDrawable.mDrawable); } else { mConversationIconView.setVisibility(GONE); // This will also inflate it! mConversationFacePile.setVisibility(VISIBLE); // rebind the value to the inflated view instead of the stub mConversationFacePile = findViewById(R.id.conversation_face_pile); bindFacePile(groupConversationAvatarData); } CharSequence conversationText = conversationHeaderData.getConversationText(); if (TextUtils.isEmpty(conversationText)) { conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName; } mConversationText.setText(conversationText); // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) // This needs to happen after all of the above o update all of the groups mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); } private void updateActionListPadding() { Loading Loading @@ -675,7 +751,12 @@ public class ConversationLayout extends FrameLayout topView.setImageIcon(secondLastIcon); } @Deprecated private void bindFacePile() { bindFacePile(null); } private void bindFacePile(@Nullable GroupConversationAvatarData groupConversationAvatarData) { ImageView bottomBackground = mConversationFacePile.findViewById( R.id.conversation_face_pile_bottom_background); ImageView bottomView = mConversationFacePile.findViewById( Loading @@ -683,7 +764,13 @@ public class ConversationLayout extends FrameLayout ImageView topView = mConversationFacePile.findViewById( R.id.conversation_face_pile_top); if (groupConversationAvatarData == null) { bindFacePile(bottomBackground, bottomView, topView); } else { bindFacePileWithDrawable(bottomBackground, bottomView, topView, groupConversationAvatarData); } int conversationAvatarSize; int facepileAvatarSize; Loading Loading @@ -718,6 +805,13 @@ public class ConversationLayout extends FrameLayout bottomBackground.setLayoutParams(layoutParams); } private void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, ImageView topView, GroupConversationAvatarData groupConversationAvatarData) { applyNotificationBackgroundColor(bottomBackground); bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon); topView.setImageDrawable(groupConversationAvatarData.mSecondLastIcon); } private void updateAppName() { mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE); } Loading Loading @@ -789,22 +883,62 @@ public class ConversationLayout extends FrameLayout mMessagingLinearLayout.getPaddingBottom()); } /** * async version of {@link ConversationLayout#setLargeIcon} */ @RemotableViewMethod public Runnable setLargeIconAsync(Icon largeIcon) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setLargeIcon(largeIcon); } mLargeIcon = largeIcon; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setLargeIconAsync") public void setLargeIcon(Icon largeIcon) { mLargeIcon = largeIcon; } /** * async version of {@link ConversationLayout#setShortcutIcon} */ @RemotableViewMethod public Runnable setShortcutIconAsync(Icon shortcutIcon) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setShortcutIcon(shortcutIcon); } mShortcutIcon = shortcutIcon; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setShortcutIconAsync") public void setShortcutIcon(Icon shortcutIcon) { mShortcutIcon = shortcutIcon; } /** * async version of {@link ConversationLayout#setConversationTitle} */ @RemotableViewMethod public Runnable setConversationTitleAsync(CharSequence conversationTitle) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setConversationTitle(conversationTitle); } // Remove formatting from the title. mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; return NotificationRunnables.NOOP; } /** * Sets the conversation title of this conversation. * * @param conversationTitle the conversation title */ @RemotableViewMethod @RemotableViewMethod(asyncImpl = "setConversationTitleAsync") public void setConversationTitle(CharSequence conversationTitle) { // Remove formatting from the title. mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; Loading Loading @@ -888,12 +1022,37 @@ public class ConversationLayout extends FrameLayout } } /** * async version of {@link ConversationLayout#setLayoutColor} */ @RemotableViewMethod public Runnable setLayoutColorAsync(int color) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setLayoutColor(color); } mLayoutColor = color; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") public void setLayoutColor(int color) { mLayoutColor = color; } /** * async version of {@link ConversationLayout#setIsOneToOne} */ @RemotableViewMethod public Runnable setIsOneToOneAsync(boolean oneToOne) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setIsOneToOne(oneToOne); } mIsOneToOne = oneToOne; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync") public void setIsOneToOne(boolean oneToOne) { mIsOneToOne = oneToOne; } Loading Loading @@ -1022,6 +1181,125 @@ public class ConversationLayout extends FrameLayout return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); } private ConversationHeaderData loadConversationHeaderData(boolean isOneToOne, CharSequence conversationTitle, Icon shortcutIcon, Icon largeIcon, List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> groups, int layoutColor) { Icon conversationIcon = shortcutIcon; CharSequence conversationText = conversationTitle; final CharSequence userKey = getKey(user); if (isOneToOne) { for (int i = messages.size() - 1; i >= 0; i--) { final Notification.MessagingStyle.Message message = messages.get(i).getMessage(); final Person sender = message.getSenderPerson(); final CharSequence senderKey = getKey(sender); if ((sender != null && senderKey != userKey) || i == 0) { if (conversationText == null || conversationText.length() == 0) { conversationText = sender != null ? sender.getName() : ""; } if (conversationIcon == null) { conversationIcon = sender != null ? sender.getIcon() : mPeopleHelper.createAvatarSymbol(conversationText, "", layoutColor); } break; } } } if (conversationIcon == null) { conversationIcon = largeIcon; } if (isOneToOne || conversationIcon != null) { return new ConversationHeaderData( conversationText, new OneToOneConversationAvatarData( resolveAvatarImage(conversationIcon))); } final List<List<Notification.MessagingStyle.Message>> groupMessages = new ArrayList<>(); for (int i = 0; i < groups.size(); i++) { final List<Notification.MessagingStyle.Message> groupMessage = new ArrayList<>(); for (int j = 0; j < groups.get(i).size(); j++) { groupMessage.add(groups.get(i).get(j).getMessage()); } groupMessages.add(groupMessage); } final PeopleHelper.NameToPrefixMap nameToPrefixMap = mPeopleHelper.mapUniqueNamesToPrefixWithGroupList(groupMessages); Icon lastIcon = null; Icon secondLastIcon = null; CharSequence lastKey = null; for (int i = groups.size() - 1; i >= 0; i--) { final Notification.MessagingStyle.Message message = groups.get(i).get(0).getMessage(); final Person sender = message.getSenderPerson() != null ? message.getSenderPerson() : user; final CharSequence senderKey = getKey(sender); final boolean notUser = senderKey != userKey; final boolean notIncluded = senderKey != lastKey; if ((notUser && notIncluded) || (i == 0 && lastKey == null)) { if (lastIcon == null) { if (sender.getIcon() != null) { lastIcon = sender.getIcon(); } else { final CharSequence senderName = sender.getName() != null ? sender.getName() : ""; lastIcon = mPeopleHelper.createAvatarSymbol( senderName, nameToPrefixMap.getPrefix(senderName), layoutColor); } lastKey = senderKey; } else { if (sender.getIcon() != null) { secondLastIcon = sender.getIcon(); } else { final CharSequence senderName = sender.getName() != null ? sender.getName() : ""; secondLastIcon = mPeopleHelper.createAvatarSymbol( senderName, nameToPrefixMap.getPrefix(senderName), layoutColor); } break; } } } if (lastIcon == null) { lastIcon = mPeopleHelper.createAvatarSymbol( "", "", layoutColor); } if (secondLastIcon == null) { secondLastIcon = mPeopleHelper.createAvatarSymbol( "", "", layoutColor); } return new ConversationHeaderData( conversationText, new GroupConversationAvatarData(resolveAvatarImage(lastIcon), resolveAvatarImage(secondLastIcon))); } /** * {@link ImageResolver#loadImage(Uri)} accepts Uri to load images. However Conversation Avatars * are received as Icon. So, we can't make use of ImageResolver. */ @Nullable private Drawable resolveAvatarImage(Icon conversationIcon) { try { return LocalImageResolver.resolveImage(conversationIcon, getContext()); } catch (Exception ex) { return null; } } /** * Creates new messages, reusing existing ones if they are available. * Loading core/java/com/android/internal/widget/MessagingData.java +12 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.internal.widget; import android.annotation.Nullable; import android.app.Person; import java.util.List; Loading @@ -32,6 +33,8 @@ final class MessagingData { private final List<Person> mSenders; private final int mUnreadCount; private ConversationHeaderData mConversationHeaderData; MessagingData(Person user, boolean showSpinner, List<MessagingMessage> historicMessagingMessages, List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups, Loading @@ -39,7 +42,7 @@ final class MessagingData { this(user, showSpinner, /* unreadCount= */0, historicMessagingMessages, newMessagingMessages, groups, senders); senders, null); } MessagingData(Person user, boolean showSpinner, Loading @@ -47,7 +50,8 @@ final class MessagingData { List<MessagingMessage> historicMessagingMessages, List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups, List<Person> senders) { List<Person> senders, @Nullable ConversationHeaderData conversationHeaderData) { mUser = user; mShowSpinner = showSpinner; mUnreadCount = unreadCount; Loading @@ -55,6 +59,7 @@ final class MessagingData { mNewMessagingMessages = newMessagingMessages; mGroups = groups; mSenders = senders; mConversationHeaderData = conversationHeaderData; } public Person getUser() { Loading Loading @@ -84,4 +89,9 @@ final class MessagingData { public List<List<MessagingMessage>> getGroups() { return mGroups; } @Nullable public ConversationHeaderData getConversationHeaderData() { return mConversationHeaderData; } } Loading
core/java/android/widget/flags/notification_widget_flags.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -16,3 +16,13 @@ flag { purpose: PURPOSE_BUGFIX } } flag { name: "conversation_style_set_avatar_async" namespace: "systemui" description: "Offloads conversation avatar drawable loading to the background thread" bug: "305540309" metadata { purpose: PURPOSE_BUGFIX } } No newline at end of file
core/java/com/android/internal/widget/ConversationAvatarData.java 0 → 100644 +42 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.graphics.drawable.Drawable; /** * @hide */ interface ConversationAvatarData { final class OneToOneConversationAvatarData implements ConversationAvatarData { final Drawable mDrawable; OneToOneConversationAvatarData(Drawable drawable) { mDrawable = drawable; } } final class GroupConversationAvatarData implements ConversationAvatarData { final Drawable mLastIcon; final Drawable mSecondLastIcon; GroupConversationAvatarData(Drawable lastIcon, Drawable secondLastIcon) { mLastIcon = lastIcon; mSecondLastIcon = secondLastIcon; } } }
core/java/com/android/internal/widget/ConversationHeaderData.java 0 → 100644 +44 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.annotation.Nullable; /** * @hide */ final class ConversationHeaderData { private final CharSequence mConversationText; private final ConversationAvatarData mConversationAvatarData; ConversationHeaderData(CharSequence conversationText, ConversationAvatarData conversationAvatarData) { mConversationText = conversationText; mConversationAvatarData = conversationAvatarData; } @Nullable CharSequence getConversationText() { return mConversationText; } @Nullable ConversationAvatarData getConversationAvatar() { return mConversationAvatarData; } }
core/java/com/android/internal/widget/ConversationLayout.java +293 −15 Original line number Diff line number Diff line Loading @@ -34,8 +34,10 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.text.Spannable; Loading @@ -59,8 +61,11 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RemoteViews; import android.widget.TextView; import android.widget.flags.Flags; import com.android.internal.R; import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData; import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData; import java.util.ArrayList; import java.util.List; Loading Loading @@ -403,11 +408,14 @@ public class ConversationLayout extends FrameLayout */ @RemotableViewMethod(asyncImpl = "setDataAsync") public void setData(Bundle extras) { bind(parseMessagingData(extras, /* usePrecomputedText= */ false)); bind(parseMessagingData(extras, /* usePrecomputedText= */ false, /*includeConversationIcon= */false)); } @NonNull private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText) { private MessagingData parseMessagingData(Bundle extras, boolean usePrecomputedText, boolean includeConversationIcon) { Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); List<Notification.MessagingStyle.Message> newMessages = Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); Loading Loading @@ -438,8 +446,20 @@ public class ConversationLayout extends FrameLayout // Lets first find the groups (populate `groups` and `senders`) findGroups(newHistoricMessagingMessages, newMessagingMessages, user, groups, senders); // load conversation header data, avatar and title. final ConversationHeaderData conversationHeaderData; if (includeConversationIcon && Flags.conversationStyleSetAvatarAsync()) { conversationHeaderData = loadConversationHeaderData(mIsOneToOne, mConversationTitle, mShortcutIcon, mLargeIcon, newMessagingMessages, user, groups, mLayoutColor); } else { conversationHeaderData = null; } return new MessagingData(user, showSpinner, unreadCount, newHistoricMessagingMessages, newMessagingMessages, groups, senders); newHistoricMessagingMessages, newMessagingMessages, groups, senders, conversationHeaderData); } /** Loading @@ -457,7 +477,9 @@ public class ConversationLayout extends FrameLayout } final MessagingData messagingData = parseMessagingData(extras, /* usePrecomputedText= */ true); parseMessagingData(extras, /* usePrecomputedText= */ true, /*includeConversationIcon=*/true); return () -> { finalizeInflate(messagingData.getHistoricMessagingMessages()); Loading Loading @@ -536,11 +558,10 @@ public class ConversationLayout extends FrameLayout mMessages = messagingData.getNewMessagingMessages(); mHistoricMessages = messagingData.getHistoricMessagingMessages(); updateHistoricMessageVisibility(); updateTitleAndNamesDisplay(); updateConversationLayout(); updateConversationLayout(messagingData); // Recycle everything at the end of the update, now that we know it's no longer needed. for (MessagingLinearLayout.MessagingChild child : mToRecycle) { Loading @@ -552,7 +573,31 @@ public class ConversationLayout extends FrameLayout /** * Update the layout according to the data provided (i.e mIsOneToOne, expanded etc); */ private void updateConversationLayout() { private void updateConversationLayout(MessagingData messagingData) { if (!Flags.conversationStyleSetAvatarAsync()) { computeAndSetConversationAvatarAndName(); } else { ConversationHeaderData conversationHeaderData = messagingData.getConversationHeaderData(); if (conversationHeaderData == null) { conversationHeaderData = loadConversationHeaderData(mIsOneToOne, mConversationTitle, mShortcutIcon, mLargeIcon, mMessages, mUser, messagingData.getGroups(), mLayoutColor); } setConversationAvatarAndNameFromData(conversationHeaderData); } updateAppName(); updateIconPositionAndSize(); updateImageMessages(); updatePaddingsBasedOnContentAvailability(); updateActionListPadding(); updateAppNameDividerVisibility(); } @Deprecated private void computeAndSetConversationAvatarAndName() { // Set avatar and name CharSequence conversationText = mConversationTitle; mConversationIcon = mShortcutIcon; Loading Loading @@ -603,12 +648,43 @@ public class ConversationLayout extends FrameLayout // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) // This needs to happen after all of the above o update all of the groups mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); updateAppName(); updateIconPositionAndSize(); updateImageMessages(); updatePaddingsBasedOnContentAvailability(); updateActionListPadding(); updateAppNameDividerVisibility(); } private void setConversationAvatarAndNameFromData( ConversationHeaderData conversationHeaderData) { final OneToOneConversationAvatarData oneToOneConversationDrawable; final GroupConversationAvatarData groupConversationAvatarData; final ConversationAvatarData conversationAvatar = conversationHeaderData.getConversationAvatar(); if (conversationAvatar instanceof OneToOneConversationAvatarData) { oneToOneConversationDrawable = ((OneToOneConversationAvatarData) conversationAvatar); groupConversationAvatarData = null; } else { oneToOneConversationDrawable = null; groupConversationAvatarData = ((GroupConversationAvatarData) conversationAvatar); } if (oneToOneConversationDrawable != null) { mConversationIconView.setVisibility(VISIBLE); mConversationFacePile.setVisibility(GONE); mConversationIconView.setImageDrawable(oneToOneConversationDrawable.mDrawable); } else { mConversationIconView.setVisibility(GONE); // This will also inflate it! mConversationFacePile.setVisibility(VISIBLE); // rebind the value to the inflated view instead of the stub mConversationFacePile = findViewById(R.id.conversation_face_pile); bindFacePile(groupConversationAvatarData); } CharSequence conversationText = conversationHeaderData.getConversationText(); if (TextUtils.isEmpty(conversationText)) { conversationText = mIsOneToOne ? mFallbackChatName : mFallbackGroupChatName; } mConversationText.setText(conversationText); // Update if the groups can hide the sender if they are first (applies to 1:1 conversations) // This needs to happen after all of the above o update all of the groups mPeopleHelper.maybeHideFirstSenderName(mGroups, mIsOneToOne, conversationText); } private void updateActionListPadding() { Loading Loading @@ -675,7 +751,12 @@ public class ConversationLayout extends FrameLayout topView.setImageIcon(secondLastIcon); } @Deprecated private void bindFacePile() { bindFacePile(null); } private void bindFacePile(@Nullable GroupConversationAvatarData groupConversationAvatarData) { ImageView bottomBackground = mConversationFacePile.findViewById( R.id.conversation_face_pile_bottom_background); ImageView bottomView = mConversationFacePile.findViewById( Loading @@ -683,7 +764,13 @@ public class ConversationLayout extends FrameLayout ImageView topView = mConversationFacePile.findViewById( R.id.conversation_face_pile_top); if (groupConversationAvatarData == null) { bindFacePile(bottomBackground, bottomView, topView); } else { bindFacePileWithDrawable(bottomBackground, bottomView, topView, groupConversationAvatarData); } int conversationAvatarSize; int facepileAvatarSize; Loading Loading @@ -718,6 +805,13 @@ public class ConversationLayout extends FrameLayout bottomBackground.setLayoutParams(layoutParams); } private void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, ImageView topView, GroupConversationAvatarData groupConversationAvatarData) { applyNotificationBackgroundColor(bottomBackground); bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon); topView.setImageDrawable(groupConversationAvatarData.mSecondLastIcon); } private void updateAppName() { mAppName.setVisibility(mIsCollapsed ? GONE : VISIBLE); } Loading Loading @@ -789,22 +883,62 @@ public class ConversationLayout extends FrameLayout mMessagingLinearLayout.getPaddingBottom()); } /** * async version of {@link ConversationLayout#setLargeIcon} */ @RemotableViewMethod public Runnable setLargeIconAsync(Icon largeIcon) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setLargeIcon(largeIcon); } mLargeIcon = largeIcon; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setLargeIconAsync") public void setLargeIcon(Icon largeIcon) { mLargeIcon = largeIcon; } /** * async version of {@link ConversationLayout#setShortcutIcon} */ @RemotableViewMethod public Runnable setShortcutIconAsync(Icon shortcutIcon) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setShortcutIcon(shortcutIcon); } mShortcutIcon = shortcutIcon; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setShortcutIconAsync") public void setShortcutIcon(Icon shortcutIcon) { mShortcutIcon = shortcutIcon; } /** * async version of {@link ConversationLayout#setConversationTitle} */ @RemotableViewMethod public Runnable setConversationTitleAsync(CharSequence conversationTitle) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setConversationTitle(conversationTitle); } // Remove formatting from the title. mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; return NotificationRunnables.NOOP; } /** * Sets the conversation title of this conversation. * * @param conversationTitle the conversation title */ @RemotableViewMethod @RemotableViewMethod(asyncImpl = "setConversationTitleAsync") public void setConversationTitle(CharSequence conversationTitle) { // Remove formatting from the title. mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; Loading Loading @@ -888,12 +1022,37 @@ public class ConversationLayout extends FrameLayout } } /** * async version of {@link ConversationLayout#setLayoutColor} */ @RemotableViewMethod public Runnable setLayoutColorAsync(int color) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setLayoutColor(color); } mLayoutColor = color; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") public void setLayoutColor(int color) { mLayoutColor = color; } /** * async version of {@link ConversationLayout#setIsOneToOne} */ @RemotableViewMethod public Runnable setIsOneToOneAsync(boolean oneToOne) { if (!Flags.conversationStyleSetAvatarAsync()) { return () -> setIsOneToOne(oneToOne); } mIsOneToOne = oneToOne; return NotificationRunnables.NOOP; } @RemotableViewMethod(asyncImpl = "setIsOneToOneAsync") public void setIsOneToOne(boolean oneToOne) { mIsOneToOne = oneToOne; } Loading Loading @@ -1022,6 +1181,125 @@ public class ConversationLayout extends FrameLayout return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); } private ConversationHeaderData loadConversationHeaderData(boolean isOneToOne, CharSequence conversationTitle, Icon shortcutIcon, Icon largeIcon, List<MessagingMessage> messages, Person user, List<List<MessagingMessage>> groups, int layoutColor) { Icon conversationIcon = shortcutIcon; CharSequence conversationText = conversationTitle; final CharSequence userKey = getKey(user); if (isOneToOne) { for (int i = messages.size() - 1; i >= 0; i--) { final Notification.MessagingStyle.Message message = messages.get(i).getMessage(); final Person sender = message.getSenderPerson(); final CharSequence senderKey = getKey(sender); if ((sender != null && senderKey != userKey) || i == 0) { if (conversationText == null || conversationText.length() == 0) { conversationText = sender != null ? sender.getName() : ""; } if (conversationIcon == null) { conversationIcon = sender != null ? sender.getIcon() : mPeopleHelper.createAvatarSymbol(conversationText, "", layoutColor); } break; } } } if (conversationIcon == null) { conversationIcon = largeIcon; } if (isOneToOne || conversationIcon != null) { return new ConversationHeaderData( conversationText, new OneToOneConversationAvatarData( resolveAvatarImage(conversationIcon))); } final List<List<Notification.MessagingStyle.Message>> groupMessages = new ArrayList<>(); for (int i = 0; i < groups.size(); i++) { final List<Notification.MessagingStyle.Message> groupMessage = new ArrayList<>(); for (int j = 0; j < groups.get(i).size(); j++) { groupMessage.add(groups.get(i).get(j).getMessage()); } groupMessages.add(groupMessage); } final PeopleHelper.NameToPrefixMap nameToPrefixMap = mPeopleHelper.mapUniqueNamesToPrefixWithGroupList(groupMessages); Icon lastIcon = null; Icon secondLastIcon = null; CharSequence lastKey = null; for (int i = groups.size() - 1; i >= 0; i--) { final Notification.MessagingStyle.Message message = groups.get(i).get(0).getMessage(); final Person sender = message.getSenderPerson() != null ? message.getSenderPerson() : user; final CharSequence senderKey = getKey(sender); final boolean notUser = senderKey != userKey; final boolean notIncluded = senderKey != lastKey; if ((notUser && notIncluded) || (i == 0 && lastKey == null)) { if (lastIcon == null) { if (sender.getIcon() != null) { lastIcon = sender.getIcon(); } else { final CharSequence senderName = sender.getName() != null ? sender.getName() : ""; lastIcon = mPeopleHelper.createAvatarSymbol( senderName, nameToPrefixMap.getPrefix(senderName), layoutColor); } lastKey = senderKey; } else { if (sender.getIcon() != null) { secondLastIcon = sender.getIcon(); } else { final CharSequence senderName = sender.getName() != null ? sender.getName() : ""; secondLastIcon = mPeopleHelper.createAvatarSymbol( senderName, nameToPrefixMap.getPrefix(senderName), layoutColor); } break; } } } if (lastIcon == null) { lastIcon = mPeopleHelper.createAvatarSymbol( "", "", layoutColor); } if (secondLastIcon == null) { secondLastIcon = mPeopleHelper.createAvatarSymbol( "", "", layoutColor); } return new ConversationHeaderData( conversationText, new GroupConversationAvatarData(resolveAvatarImage(lastIcon), resolveAvatarImage(secondLastIcon))); } /** * {@link ImageResolver#loadImage(Uri)} accepts Uri to load images. However Conversation Avatars * are received as Icon. So, we can't make use of ImageResolver. */ @Nullable private Drawable resolveAvatarImage(Icon conversationIcon) { try { return LocalImageResolver.resolveImage(conversationIcon, getContext()); } catch (Exception ex) { return null; } } /** * Creates new messages, reusing existing ones if they are available. * Loading
core/java/com/android/internal/widget/MessagingData.java +12 −2 Original line number Diff line number Diff line Loading @@ -16,6 +16,7 @@ package com.android.internal.widget; import android.annotation.Nullable; import android.app.Person; import java.util.List; Loading @@ -32,6 +33,8 @@ final class MessagingData { private final List<Person> mSenders; private final int mUnreadCount; private ConversationHeaderData mConversationHeaderData; MessagingData(Person user, boolean showSpinner, List<MessagingMessage> historicMessagingMessages, List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups, Loading @@ -39,7 +42,7 @@ final class MessagingData { this(user, showSpinner, /* unreadCount= */0, historicMessagingMessages, newMessagingMessages, groups, senders); senders, null); } MessagingData(Person user, boolean showSpinner, Loading @@ -47,7 +50,8 @@ final class MessagingData { List<MessagingMessage> historicMessagingMessages, List<MessagingMessage> newMessagingMessages, List<List<MessagingMessage>> groups, List<Person> senders) { List<Person> senders, @Nullable ConversationHeaderData conversationHeaderData) { mUser = user; mShowSpinner = showSpinner; mUnreadCount = unreadCount; Loading @@ -55,6 +59,7 @@ final class MessagingData { mNewMessagingMessages = newMessagingMessages; mGroups = groups; mSenders = senders; mConversationHeaderData = conversationHeaderData; } public Person getUser() { Loading Loading @@ -84,4 +89,9 @@ final class MessagingData { public List<List<MessagingMessage>> getGroups() { return mGroups; } @Nullable public ConversationHeaderData getConversationHeaderData() { return mConversationHeaderData; } }