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

Commit bc6ede4c authored by Ibrahim Yilmaz's avatar Ibrahim Yilmaz
Browse files

2/n ConversationLayout: offload conversation icon loading to background thread

This CL offloads Conversation Avatar drawable loading to the background thread. Conversation header data extraction logic including drawable loading is extracted to loadConversationHeaderData method.
It is used in setDataAsync when `conversation_style_set_avatar_async` is enabled.

Bug: 305540309
Test: presubmit
Flag: ACONFIG android.widget.flags.conversation_style_set_avatar_async DEVELOPMENT
Change-Id: Ib13c76d500073adcb0fb0cb183507bec4a1e8e06
parent 0ec7e254
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