Detailed changes
@@ -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;
@@ -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) {
@@ -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;
}
@@ -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
@@ -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";
@@ -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,
@@ -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}");
- }
}
@@ -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}");
}
}