VCardManager.java

  1package eu.siacs.conversations.xmpp.manager;
  2
  3import android.content.Context;
  4import android.util.Log;
  5import com.google.common.cache.Cache;
  6import com.google.common.cache.CacheBuilder;
  7import com.google.common.util.concurrent.Futures;
  8import com.google.common.util.concurrent.ListenableFuture;
  9import com.google.common.util.concurrent.MoreExecutors;
 10import eu.siacs.conversations.Config;
 11import eu.siacs.conversations.xmpp.Jid;
 12import eu.siacs.conversations.xmpp.XmppConnection;
 13import im.conversations.android.xmpp.IqErrorException;
 14import im.conversations.android.xmpp.model.error.Condition;
 15import im.conversations.android.xmpp.model.stanza.Iq;
 16import im.conversations.android.xmpp.model.vcard.BinaryValue;
 17import im.conversations.android.xmpp.model.vcard.Photo;
 18import im.conversations.android.xmpp.model.vcard.VCard;
 19import java.time.Duration;
 20import java.util.Objects;
 21
 22public class VCardManager extends AbstractManager {
 23
 24    private final Cache<Jid, Exception> photoExceptionCache =
 25            CacheBuilder.newBuilder()
 26                    .maximumSize(24_576)
 27                    .expireAfterWrite(Duration.ofHours(36))
 28                    .build();
 29
 30    public VCardManager(final Context context, final XmppConnection connection) {
 31        super(context, connection);
 32    }
 33
 34    public ListenableFuture<VCard> retrieve(final Jid address) {
 35        final var iq = new Iq(Iq.Type.GET, new VCard());
 36        iq.setTo(address);
 37        return Futures.transform(
 38                this.connection.sendIqPacket(iq),
 39                result -> {
 40                    final var vCard = result.getExtension(VCard.class);
 41                    if (vCard == null) {
 42                        throw new IllegalStateException("Result did not include vCard");
 43                    }
 44                    return vCard;
 45                },
 46                MoreExecutors.directExecutor());
 47    }
 48
 49    public ListenableFuture<byte[]> retrievePhotoCacheException(final Jid address) {
 50        final var existingException = this.photoExceptionCache.getIfPresent(address);
 51        if (existingException != null) {
 52            return Futures.immediateFailedFuture(existingException);
 53        }
 54        final var future = retrievePhoto(address);
 55        return Futures.catchingAsync(
 56                future,
 57                Exception.class,
 58                ex -> {
 59                    if (ex instanceof IllegalStateException || ex instanceof IqErrorException) {
 60                        photoExceptionCache.put(address, ex);
 61                    }
 62                    return Futures.immediateFailedFuture(ex);
 63                },
 64                MoreExecutors.directExecutor());
 65    }
 66
 67    private ListenableFuture<byte[]> retrievePhoto(final Jid address) {
 68        final var vCardFuture = retrieve(address);
 69        return Futures.transform(
 70                vCardFuture,
 71                vCard -> {
 72                    final var photo = vCard.getPhoto();
 73                    if (photo == null) {
 74                        throw new IllegalStateException(
 75                                String.format("No photo in vCard of %s", address));
 76                    }
 77                    final var binaryValue = photo.getBinaryValue();
 78                    if (binaryValue == null) {
 79                        throw new IllegalStateException(
 80                                String.format("Photo has no binary value in vCard of %s", address));
 81                    }
 82                    return binaryValue.asBytes();
 83                },
 84                MoreExecutors.directExecutor());
 85    }
 86
 87    public ListenableFuture<Void> publish(final VCard vCard) {
 88        return publish(getAccount().getJid().asBareJid(), vCard);
 89    }
 90
 91    public ListenableFuture<Void> publish(final Jid address, final VCard vCard) {
 92        final var iq = new Iq(Iq.Type.SET, vCard);
 93        iq.setTo(address);
 94        return Futures.transform(
 95                connection.sendIqPacket(iq), result -> null, MoreExecutors.directExecutor());
 96    }
 97
 98    public ListenableFuture<Void> deletePhoto() {
 99        final var vCardFuture = retrieve(getAccount().getJid().asBareJid());
100        return Futures.transformAsync(
101                vCardFuture,
102                vCard -> {
103                    final var photo = vCard.getPhoto();
104                    if (photo == null) {
105                        return Futures.immediateFuture(null);
106                    }
107                    Log.d(
108                            Config.LOGTAG,
109                            "deleting photo from vCard. binaryValue="
110                                    + Objects.nonNull(photo.getBinaryValue()));
111                    photo.clearChildren();
112                    return publish(vCard);
113                },
114                MoreExecutors.directExecutor());
115    }
116
117    public ListenableFuture<Void> publishPhoto(
118            final Jid address, final String type, final byte[] image) {
119        final var retrieveFuture = this.retrieve(address);
120
121        final var caughtFuture =
122                Futures.catchingAsync(
123                        retrieveFuture,
124                        IqErrorException.class,
125                        ex -> {
126                            final var error = ex.getError();
127                            if (error != null
128                                    && error.getCondition() instanceof Condition.ItemNotFound) {
129                                return Futures.immediateFuture(null);
130                            } else {
131                                return Futures.immediateFailedFuture(ex);
132                            }
133                        },
134                        MoreExecutors.directExecutor());
135
136        return Futures.transformAsync(
137                caughtFuture,
138                existing -> {
139                    final VCard vCard;
140                    if (existing == null) {
141                        Log.d(Config.LOGTAG, "item-not-found. created fresh vCard");
142                        vCard = new VCard();
143                    } else {
144                        vCard = existing;
145                    }
146                    final var photo = new Photo();
147                    photo.setType(type);
148                    photo.addExtension(new BinaryValue()).setContent(image);
149                    vCard.setExtension(photo);
150                    return publish(address, vCard);
151                },
152                MoreExecutors.directExecutor());
153    }
154}