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}