fetch vCard avatars for MUCs and MUC users only

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/MucOptions.java                   |  10 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java                 |  16 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java                 |  60 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java        | 185 
src/main/java/eu/siacs/conversations/xml/Namespace.java                         |   1 
src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java            | 108 
src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java                       |  56 
src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java |   7 
8 files changed, 131 insertions(+), 312 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/MucOptions.java πŸ”—

@@ -14,7 +14,6 @@ import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.chatstate.ChatState;
-import eu.siacs.conversations.xmpp.pep.Avatar;
 import im.conversations.android.xmpp.model.data.Data;
 import im.conversations.android.xmpp.model.data.Field;
 import im.conversations.android.xmpp.model.disco.info.InfoQuery;
@@ -158,8 +157,7 @@ public class MucOptions {
         final var serviceDiscoveryResult = getServiceDiscoveryResult();
         return serviceDiscoveryResult == null
                 ? null
-                : serviceDiscoveryResult.getServiceDiscoveryExtension(
-                        "http://jabber.org/protocol/muc#roominfo");
+                : serviceDiscoveryResult.getServiceDiscoveryExtension(Namespace.MUC_ROOM_INFO);
     }
 
     public String getAvatar() {
@@ -857,7 +855,7 @@ public class MucOptions {
         private Jid realJid;
         private Jid fullJid;
         private long pgpKeyId = 0;
-        private Avatar avatar;
+        private String avatar;
         private final MucOptions options;
         private ChatState chatState = Config.DEFAULT_CHAT_STATE;
         private String occupantId;
@@ -913,7 +911,7 @@ public class MucOptions {
             }
         }
 
-        public boolean setAvatar(final Avatar avatar) {
+        public boolean setAvatar(final String avatar) {
             if (this.avatar != null && this.avatar.equals(avatar)) {
                 return false;
             } else {
@@ -930,7 +928,7 @@ public class MucOptions {
             getContact();
 
             if (avatar != null) {
-                return avatar.getFilename();
+                return avatar;
             }
             if (realJid == null) {
                 return null;

src/main/java/eu/siacs/conversations/generator/IqGenerator.java πŸ”—

@@ -13,7 +13,6 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.pep.Avatar;
 import im.conversations.android.xmpp.model.stanza.Iq;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
@@ -72,21 +71,6 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public Iq retrievePepAvatar(final Avatar avatar) {
-        final Element item = new Element("item");
-        item.setAttribute("id", avatar.sha1sum);
-        final var packet = retrieve(Namespace.AVATAR_DATA, item);
-        packet.setTo(avatar.owner);
-        return packet;
-    }
-
-    public Iq retrieveVcardAvatar(final Avatar avatar) {
-        final Iq packet = new Iq(Iq.Type.GET);
-        packet.setTo(avatar.owner);
-        packet.addChild("vCard", "vcard-temp");
-        return packet;
-    }
-
     public Iq retrieveDeviceIds(final Jid to) {
         final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
         if (to != null) {

src/main/java/eu/siacs/conversations/parser/PresenceParser.java πŸ”—

@@ -23,12 +23,13 @@ import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.AvatarManager;
 import eu.siacs.conversations.xmpp.manager.DiscoManager;
 import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import eu.siacs.conversations.xmpp.manager.RosterManager;
-import eu.siacs.conversations.xmpp.pep.Avatar;
 import im.conversations.android.xmpp.Entity;
 import im.conversations.android.xmpp.model.occupant.OccupantId;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.TimeoutException;
@@ -81,7 +82,7 @@ public class PresenceParser extends AbstractParser
         if (!from.isBareJid()) {
             final String type = packet.getAttribute("type");
             final Element x = packet.findChild("x", Namespace.MUC_USER);
-            Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
+            final var vCardUpdate = packet.getExtension(VCardUpdate.class);
             final List<String> codes = getStatusCodes(x);
             if (type == null) {
                 if (x != null) {
@@ -162,32 +163,8 @@ public class PresenceParser extends AbstractParser
                                 }
                             }
                         }
-                        if (avatar != null) {
-                            avatar.owner = from;
-                            if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
-                                if (user.setAvatar(avatar)) {
-                                    mXmppConnectionService.getAvatarService().clear(user);
-                                }
-
-                                // TODO don’t do that. This will just overwrite (better) PEP avatars
-
-                                if (user.getRealJid() != null) {
-                                    final Contact c =
-                                            conversation
-                                                    .getAccount()
-                                                    .getRoster()
-                                                    .getContact(user.getRealJid());
-                                    if (c.setAvatar(avatar.sha1sum)) {
-                                        connection
-                                                .getManager(RosterManager.class)
-                                                .writeToDatabaseAsync();
-                                        mXmppConnectionService.getAvatarService().clear(c);
-                                    }
-                                    mXmppConnectionService.updateRosterUi();
-                                }
-                            } else if (mXmppConnectionService.isDataSaverDisabled()) {
-                                mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
-                            }
+                        if (vCardUpdate != null) {
+                            getManager(AvatarManager.class).handleVCardUpdate(from, vCardUpdate);
                         }
                     }
                 }
@@ -346,33 +323,6 @@ public class PresenceParser extends AbstractParser
         if (type == null) {
             final String resource = from.isBareJid() ? "" : from.getResource();
 
-            // TODO simply don’t parse avatars for contacts at all. Only if presence is bare and a
-            // MUC
-
-            final Avatar avatar =
-                    Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
-            if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
-                avatar.owner = from.asBareJid();
-                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
-                    if (avatar.owner.equals(account.getJid().asBareJid())) {
-                        account.setAvatar(avatar.getFilename());
-                        mXmppConnectionService.databaseBackend.updateAccount(account);
-                        mXmppConnectionService.getAvatarService().clear(account);
-                        mXmppConnectionService.updateConversationUi();
-                        mXmppConnectionService.updateAccountUi();
-                    } else {
-                        if (contact.setAvatar(avatar.sha1sum)) {
-                            connection.getManager(RosterManager.class).writeToDatabaseAsync();
-                            mXmppConnectionService.getAvatarService().clear(contact);
-                            mXmppConnectionService.updateConversationUi();
-                            mXmppConnectionService.updateRosterUi();
-                        }
-                    }
-                } else if (mXmppConnectionService.isDataSaverDisabled()) {
-                    mXmppConnectionService.fetchAvatar(account, avatar);
-                }
-            }
-
             if (mXmppConnectionService.isMuc(account, from)) {
                 return;
             }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -150,6 +150,7 @@ import im.conversations.android.xmpp.IqErrorException;
 import im.conversations.android.xmpp.model.disco.info.InfoQuery;
 import im.conversations.android.xmpp.model.stanza.Iq;
 import im.conversations.android.xmpp.model.up.Push;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
 import java.io.File;
 import java.security.Security;
 import java.security.cert.CertificateException;
@@ -3958,18 +3959,25 @@ public class XmppConnectionService extends Service {
             final Conversation conversation, final OnConferenceConfigurationFetched callback) {
         final var account = conversation.getAccount();
         final var connection = account.getXmppConnection();
+        final var address = conversation.getJid().asBareJid();
         if (connection == null) {
             return;
         }
         final var future =
-                connection
-                        .getManager(DiscoManager.class)
-                        .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
+                connection.getManager(DiscoManager.class).info(Entity.discoItem(address), null);
         Futures.addCallback(
                 future,
                 new FutureCallback<>() {
                     @Override
                     public void onSuccess(InfoQuery result) {
+                        final var avatarHash =
+                                result.getServiceDiscoveryExtension(
+                                        Namespace.MUC_ROOM_INFO, "muc#roominfo_avatarhash");
+                        if (VCardUpdate.isValidSHA1(avatarHash)) {
+                            connection
+                                    .getManager(AvatarManager.class)
+                                    .handleVCardUpdate(address, avatarHash);
+                        }
                         final MucOptions mucOptions = conversation.getMucOptions();
                         final Bookmark bookmark = conversation.getBookmark();
                         final boolean sameBefore =
@@ -4403,177 +4411,6 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void fetchAvatar(Account account, final Avatar avatar) {
-        final String KEY = generateFetchKey(account, avatar);
-        synchronized (this.mInProgressAvatarFetches) {
-            if (mInProgressAvatarFetches.add(KEY)) {
-                switch (avatar.origin) {
-                    case PEP:
-                        this.mInProgressAvatarFetches.add(KEY);
-                        fetchAvatarPep(account, avatar, null);
-                        break;
-                    case VCARD:
-                        this.mInProgressAvatarFetches.add(KEY);
-                        fetchAvatarVcard(account, avatar);
-                        break;
-                }
-            } else if (avatar.origin == Avatar.Origin.PEP) {
-                mOmittedPepAvatarFetches.add(KEY);
-            } else {
-                Log.d(
-                        Config.LOGTAG,
-                        account.getJid().asBareJid()
-                                + ": already fetching "
-                                + avatar.origin
-                                + " avatar for "
-                                + avatar.owner);
-            }
-        }
-    }
-
-    private void fetchAvatarPep(
-            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
-        final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
-        sendIqPacket(
-                account,
-                packet,
-                (result) -> {
-                    synchronized (mInProgressAvatarFetches) {
-                        mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
-                    }
-                    final String ERROR =
-                            account.getJid().asBareJid()
-                                    + ": fetching avatar for "
-                                    + avatar.owner
-                                    + " failed ";
-                    if (result.getType() == Iq.Type.RESULT) {
-                        avatar.image = IqParser.avatarData(result);
-                        if (avatar.image != null) {
-                            if (getFileBackend().save(avatar)) {
-                                if (account.getJid().asBareJid().equals(avatar.owner)) {
-                                    if (account.setAvatar(avatar.getFilename())) {
-                                        databaseBackend.updateAccount(account);
-                                    }
-                                    getAvatarService().clear(account);
-                                    updateConversationUi();
-                                    updateAccountUi();
-                                } else {
-                                    final Contact contact =
-                                            account.getRoster().getContact(avatar.owner);
-                                    contact.setAvatar(avatar.sha1sum);
-                                    account.getXmppConnection()
-                                            .getManager(RosterManager.class)
-                                            .writeToDatabaseAsync();
-                                    getAvatarService().clear(contact);
-                                    updateConversationUi();
-                                    updateRosterUi();
-                                }
-                                if (callback != null) {
-                                    callback.success(avatar);
-                                }
-                                Log.d(
-                                        Config.LOGTAG,
-                                        account.getJid().asBareJid()
-                                                + ": successfully fetched pep avatar for "
-                                                + avatar.owner);
-                                return;
-                            }
-                        } else {
-
-                            Log.d(Config.LOGTAG, ERROR + "(parsing error)");
-                        }
-                    } else {
-                        Element error = result.findChild("error");
-                        if (error == null) {
-                            Log.d(Config.LOGTAG, ERROR + "(server error)");
-                        } else {
-                            Log.d(Config.LOGTAG, ERROR + error);
-                        }
-                    }
-                    if (callback != null) {
-                        callback.error(0, null);
-                    }
-                });
-    }
-
-    private void fetchAvatarVcard(final Account account, final Avatar avatar) {
-        final var address = avatar.owner;
-        final var connection = account.getXmppConnection();
-        final var future = connection.getManager(VCardManager.class).retrievePhoto(address);
-        Futures.addCallback(
-                future,
-                new FutureCallback<>() {
-                    @Override
-                    public void onSuccess(byte[] result) {
-                        avatar.image = BaseEncoding.base64().encode(result);
-                        if (fileBackend.save(avatar)) {
-                            setVCardAvatar(account, avatar);
-                        }
-                    }
-
-                    @Override
-                    public void onFailure(@NonNull Throwable t) {
-                        Log.d(
-                                Config.LOGTAG,
-                                account.getJid().asBareJid()
-                                        + ": could not retrieve vCard avatar of "
-                                        + avatar.owner);
-                    }
-                },
-                MoreExecutors.directExecutor());
-    }
-
-    // TODO move this into VCard manager
-    private void setVCardAvatar(final Account account, final Avatar avatar) {
-        Log.d(
-                Config.LOGTAG,
-                account.getJid().asBareJid()
-                        + ": successfully fetched vCard avatar for "
-                        + avatar.owner);
-        if (avatar.owner.isBareJid()) {
-            if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) {
-                Log.d(
-                        Config.LOGTAG,
-                        account.getJid().asBareJid() + ": had no avatar. replacing with vcard");
-                account.setAvatar(avatar.getFilename());
-                databaseBackend.updateAccount(account);
-                getAvatarService().clear(account);
-                updateAccountUi();
-            } else {
-                // TODO if this is a MUC clear MucOptions too
-                // TODO do the same clearing for when setting a cached version
-                final Contact contact = account.getRoster().getContact(avatar.owner);
-                contact.setAvatar(avatar.sha1sum);
-                account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
-                getAvatarService().clear(contact);
-                updateRosterUi();
-            }
-            updateConversationUi();
-        } else {
-            Conversation conversation = find(account, avatar.owner.asBareJid());
-            if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
-                MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
-                if (user != null) {
-                    if (user.setAvatar(avatar)) {
-                        getAvatarService().clear(user);
-                        updateConversationUi();
-                        updateMucRosterUi();
-                    }
-                    // TODO don’t do that. this will put lower quality vCard avatars into contacts
-                    if (user.getRealJid() != null) {
-                        Contact contact = account.getRoster().getContact(user.getRealJid());
-                        contact.setAvatar(avatar.sha1sum);
-                        account.getXmppConnection()
-                                .getManager(RosterManager.class)
-                                .writeToDatabaseAsync();
-                        getAvatarService().clear(contact);
-                        updateRosterUi();
-                    }
-                }
-            }
-        }
-    }
-
     public ListenableFuture<Void> checkForAvatar(final Account account) {
         final var connection = account.getXmppConnection();
         return connection

src/main/java/eu/siacs/conversations/xml/Namespace.java πŸ”—

@@ -47,6 +47,7 @@ public final class Namespace {
     public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
     public static final String PUBSUB_EVENT = PUBSUB + "#event";
     public static final String MUC = "http://jabber.org/protocol/muc";
+    public static final String MUC_ROOM_INFO = MUC + "#roominfo";
     public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options";
     public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max";
     public static final String PUBSUB_ERROR = PUBSUB + "#errors";

src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java πŸ”—

@@ -24,6 +24,8 @@ import com.google.common.util.concurrent.SettableFuture;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.Compatibility;
@@ -38,6 +40,7 @@ import im.conversations.android.xmpp.model.avatar.Info;
 import im.conversations.android.xmpp.model.avatar.Metadata;
 import im.conversations.android.xmpp.model.pubsub.Items;
 import im.conversations.android.xmpp.model.upload.purpose.Profile;
+import im.conversations.android.xmpp.model.vcard.update.VCardUpdate;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -231,11 +234,23 @@ public class AvatarManager extends AbstractManager {
         throw new IOException("Could not move avatar to avatar location");
     }
 
-    private void setAvatar(final Jid from, final Info info) {
-        Log.d(Config.LOGTAG, "setting avatar for " + from + " to " + info.getId());
+    private void setAvatar(final Jid address, final Info info) {
+        setAvatar(address, info.getId());
+    }
+
+    private void setAvatar(final Jid from, final String id) {
+        Log.d(Config.LOGTAG, "setting avatar for " + from + " to " + id);
+        if (from.isBareJid()) {
+            setAvatarContact(from, id);
+        } else {
+            setAvatarMucUser(from, id);
+        }
+    }
+
+    private void setAvatarContact(final Jid from, final String id) {
         final var account = getAccount();
         if (account.getJid().asBareJid().equals(from)) {
-            if (account.setAvatar(info.getId())) {
+            if (account.setAvatar(id)) {
                 getDatabase().updateAccount(account);
                 service.notifyAccountAvatarHasChanged(account);
             }
@@ -244,15 +259,38 @@ public class AvatarManager extends AbstractManager {
             service.updateAccountUi();
         } else {
             final Contact contact = account.getRoster().getContact(from);
-            if (contact.setAvatar(info.getId())) {
+            if (contact.setAvatar(id)) {
                 connection.getManager(RosterManager.class).writeToDatabaseAsync();
                 service.getAvatarService().clear(contact);
+
+                final var conversation = service.find(account, from);
+                if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
+                    service.getAvatarService().clear(conversation.getMucOptions());
+                }
+
                 service.updateConversationUi();
                 service.updateRosterUi();
             }
         }
     }
 
+    private void setAvatarMucUser(final Jid from, final String id) {
+        final var account = getAccount();
+        final Conversation conversation = service.find(account, from.asBareJid());
+        if (conversation == null || conversation.getMode() != Conversation.MODE_MULTI) {
+            return;
+        }
+        final var user = conversation.getMucOptions().findUserByFullJid(from);
+        if (user == null) {
+            return;
+        }
+        if (user.setAvatar(id)) {
+            service.getAvatarService().clear(user);
+            service.updateConversationUi();
+            service.updateMucRosterUi();
+        }
+    }
+
     public void handleItems(final Jid from, final Items items) {
         final var account = getAccount();
         // TODO support retract
@@ -296,6 +334,38 @@ public class AvatarManager extends AbstractManager {
         }
     }
 
+    public void handleVCardUpdate(final Jid address, final VCardUpdate vCardUpdate) {
+        final var hash = vCardUpdate.getHash();
+        if (hash == null) {
+            return;
+        }
+        handleVCardUpdate(address, hash);
+    }
+
+    public void handleVCardUpdate(final Jid address, final String hash) {
+        Preconditions.checkArgument(VCardUpdate.isValidSHA1(hash));
+        final var avatarFile = FileBackend.getAvatarFile(context, hash);
+        if (avatarFile.exists()) {
+            setAvatar(address, hash);
+        } else if (service.isDataSaverDisabled()) {
+            final var future = this.fetchAndStoreVCard(address, hash);
+            Futures.addCallback(
+                    future,
+                    new FutureCallback<Void>() {
+                        @Override
+                        public void onSuccess(Void result) {
+                            Log.d(Config.LOGTAG, "successfully fetch vCard avatar for " + address);
+                        }
+
+                        @Override
+                        public void onFailure(@NonNull Throwable t) {
+                            Log.d(Config.LOGTAG, "could not fetch avatar for " + address, t);
+                        }
+                    },
+                    MoreExecutors.directExecutor());
+        }
+    }
+
     private PreferredFallback getPreferredFallback(final Map.Entry<String, Metadata> entry) {
         final var mainItemId = entry.getKey();
         final var infos = entry.getValue().getInfos();
@@ -617,6 +687,36 @@ public class AvatarManager extends AbstractManager {
         }
     }
 
+    public ListenableFuture<Void> fetchAndStoreVCard(final Jid address, final String expectedHash) {
+        final var future = connection.getManager(VCardManager.class).retrievePhoto(address);
+        return Futures.transformAsync(
+                future,
+                photo -> {
+                    final var actualHash = Hashing.sha1().hashBytes(photo).toString();
+                    if (!actualHash.equals(expectedHash)) {
+                        return Futures.immediateFailedFuture(
+                                new IllegalStateException(
+                                        String.format(
+                                                "Hash in vCard update for %s did not match",
+                                                address)));
+                    }
+                    final var avatarFile = FileBackend.getAvatarFile(context, actualHash);
+                    if (avatarFile.exists()) {
+                        setAvatar(address, actualHash);
+                        return Futures.immediateVoidFuture();
+                    }
+                    final var writeFuture = write(avatarFile, photo);
+                    return Futures.transform(
+                            writeFuture,
+                            v -> {
+                                setAvatar(address, actualHash);
+                                return null;
+                            },
+                            MoreExecutors.directExecutor());
+                },
+                AVATAR_COMPRESSION_EXECUTOR);
+    }
+
     public enum ImageFormat {
         PNG,
         JPEG,

src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java πŸ”—

@@ -3,9 +3,7 @@ package eu.siacs.conversations.xmpp.pep;
 import android.util.Base64;
 import androidx.annotation.NonNull;
 import com.google.common.base.MoreObjects;
-import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.Jid;
-import im.conversations.android.xmpp.model.avatar.Metadata;
 import okhttp3.HttpUrl;
 
 public class Avatar {
@@ -49,42 +47,6 @@ public class Avatar {
         return sha1sum;
     }
 
-    public static Avatar parseMetadata(final String primaryId, final Metadata metadata) {
-        if (primaryId == null || metadata == null) {
-            return null;
-        }
-        for (Element child : metadata.getChildren()) {
-            if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) {
-                Avatar avatar = new Avatar();
-                String height = child.getAttribute("height");
-                String width = child.getAttribute("width");
-                String size = child.getAttribute("bytes");
-                try {
-                    if (height != null) {
-                        avatar.height = Integer.parseInt(height);
-                    }
-                    if (width != null) {
-                        avatar.width = Integer.parseInt(width);
-                    }
-                    if (size != null) {
-                        avatar.size = Long.parseLong(size);
-                    }
-                } catch (NumberFormatException e) {
-                    return null;
-                }
-                avatar.type = child.getAttribute("type");
-                String hash = child.getAttribute("id");
-                if (!isValidSHA1(hash)) {
-                    return null;
-                }
-                avatar.sha1sum = hash;
-                avatar.origin = Origin.PEP;
-                return avatar;
-            }
-        }
-        return null;
-    }
-
     @Override
     public boolean equals(Object object) {
         if (object != null && object instanceof Avatar other) {
@@ -93,22 +55,4 @@ public class Avatar {
             return false;
         }
     }
-
-    public static Avatar parsePresence(Element x) {
-        String hash = x == null ? null : x.findChildContent("photo");
-        if (hash == null) {
-            return null;
-        }
-        if (!isValidSHA1(hash)) {
-            return null;
-        }
-        Avatar avatar = new Avatar();
-        avatar.sha1sum = hash;
-        avatar.origin = Origin.VCARD;
-        return avatar;
-    }
-
-    private static boolean isValidSHA1(String s) {
-        return s != null && s.matches("[a-fA-F0-9]{40}");
-    }
 }

src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java πŸ”—

@@ -16,6 +16,11 @@ public class VCardUpdate extends Extension {
 
     public String getHash() {
         final var photo = getPhoto();
-        return photo == null ? null : photo.getContent();
+        final var hash = photo == null ? null : photo.getContent();
+        return isValidSHA1(hash) ? hash : null;
+    }
+
+    public static boolean isValidSHA1(final String s) {
+        return s != null && s.matches("[a-fA-F0-9]{40}");
     }
 }