fetch vcard avatars
This commit is contained in:
parent
417e801811
commit
9819ef7d05
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue