diff options
| -rw-r--r-- | core/java/android/app/Notification.java | 10 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/CompactMessagingLayout.java | 263 | ||||
| -rw-r--r-- | core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml | 12 | ||||
| -rw-r--r-- | core/res/res/values/dimens.xml | 6 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 3 |
5 files changed, 292 insertions, 2 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 7ea309193612..d8f03b165a00 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -9282,9 +9282,19 @@ public class Notification implements Parcelable contentView.setViewVisibility(R.id.header_text_divider, View.GONE); if (conversationIcon != null) { contentView.setViewVisibility(R.id.icon, View.GONE); + contentView.setViewVisibility(R.id.conversation_face_pile, View.GONE); contentView.setViewVisibility(R.id.conversation_icon, View.VISIBLE); contentView.setBoolean(R.id.conversation_icon, "setApplyCircularCrop", true); contentView.setImageViewIcon(R.id.conversation_icon, conversationIcon); + } else if (mIsGroupConversation) { + contentView.setViewVisibility(R.id.icon, View.GONE); + contentView.setViewVisibility(R.id.conversation_icon, View.GONE); + contentView.setInt(R.id.status_bar_latest_event_content, + "setNotificationBackgroundColor", mBuilder.getBackgroundColor(p)); + contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", + mBuilder.getSmallIconColor(p)); + contentView.setBundle(R.id.status_bar_latest_event_content, "setGroupFacePile", + mBuilder.mN.extras); } if (remoteInputAction != null) { diff --git a/core/java/com/android/internal/widget/CompactMessagingLayout.java b/core/java/com/android/internal/widget/CompactMessagingLayout.java new file mode 100644 index 000000000000..1e2c01ae4be9 --- /dev/null +++ b/core/java/com/android/internal/widget/CompactMessagingLayout.java @@ -0,0 +1,263 @@ +/* + * 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.app.Notification; +import android.app.Notification.MessagingStyle; +import android.app.Person; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.RemotableViewMethod; +import android.view.View; +import android.view.ViewStub; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.RemoteViews; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.internal.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * A custom-built layout for the compact Heads Up of Notification.MessagingStyle . + */ +@RemoteViews.RemoteView +public class CompactMessagingLayout extends FrameLayout { + + private final PeopleHelper mPeopleHelper = new PeopleHelper(); + + private ViewStub mConversationFacePileViewStub; + + private int mNotificationBackgroundColor; + private int mFacePileSize; + private int mFacePileAvatarSize; + private int mFacePileProtectionWidth; + private int mLayoutColor; + + public CompactMessagingLayout(@NonNull Context context) { + super(context); + } + + public CompactMessagingLayout(@NonNull Context context, + @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CompactMessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CompactMessagingLayout(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPeopleHelper.init(getContext()); + mConversationFacePileViewStub = requireViewById(R.id.conversation_face_pile); + mFacePileSize = getResources() + .getDimensionPixelSize(R.dimen.conversation_compact_face_pile_size); + mFacePileAvatarSize = getResources() + .getDimensionPixelSize(R.dimen.conversation_compact_face_pile_avatar_size); + mFacePileProtectionWidth = getResources().getDimensionPixelSize( + R.dimen.conversation_compact_face_pile_protection_width); + } + + /** + * Set conversation data + * + * @param extras Bundle contains conversation data + */ + @RemotableViewMethod(asyncImpl = "setGroupFacePileAsync") + public void setGroupFacePile(Bundle extras) { + // NO-OP + } + + /** + * async version of {@link ConversationLayout#setLayoutColor} + */ + @RemotableViewMethod + public Runnable setLayoutColorAsync(int color) { + mLayoutColor = color; + return NotificationRunnables.NOOP; + } + + @RemotableViewMethod(asyncImpl = "setLayoutColorAsync") + public void setLayoutColor(int color) { + mLayoutColor = color; + } + + /** + * @param color the color of the notification background + */ + @RemotableViewMethod + public void setNotificationBackgroundColor(int color) { + mNotificationBackgroundColor = color; + } + + /** + * async version of {@link CompactMessagingLayout#setGroupFacePile} + * setGroupFacePile! + */ + public Runnable setGroupFacePileAsync(Bundle extras) { + final Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); + final List<Notification.MessagingStyle.Message> newMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray(messages); + final Parcelable[] histMessages = + extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES); + final List<Notification.MessagingStyle.Message> newHistoricMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages); + final Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON, Person.class); + + final List<List<MessagingStyle.Message>> groups = groupMessages(newMessages, + newHistoricMessages); + final PeopleHelper.NameToPrefixMap nameToPrefixMap = + mPeopleHelper.mapUniqueNamesToPrefixWithGroupList(groups); + final int layoutColor = mLayoutColor; + // Find last two person's icon to show them in the face pile. + Icon secondLastIcon = null; + Icon lastIcon = null; + CharSequence lastKey = null; + final CharSequence userKey = getPersonKey(user); + for (int i = groups.size() - 1; i >= 0; i--) { + final MessagingStyle.Message message = groups.get(i).get(0); + final Person sender = + message.getSenderPerson() != null ? message.getSenderPerson() : user; + final CharSequence senderKey = getPersonKey(sender); + final boolean notUser = senderKey != userKey; + final boolean notIncluded = senderKey != lastKey; + + if ((notUser && notIncluded) || (i == 0 && lastKey == null)) { + final Icon icon = getSenderIcon(sender, nameToPrefixMap, layoutColor); + if (lastIcon == null) { + lastIcon = icon; + lastKey = senderKey; + } else { + secondLastIcon = icon; + break; + } + } + } + + if (lastIcon == null) { + lastIcon = getSenderIcon(null, null, layoutColor); + } + + if (secondLastIcon == null) { + secondLastIcon = getSenderIcon(null, null, layoutColor); + } + final Drawable secondLastIconDrawable = secondLastIcon.loadDrawable(getContext()); + final Drawable lastIconDrawable = lastIcon.loadDrawable(getContext()); + return () -> { + final View conversationFacePile = mConversationFacePileViewStub.inflate(); + conversationFacePile.setVisibility(VISIBLE); + + final ImageView facePileBottomBg = conversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom_background); + final ImageView facePileTop = conversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_top); + final ImageView facePileBottom = conversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom); + + facePileTop.setImageDrawable(secondLastIconDrawable); + facePileBottom.setImageDrawable(lastIconDrawable); + facePileBottomBg.setImageTintList(ColorStateList.valueOf(mNotificationBackgroundColor)); + setSize(conversationFacePile, mFacePileSize); + setSize(facePileBottom, mFacePileAvatarSize); + setSize(facePileTop, mFacePileAvatarSize); + setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); + }; + } + + @NonNull + private Icon getSenderIcon(@Nullable Person sender, + @Nullable PeopleHelper.NameToPrefixMap uniqueNames, + int layoutColor) { + if (sender == null) { + return mPeopleHelper.createAvatarSymbol(/* name = */ "", /* symbol = */ "", + layoutColor); + } + + if (sender.getIcon() != null) { + return sender.getIcon(); + } + + final CharSequence senderName = sender.getName(); + if (!TextUtils.isEmpty(senderName)) { + final String symbol = uniqueNames != null ? uniqueNames.getPrefix(senderName) : ""; + return mPeopleHelper.createAvatarSymbol(senderName, symbol, layoutColor); + } + + return mPeopleHelper.createAvatarSymbol(/* name = */ "", /* symbol = */ "", layoutColor); + } + + + /** + * Groups the given messages by their sender. + */ + private static List<List<MessagingStyle.Message>> groupMessages( + List<MessagingStyle.Message> messages, + List<MessagingStyle.Message> historicMessages + ) { + if (messages.isEmpty() && historicMessages.isEmpty()) return List.of(); + + ArrayList<MessagingStyle.Message> currentGroup = null; + CharSequence currentSenderKey = null; + final ArrayList<List<MessagingStyle.Message>> groups = new ArrayList<>(); + final int histSize = historicMessages.size(); + + for (int i = 0; i < histSize + messages.size(); i++) { + final MessagingStyle.Message message = i < histSize ? historicMessages.get(i) + : messages.get(i - histSize); + if (message == null) continue; + + final CharSequence senderKey = getPersonKey(message.getSenderPerson()); + final boolean isNewGroup = currentGroup == null || senderKey != currentSenderKey; + if (isNewGroup) { + currentGroup = new ArrayList<>(); + groups.add(currentGroup); + currentSenderKey = senderKey; + } + currentGroup.add(message); + } + return groups; + } + + private static CharSequence getPersonKey(@Nullable Person person) { + return person == null ? null : person.getKey() == null ? person.getName() : person.getKey(); + } + + private static void setSize(View view, int size) { + final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams(); + lp.width = size; + lp.height = size; + view.setLayoutParams(lp); + } +} diff --git a/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml b/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml index 3b288d7038e1..82920bad95cd 100644 --- a/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml +++ b/core/res/res/layout/notification_template_material_messaging_compact_heads_up.xml @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<FrameLayout +<com.android.internal.widget.CompactMessagingLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/status_bar_latest_event_content" android:layout_width="match_parent" @@ -45,6 +45,14 @@ android:scaleType="centerCrop" android:importantForAccessibility="no" /> + <ViewStub + android:layout="@layout/conversation_face_pile_layout" + android:layout_gravity="center_vertical|start" + android:layout_width="@dimen/conversation_compact_face_pile_size" + android:layout_height="@dimen/conversation_compact_face_pile_size" + android:layout_marginStart="@dimen/notification_icon_circle_start" + android:id="@+id/conversation_face_pile" + /> <FrameLayout android:id="@+id/alternate_expand_target" android:layout_width="@dimen/notification_content_margin_start" @@ -101,4 +109,4 @@ /> </FrameLayout> </LinearLayout> -</FrameLayout> +</com.android.internal.widget.CompactMessagingLayout> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 52ce9936787f..b885e03bc098 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -846,6 +846,12 @@ <dimen name="conversation_face_pile_protection_width">2dp</dimen> <!-- The width of the protection of the face pile layout when expanded--> <dimen name="conversation_face_pile_protection_width_expanded">@dimen/conversation_face_pile_protection_width</dimen> + <!-- size of the compact face pile --> + <dimen name="conversation_compact_face_pile_size">24dp</dimen> + <!-- size of the face pile avatar --> + <dimen name="conversation_compact_face_pile_avatar_size">17dp</dimen> + <!-- size of the face pile protection --> + <dimen name="conversation_compact_face_pile_protection_width">1dp</dimen> <!-- The padding of the expanded message container--> <dimen name="expanded_group_conversation_message_padding">32dp</dimen> <!-- The stroke width of the ring used to visually mark a conversation as important --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 6c6764a3175c..5a3eaeb8327b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4543,6 +4543,9 @@ <java-symbol type="dimen" name="conversation_avatar_size_group_expanded" /> <java-symbol type="dimen" name="conversation_face_pile_avatar_size" /> <java-symbol type="dimen" name="conversation_face_pile_avatar_size_group_expanded" /> + <java-symbol type="dimen" name="conversation_compact_face_pile_size" /> + <java-symbol type="dimen" name="conversation_compact_face_pile_avatar_size" /> + <java-symbol type="dimen" name="conversation_compact_face_pile_protection_width" /> <java-symbol type="dimen" name="conversation_face_pile_protection_width" /> <java-symbol type="dimen" name="conversation_face_pile_protection_width_expanded" /> <java-symbol type="dimen" name="conversation_badge_protrusion_group_expanded" /> |