AvatarManager.java

  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}