Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8cf96833 authored by Ibrahim Yilmaz's avatar Ibrahim Yilmaz Committed by Android (Google) Code Review
Browse files

Merge "[Minimal HUN] Create Facepile for Group Conversation" into main

parents eea2a101 35681209
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -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) {
+263 −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.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);
    }
}
+10 −2
Original line number Diff line number Diff line
@@ -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>
+6 −0
Original line number Diff line number Diff line
@@ -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 -->
+3 −0
Original line number Diff line number Diff line
@@ -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" />