fetch vcard avatars

This commit is contained in:
Daniel Gultsch 2023-03-07 20:05:20 +01:00
parent 417e801811
commit 9819ef7d05
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
8 changed files with 82 additions and 17 deletions

View file

@ -16,6 +16,7 @@ import im.conversations.android.ui.graphics.drawable.AvatarDrawable;
import im.conversations.android.xmpp.ConnectionPool; import im.conversations.android.xmpp.ConnectionPool;
import im.conversations.android.xmpp.manager.AvatarManager; import im.conversations.android.xmpp.manager.AvatarManager;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.concurrent.CancellationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -66,6 +67,10 @@ public class AvatarFetcher {
@Override @Override
public void onFailure(@NonNull final Throwable throwable) { public void onFailure(@NonNull final Throwable throwable) {
if (throwable instanceof CancellationException) {
return;
}
// TODO on IqTimeout remove tag?
final var imageView = imageViewWeakReference.get(); final var imageView = imageViewWeakReference.get();
if (imageView == null) { if (imageView == null) {
LOGGER.info("ImageView reference was gone after avatar fetch failed"); 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) { public static void fetchInto(final ImageView imageView, final AvatarWithAccount avatar) {
final var tag = imageView.getTag(); final var tag = imageView.getTag();
if (tag instanceof AvatarFetcher avatarFetcher) { 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; return;
} }
avatarFetcher.future.cancel(true); avatarFetcher.future.cancel(true);

View file

@ -43,6 +43,7 @@ public class ChatOverviewAdapter
chatOverviewItem == null ? null : chatOverviewItem.getAddressWithName(); chatOverviewItem == null ? null : chatOverviewItem.getAddressWithName();
final var avatar = chatOverviewItem == null ? null : chatOverviewItem.getAvatar(); final var avatar = chatOverviewItem == null ? null : chatOverviewItem.getAvatar();
if (avatar != null) { if (avatar != null) {
holder.binding.avatar.setVisibility(View.VISIBLE);
AvatarFetcher.fetchInto(holder.binding.avatar, avatar); AvatarFetcher.fetchInto(holder.binding.avatar, avatar);
} else if (addressWithName != null) { } else if (addressWithName != null) {
holder.binding.avatar.setVisibility(View.VISIBLE); holder.binding.avatar.setVisibility(View.VISIBLE);

View file

@ -86,6 +86,7 @@ public class AvatarDrawable extends ColorDrawable {
canvas.getClipBounds(r); canvas.getClipBounds(r);
final int cHeight = r.height(); final int cHeight = r.height();
final int cWidth = r.width(); final int cWidth = r.width();
// TODO if we ever want to do rounded corners we can use drawRoundRect()
canvas.drawCircle(midX, midY, radius, paint); canvas.drawCircle(midX, midY, radius, paint);
if (letter == null) { if (letter == null) {
return; return;

View file

@ -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.Info;
import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.avatar.Metadata;
import im.conversations.android.xmpp.model.pubsub.Items; 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.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -70,7 +74,8 @@ public class AvatarManager extends AbstractManager {
} }
} }
public ListenableFuture<byte[]> getAvatar(final Jid address, final String id) { public ListenableFuture<byte[]> getAvatar(
final Jid address, final AvatarType type, final String id) {
final var fetch = new Fetch(address, id); final var fetch = new Fetch(address, id);
final SettableFuture<byte[]> future; final SettableFuture<byte[]> future;
synchronized (avatarFetches) { synchronized (avatarFetches) {
@ -81,7 +86,7 @@ public class AvatarManager extends AbstractManager {
future = SettableFuture.create(); future = SettableFuture.create();
avatarFetches.put(fetch, future); avatarFetches.put(fetch, future);
} }
future.setFuture(getCachedOrFetch(address, id)); future.setFuture(getCachedOrFetch(address, type, id));
future.addListener( future.addListener(
() -> { () -> {
synchronized (this.avatarFetches) { synchronized (this.avatarFetches) {
@ -94,15 +99,8 @@ public class AvatarManager extends AbstractManager {
public ListenableFuture<Bitmap> getAvatarBitmap( public ListenableFuture<Bitmap> getAvatarBitmap(
final Jid address, final AvatarType type, final String id) { final Jid address, final AvatarType type, final String id) {
final ListenableFuture<byte[]> 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( return Futures.transform(
avatar, getAvatar(address, type, id),
bytes -> { bytes -> {
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}, },
@ -120,26 +118,55 @@ public class AvatarManager extends AbstractManager {
} }
} }
private ListenableFuture<byte[]> getCachedOrFetch(final Jid address, final String id) { private ListenableFuture<byte[]> getCachedOrFetch(
final Jid address, final AvatarType type, final String id) {
final var cachedFuture = final var cachedFuture =
Futures.submit(() -> getCachedAvatar(address, id), FILE_IO_EXECUTOR); Futures.submit(() -> getCachedAvatar(address, id), FILE_IO_EXECUTOR);
return Futures.catchingAsync( return Futures.catchingAsync(
cachedFuture, cachedFuture,
Exception.class, Exception.class,
exception -> fetchAndCacheAvatar(address, id), exception -> fetchAndCacheAvatar(address, type, id),
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
} }
private ListenableFuture<byte[]> fetchAvatar(final Jid address, final String id) { private ListenableFuture<byte[]> fetchPepAvatar(final Jid address, final String id) {
return Futures.transform( return Futures.transform(
getManager(PubSubManager.class).fetchItem(address, id, Data.class), getManager(PubSubManager.class).fetchItem(address, id, Data.class),
Data::asBytes, Data::asBytes,
MoreExecutors.directExecutor()); MoreExecutors.directExecutor());
} }
private ListenableFuture<byte[]> fetchAndCacheAvatar(final Jid address, final String id) { private ListenableFuture<byte[]> 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( 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<byte[]> fetchAndCacheAvatar(
final Jid address, final AvatarType avatarType, final String id) {
final ListenableFuture<byte[]> avatarFetchFuture =
switch (avatarType) {
case PEP -> fetchPepAvatar(address, id);
case VCARD -> fetchVcardAvatar(address, id);
};
return Futures.transform(
avatarFetchFuture,
avatar -> { avatar -> {
final var sha1Hash = Hashing.sha1().hashBytes(avatar).toString(); final var sha1Hash = Hashing.sha1().hashBytes(avatar).toString();
if (sha1Hash.equalsIgnoreCase(id)) { if (sha1Hash.equalsIgnoreCase(id)) {

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -3,7 +3,7 @@ package im.conversations.android.xmpp.model.vcard;
import im.conversations.android.annotation.XmlElement; import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.Extension;
@XmlElement @XmlElement(name = "vCard")
public class VCard extends Extension { public class VCard extends Extension {
public VCard() { public VCard() {

View file

@ -29,6 +29,7 @@
app:layout_constraintEnd_toStartOf="@+id/sentAt" app:layout_constraintEnd_toStartOf="@+id/sentAt"
app:layout_constraintStart_toEndOf="@+id/avatar" app:layout_constraintStart_toEndOf="@+id/avatar"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.5"
app:layout_constraintVertical_chainStyle="packed" app:layout_constraintVertical_chainStyle="packed"
tools:text="The City of Verona" /> tools:text="The City of Verona" />