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

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

Merge "2/n ConversationLayout: offload conversation icon loading to background thread" into main

parents ef8c8026 bc6ede4c
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -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
+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;
        }
    }
}
+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;
    }
}
+293 −15
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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);
@@ -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);
    }

    /**
@@ -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());
@@ -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) {
@@ -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;
@@ -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() {
@@ -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(
@@ -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;
@@ -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);
    }
@@ -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;
@@ -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;
    }
@@ -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.
     *
+12 −2
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.internal.widget;

import android.annotation.Nullable;
import android.app.Person;

import java.util.List;
@@ -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,
@@ -39,7 +42,7 @@ final class MessagingData {
        this(user, showSpinner, /* unreadCount= */0,
                historicMessagingMessages, newMessagingMessages,
                groups,
                senders);
                senders, null);
    }

    MessagingData(Person user, boolean showSpinner,
@@ -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;
@@ -55,6 +59,7 @@ final class MessagingData {
        mNewMessagingMessages = newMessagingMessages;
        mGroups = groups;
        mSenders = senders;
        mConversationHeaderData = conversationHeaderData;
    }

    public Person getUser() {
@@ -84,4 +89,9 @@ final class MessagingData {
    public List<List<MessagingMessage>> getGroups() {
        return mGroups;
    }

    @Nullable
    public ConversationHeaderData getConversationHeaderData() {
        return mConversationHeaderData;
    }
}
Loading