1package eu.siacs.conversations.xmpp.manager;
2
3import android.util.Log;
4import androidx.annotation.NonNull;
5import com.google.common.io.BaseEncoding;
6import com.google.common.util.concurrent.FutureCallback;
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.entities.Contact;
12import eu.siacs.conversations.services.XmppConnectionService;
13import eu.siacs.conversations.xml.Namespace;
14import eu.siacs.conversations.xmpp.Jid;
15import eu.siacs.conversations.xmpp.XmppConnection;
16import eu.siacs.conversations.xmpp.pep.Avatar;
17import im.conversations.android.xmpp.NodeConfiguration;
18import im.conversations.android.xmpp.model.ByteContent;
19import im.conversations.android.xmpp.model.avatar.Data;
20import im.conversations.android.xmpp.model.avatar.Info;
21import im.conversations.android.xmpp.model.avatar.Metadata;
22import im.conversations.android.xmpp.model.pubsub.Items;
23
24public class AvatarManager extends AbstractManager {
25
26 private final XmppConnectionService service;
27
28 public AvatarManager(final XmppConnectionService service, XmppConnection connection) {
29 super(service.getApplicationContext(), connection);
30 this.service = service;
31 }
32
33 public ListenableFuture<byte[]> fetch(final Jid address, final String itemId) {
34 final var future = getManager(PubSubManager.class).fetchItem(address, itemId, Data.class);
35 return Futures.transform(future, ByteContent::asBytes, MoreExecutors.directExecutor());
36 }
37
38 public ListenableFuture<Void> fetchAndStore(final Avatar avatar) {
39 final var future = fetch(avatar.owner, avatar.sha1sum);
40 return Futures.transform(
41 future,
42 data -> {
43 avatar.image = BaseEncoding.base64().encode(data);
44 if (service.getFileBackend().save(avatar)) {
45 setPepAvatar(avatar);
46 return null;
47 } else {
48 throw new IllegalStateException("Could not store avatar");
49 }
50 },
51 MoreExecutors.directExecutor());
52 }
53
54 private void setPepAvatar(final Avatar avatar) {
55 final var account = getAccount();
56 if (account.getJid().asBareJid().equals(avatar.owner)) {
57 if (account.setAvatar(avatar.getFilename())) {
58 getDatabase().updateAccount(account);
59 }
60 this.service.getAvatarService().clear(account);
61 this.service.updateConversationUi();
62 this.service.updateAccountUi();
63 } else {
64 final Contact contact = account.getRoster().getContact(avatar.owner);
65 contact.setAvatar(avatar);
66 account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
67 this.service.getAvatarService().clear(contact);
68 this.service.updateConversationUi();
69 this.service.updateRosterUi();
70 }
71 }
72
73 public void handleItems(final Jid from, final Items items) {
74 final var account = getAccount();
75 // TODO support retract
76 final var entry = items.getFirstItemWithId(Metadata.class);
77 final var avatar =
78 entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue());
79 if (avatar == null) {
80 Log.d(Config.LOGTAG, "could not parse avatar metadata from " + from);
81 return;
82 }
83 avatar.owner = from.asBareJid();
84 if (service.getFileBackend().isAvatarCached(avatar)) {
85 if (account.getJid().asBareJid().equals(from)) {
86 if (account.setAvatar(avatar.getFilename())) {
87 service.databaseBackend.updateAccount(account);
88 service.notifyAccountAvatarHasChanged(account);
89 }
90 service.getAvatarService().clear(account);
91 service.updateConversationUi();
92 service.updateAccountUi();
93 } else {
94 final Contact contact = account.getRoster().getContact(from);
95 if (contact.setAvatar(avatar)) {
96 connection.getManager(RosterManager.class).writeToDatabaseAsync();
97 service.getAvatarService().clear(contact);
98 service.updateConversationUi();
99 service.updateRosterUi();
100 }
101 }
102 } else if (service.isDataSaverDisabled()) {
103 final var future = this.fetchAndStore(avatar);
104 Futures.addCallback(
105 future,
106 new FutureCallback<Void>() {
107 @Override
108 public void onSuccess(Void result) {
109 Log.d(
110 Config.LOGTAG,
111 account.getJid().asBareJid()
112 + ": successfully fetched pep avatar for "
113 + avatar.owner);
114 }
115
116 @Override
117 public void onFailure(@NonNull Throwable t) {
118 Log.d(Config.LOGTAG, "could not fetch avatar", t);
119 }
120 },
121 MoreExecutors.directExecutor());
122 }
123 }
124
125 public void handleDelete(final Jid from) {
126 final var account = getAccount();
127 final boolean isAccount = account.getJid().asBareJid().equals(from);
128 if (isAccount) {
129 account.setAvatar(null);
130 getDatabase().updateAccount(account);
131 service.getAvatarService().clear(account);
132 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node");
133 }
134 }
135
136 public ListenableFuture<Void> publish(final Avatar avatar, final boolean open) {
137 final NodeConfiguration configuration =
138 open ? NodeConfiguration.OPEN : NodeConfiguration.PRESENCE;
139 final var avatarData = new Data();
140 avatarData.setContent(avatar.getImageAsBytes());
141 final var future =
142 getManager(PepManager.class).publish(avatarData, avatar.sha1sum, configuration);
143 return Futures.transformAsync(
144 future,
145 v -> {
146 final var id = avatar.sha1sum;
147 final var metadata = new Metadata();
148 final var info = metadata.addExtension(new Info());
149 info.setBytes(avatar.size);
150 info.setId(avatar.sha1sum);
151 info.setHeight(avatar.height);
152 info.setWidth(avatar.width);
153 info.setType(avatar.type);
154 return getManager(PepManager.class).publish(metadata, id, configuration);
155 },
156 MoreExecutors.directExecutor());
157 }
158
159 public boolean hasPepToVCardConversion() {
160 return getManager(DiscoManager.class).hasAccountFeature(Namespace.AVATAR_CONVERSION);
161 }
162}