From 9819ef7d0520fcd95d7b8f71cbf1d1a5d7654f34 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 7 Mar 2023 20:05:20 +0100 Subject: [PATCH] fetch vcard avatars --- .../android/ui/AvatarFetcher.java | 13 ++++- .../ui/adapter/ChatOverviewAdapter.java | 1 + .../ui/graphics/drawable/AvatarDrawable.java | 1 + .../android/xmpp/manager/AvatarManager.java | 57 ++++++++++++++----- .../android/xmpp/model/vcard/BinaryValue.java | 13 +++++ .../android/xmpp/model/vcard/Photo.java | 11 ++++ .../android/xmpp/model/vcard/VCard.java | 2 +- app/src/main/res/layout/item_chatoverview.xml | 1 + 8 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java diff --git a/app/src/main/java/im/conversations/android/ui/AvatarFetcher.java b/app/src/main/java/im/conversations/android/ui/AvatarFetcher.java index 79e62dd2d..54d7e69d3 100644 --- a/app/src/main/java/im/conversations/android/ui/AvatarFetcher.java +++ b/app/src/main/java/im/conversations/android/ui/AvatarFetcher.java @@ -16,6 +16,7 @@ import im.conversations.android.ui.graphics.drawable.AvatarDrawable; import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.manager.AvatarManager; import java.lang.ref.WeakReference; +import java.util.concurrent.CancellationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,6 +67,10 @@ public class AvatarFetcher { @Override public void onFailure(@NonNull final Throwable throwable) { + if (throwable instanceof CancellationException) { + return; + } + // TODO on IqTimeout remove tag? final var imageView = imageViewWeakReference.get(); if (imageView == null) { LOGGER.info("ImageView reference was gone after avatar fetch failed"); @@ -79,7 +84,13 @@ public class AvatarFetcher { public static void fetchInto(final ImageView imageView, final AvatarWithAccount avatar) { final var tag = imageView.getTag(); if (tag instanceof AvatarFetcher avatarFetcher) { - if (avatar.equals(avatarFetcher.avatar)) { + if (avatar.equals(avatarFetcher.avatar) && !avatarFetcher.future.isCancelled()) { + if (avatarFetcher.future.isDone()) { + Futures.addCallback( + avatarFetcher.future, + new Callback(imageView, avatar.addressWithName), + ContextCompat.getMainExecutor(imageView.getContext())); + } return; } avatarFetcher.future.cancel(true); diff --git a/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java b/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java index 537654a6e..115460fdd 100644 --- a/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java +++ b/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewAdapter.java @@ -43,6 +43,7 @@ public class ChatOverviewAdapter chatOverviewItem == null ? null : chatOverviewItem.getAddressWithName(); final var avatar = chatOverviewItem == null ? null : chatOverviewItem.getAvatar(); if (avatar != null) { + holder.binding.avatar.setVisibility(View.VISIBLE); AvatarFetcher.fetchInto(holder.binding.avatar, avatar); } else if (addressWithName != null) { holder.binding.avatar.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/im/conversations/android/ui/graphics/drawable/AvatarDrawable.java b/app/src/main/java/im/conversations/android/ui/graphics/drawable/AvatarDrawable.java index cf645cf3c..b51d8b32b 100644 --- a/app/src/main/java/im/conversations/android/ui/graphics/drawable/AvatarDrawable.java +++ b/app/src/main/java/im/conversations/android/ui/graphics/drawable/AvatarDrawable.java @@ -86,6 +86,7 @@ public class AvatarDrawable extends ColorDrawable { canvas.getClipBounds(r); final int cHeight = r.height(); final int cWidth = r.width(); + // TODO if we ever want to do rounded corners we can use drawRoundRect() canvas.drawCircle(midX, midY, radius, paint); if (letter == null) { return; diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java index 9178e2c03..7913a33d7 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/AvatarManager.java @@ -17,6 +17,10 @@ import im.conversations.android.xmpp.model.avatar.Data; 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.stanza.Iq; +import im.conversations.android.xmpp.model.vcard.BinaryValue; +import im.conversations.android.xmpp.model.vcard.Photo; +import im.conversations.android.xmpp.model.vcard.VCard; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -70,7 +74,8 @@ public class AvatarManager extends AbstractManager { } } - public ListenableFuture getAvatar(final Jid address, final String id) { + public ListenableFuture getAvatar( + final Jid address, final AvatarType type, final String id) { final var fetch = new Fetch(address, id); final SettableFuture future; synchronized (avatarFetches) { @@ -81,7 +86,7 @@ public class AvatarManager extends AbstractManager { future = SettableFuture.create(); avatarFetches.put(fetch, future); } - future.setFuture(getCachedOrFetch(address, id)); + future.setFuture(getCachedOrFetch(address, type, id)); future.addListener( () -> { synchronized (this.avatarFetches) { @@ -94,15 +99,8 @@ public class AvatarManager extends AbstractManager { public ListenableFuture getAvatarBitmap( final Jid address, final AvatarType type, final String id) { - final ListenableFuture avatar; - if (type == AvatarType.PEP) { - avatar = getAvatar(address, id); - } else { - return Futures.immediateFailedFuture( - new Exception(String.format("Can not load type %s avatar", type))); - } return Futures.transform( - avatar, + getAvatar(address, type, id), bytes -> { return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); }, @@ -120,26 +118,55 @@ public class AvatarManager extends AbstractManager { } } - private ListenableFuture getCachedOrFetch(final Jid address, final String id) { + private ListenableFuture getCachedOrFetch( + final Jid address, final AvatarType type, final String id) { final var cachedFuture = Futures.submit(() -> getCachedAvatar(address, id), FILE_IO_EXECUTOR); return Futures.catchingAsync( cachedFuture, Exception.class, - exception -> fetchAndCacheAvatar(address, id), + exception -> fetchAndCacheAvatar(address, type, id), MoreExecutors.directExecutor()); } - private ListenableFuture fetchAvatar(final Jid address, final String id) { + private ListenableFuture fetchPepAvatar(final Jid address, final String id) { return Futures.transform( getManager(PubSubManager.class).fetchItem(address, id, Data.class), Data::asBytes, MoreExecutors.directExecutor()); } - private ListenableFuture fetchAndCacheAvatar(final Jid address, final String id) { + private ListenableFuture fetchVcardAvatar(final Jid address, final String id) { + final var iq = new Iq(Iq.Type.GET); + iq.setTo(address); + iq.addExtension(new VCard()); + final var iqFuture = connection.sendIqPacket(iq); return Futures.transform( - fetchAvatar(address, id), + iqFuture, + result -> { + final var vcard = result.getExtension(VCard.class); + if (vcard == null) { + throw new IllegalStateException("No vCard in response"); + } + final var photo = vcard.getExtension(Photo.class); + final var binary = photo == null ? null : photo.getExtension(BinaryValue.class); + if (binary == null) { + throw new IllegalStateException("vCard did not have embedded photo"); + } + return binary.asBytes(); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture fetchAndCacheAvatar( + final Jid address, final AvatarType avatarType, final String id) { + final ListenableFuture avatarFetchFuture = + switch (avatarType) { + case PEP -> fetchPepAvatar(address, id); + case VCARD -> fetchVcardAvatar(address, id); + }; + return Futures.transform( + avatarFetchFuture, avatar -> { final var sha1Hash = Hashing.sha1().hashBytes(avatar).toString(); if (sha1Hash.equalsIgnoreCase(id)) { diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java new file mode 100644 index 000000000..273dcfb25 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "BINVAL") +public class BinaryValue extends Extension implements ByteContent { + + public BinaryValue() { + super(BinaryValue.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java new file mode 100644 index 000000000..92adc6831 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "PHOTO") +public class Photo extends Extension { + public Photo() { + super(Photo.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java b/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java index 9b424e3ee..20a694977 100644 --- a/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java +++ b/app/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java @@ -3,7 +3,7 @@ package im.conversations.android.xmpp.model.vcard; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; -@XmlElement +@XmlElement(name = "vCard") public class VCard extends Extension { public VCard() { diff --git a/app/src/main/res/layout/item_chatoverview.xml b/app/src/main/res/layout/item_chatoverview.xml index d255e42bf..68650a18f 100644 --- a/app/src/main/res/layout/item_chatoverview.xml +++ b/app/src/main/res/layout/item_chatoverview.xml @@ -29,6 +29,7 @@ app:layout_constraintEnd_toStartOf="@+id/sentAt" app:layout_constraintStart_toEndOf="@+id/avatar" app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" app:layout_constraintVertical_chainStyle="packed" tools:text="The City of Verona" />