diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index d9a215f41b5039f173acf4e5d2a8e5af7820cf0c..8b023b9b46a3bab029dcd80099f6dbdd0a278759 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -438,15 +438,10 @@ public class Contact implements ListItem, Blockable { } public boolean setAvatar(final Avatar avatar) { - return setAvatar(avatar, false); - } - - public boolean setAvatar(final Avatar avatar, final boolean previouslyOmittedPepFetch) { if (this.avatar != null && this.avatar.equals(avatar)) { return false; } - if (!previouslyOmittedPepFetch - && this.avatar != null + if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) { return false; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 6898c7dc9b2ee225ade4b40091532cf3feb235ae..69276d9512e1cf91f2a0ae337e4287f80b423aab 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -57,6 +57,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -142,6 +143,7 @@ import eu.siacs.conversations.xmpp.manager.NickManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.RosterManager; +import eu.siacs.conversations.xmpp.manager.VCardManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.IqErrorException; @@ -4462,23 +4464,18 @@ public class XmppConnectionService extends Service { } } - public void fetchAvatar(Account account, Avatar avatar) { - fetchAvatar(account, avatar, null); - } - - public void fetchAvatar( - Account account, final Avatar avatar, final UiCallback callback) { + 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, callback); + fetchAvatarPep(account, avatar, null); break; case VCARD: this.mInProgressAvatarFetches.add(KEY); - fetchAvatarVcard(account, avatar, callback); + fetchAvatarVcard(account, avatar); break; } } else if (avatar.origin == Avatar.Origin.PEP) { @@ -4560,88 +4557,81 @@ public class XmppConnectionService extends Service { }); } - private void fetchAvatarVcard( - final Account account, final Avatar avatar, final UiCallback callback) { - final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket( - account, - packet, - response -> { - final boolean previouslyOmittedPepFetch; - synchronized (mInProgressAvatarFetches) { - final String KEY = generateFetchKey(account, avatar); - mInProgressAvatarFetches.remove(KEY); - previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); - } - if (response.getType() == Iq.Type.RESULT) { - Element vCard = response.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully fetched vCard avatar for " - + avatar.owner - + " omittedPep=" - + previouslyOmittedPepFetch); - 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 { - final Contact contact = - account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar, previouslyOmittedPepFetch); - 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(); - } - if (user.getRealJid() != null) { - Contact contact = - account.getRoster() - .getContact(user.getRealJid()); - contact.setAvatar(avatar); - account.getXmppConnection() - .getManager(RosterManager.class) - .writeToDatabaseAsync(); - getAvatarService().clear(contact); - updateRosterUi(); - } - } - } - } - } + 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, + "could not retrieve avatar from " + + address + + " (" + + avatar.sha1sum + + ")", + t); + } + }, + MoreExecutors.directExecutor()); + } + + 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 { + final Contact contact = account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar); + 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(); + } + if (user.getRealJid() != null) { + Contact contact = account.getRoster().getContact(user.getRealJid()); + contact.setAvatar(avatar); + account.getXmppConnection() + .getManager(RosterManager.class) + .writeToDatabaseAsync(); + getAvatarService().clear(contact); + updateRosterUi(); + } + } + } + } } public void checkForAvatar(final Account account, final UiCallback callback) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index e4761de5fa73ed3332c5e40542708900a2454228..f3c873a90c130d6d51984956dd8e165f6b308bbe 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -21,6 +21,7 @@ import eu.siacs.conversations.xmpp.manager.PrivateStorageManager; import eu.siacs.conversations.xmpp.manager.PubSubManager; import eu.siacs.conversations.xmpp.manager.RosterManager; import eu.siacs.conversations.xmpp.manager.UnifiedPushManager; +import eu.siacs.conversations.xmpp.manager.VCardManager; public class Managers { @@ -50,6 +51,7 @@ public class Managers { .put(PubSubManager.class, new PubSubManager(context, connection)) .put(RosterManager.class, new RosterManager(context, connection)) .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection)) + .put(VCardManager.class, new VCardManager(context, connection)) .build(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java index 7104ead02a1dcc4105af573f35750abf330590a3..53960dbbd1b7aae1192e6d3eac0a691b70ba8627 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AvatarManager.java @@ -1,6 +1,9 @@ package eu.siacs.conversations.xmpp.manager; import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.io.BaseEncoding; +import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -12,6 +15,7 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.ByteContent; import im.conversations.android.xmpp.model.avatar.Data; import im.conversations.android.xmpp.model.avatar.Info; import im.conversations.android.xmpp.model.avatar.Metadata; @@ -26,11 +30,50 @@ public class AvatarManager extends AbstractManager { this.service = service; } + public ListenableFuture fetch(final Jid address, final String itemId) { + final var future = getManager(PubSubManager.class).fetchItem(address, itemId, Data.class); + return Futures.transform(future, ByteContent::asBytes, MoreExecutors.directExecutor()); + } + + public ListenableFuture fetchAndStore(final Avatar avatar) { + final var future = fetch(avatar.owner, avatar.sha1sum); + return Futures.transform( + future, + data -> { + avatar.image = BaseEncoding.base64().encode(data); + if (service.getFileBackend().save(avatar)) { + setPepAvatar(avatar); + return null; + } else { + throw new IllegalStateException("Could not store avatar"); + } + }, + MoreExecutors.directExecutor()); + } + + private void setPepAvatar(final Avatar avatar) { + final var account = getAccount(); + if (account.getJid().asBareJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + getDatabase().updateAccount(account); + } + this.service.getAvatarService().clear(account); + this.service.updateConversationUi(); + this.service.updateAccountUi(); + } else { + final Contact contact = account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar); + account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync(); + this.service.getAvatarService().clear(contact); + this.service.updateConversationUi(); + this.service.updateRosterUi(); + } + } + public void handleItems(final Jid from, final Items items) { final var account = getAccount(); // TODO support retract final var entry = items.getFirstItemWithId(Metadata.class); - Log.d(Config.LOGTAG, "<-- " + entry + " (" + from + ")"); final var avatar = entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); if (avatar == null) { @@ -57,8 +100,25 @@ public class AvatarManager extends AbstractManager { } } } else if (service.isDataSaverDisabled()) { - // TODO use internal mechanism to fetch PEP avatars - service.fetchAvatar(account, avatar); + final var future = this.fetchAndStore(avatar); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(Void result) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully fetched pep avatar for " + + avatar.owner); + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "could not fetch avatar", t); + } + }, + MoreExecutors.directExecutor()); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java new file mode 100644 index 0000000000000000000000000000000000000000..aa2765deb0d29e462ffa013311c94321b3f0ea44 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/VCardManager.java @@ -0,0 +1,50 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.vcard.VCard; + +public class VCardManager extends AbstractManager { + + public VCardManager(final Context context, final XmppConnection connection) { + super(context, connection); + } + + public ListenableFuture retrieve(final Jid address) { + final var iq = new Iq(Iq.Type.GET, new VCard()); + iq.setTo(address); + return Futures.transform( + this.connection.sendIqPacket(iq), + result -> { + final var vCard = result.getExtension(VCard.class); + if (vCard == null) { + throw new IllegalStateException("Result did not include vCard"); + } + return vCard; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture retrievePhoto(final Jid address) { + final var vCardFuture = retrieve(address); + return Futures.transform( + vCardFuture, + vCard -> { + final var photo = vCard.getPhoto(); + if (photo == null) { + throw new IllegalStateException("No photo in vCard"); + } + final var binaryValue = photo.getBinaryValue(); + if (binaryValue == null) { + throw new IllegalStateException("Photo has no binary value"); + } + return binaryValue.asBytes(); + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java index 92adc6831c5346cc15a1d2a5663341e29497e2c5..3ca485341cea54ba7db5d892e3390a6b5d959ce7 100644 --- a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java @@ -8,4 +8,8 @@ public class Photo extends Extension { public Photo() { super(Photo.class); } + + public BinaryValue getBinaryValue() { + return this.getExtension(BinaryValue.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java index 20a6949775b4348d1c703d3362e570ff98b4f07c..0851966f022674ab20c23ed879bd298339f71dc2 100644 --- a/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java @@ -9,4 +9,8 @@ public class VCard extends Extension { public VCard() { super(VCard.class); } + + public Photo getPhoto() { + return this.getExtension(Photo.class); + } }