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