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}