refactor pep event parsing to new api

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java       | 805 
src/main/java/eu/siacs/conversations/entities/Bookmark.java                   |  40 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                | 133 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      | 127 
src/main/java/eu/siacs/conversations/xml/Namespace.java                       |   1 
src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java                     | 163 
src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java    |  22 
src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java       |  12 
src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java  |   4 
src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java   |  32 
src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java   |   2 
src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java         |   2 
src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java |   5 
src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java          |   5 
src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java           |   5 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java    |  14 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java    |  11 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java     |  14 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java     |   3 
src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java   |   3 
src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java |  13 
21 files changed, 894 insertions(+), 522 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java πŸ”—

@@ -6,10 +6,8 @@ import android.os.Bundle;
 import android.security.KeyChain;
 import android.util.Log;
 import android.util.Pair;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -17,38 +15,6 @@ import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
-
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.whispersystems.libsignal.IdentityKey;
-import org.whispersystems.libsignal.IdentityKeyPair;
-import org.whispersystems.libsignal.InvalidKeyException;
-import org.whispersystems.libsignal.InvalidKeyIdException;
-import org.whispersystems.libsignal.SessionBuilder;
-import org.whispersystems.libsignal.SignalProtocolAddress;
-import org.whispersystems.libsignal.UntrustedIdentityException;
-import org.whispersystems.libsignal.ecc.ECPublicKey;
-import org.whispersystems.libsignal.state.PreKeyBundle;
-import org.whispersystems.libsignal.state.PreKeyRecord;
-import org.whispersystems.libsignal.state.SignedPreKeyRecord;
-import org.whispersystems.libsignal.util.KeyHelper;
-
-import java.security.PrivateKey;
-import java.security.Security;
-import java.security.Signature;
-import java.security.cert.X509Certificate;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -71,6 +37,35 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportIn
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
 import im.conversations.android.xmpp.model.stanza.Iq;
+import java.security.PrivateKey;
+import java.security.Security;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.whispersystems.libsignal.IdentityKey;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.InvalidKeyException;
+import org.whispersystems.libsignal.InvalidKeyIdException;
+import org.whispersystems.libsignal.SessionBuilder;
+import org.whispersystems.libsignal.SignalProtocolAddress;
+import org.whispersystems.libsignal.UntrustedIdentityException;
+import org.whispersystems.libsignal.ecc.ECPublicKey;
+import org.whispersystems.libsignal.state.PreKeyBundle;
+import org.whispersystems.libsignal.state.PreKeyRecord;
+import org.whispersystems.libsignal.state.SignedPreKeyRecord;
+import org.whispersystems.libsignal.util.KeyHelper;
 
 public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
@@ -102,8 +97,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
     private int numPublishTriesOnEmptyPep = 0;
     private boolean pepBroken = false;
     private int lastDeviceListNotificationHash = 0;
-    private final Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
-    private final Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
+    private final Set<XmppAxolotlSession> postponedSessions =
+            new HashSet<>(); // sessions stored here will receive after mam catchup treatment
+    private final Set<SignalProtocolAddress> postponedHealing =
+            new HashSet<>(); // addresses stored here will need a healing notification after mam
+    // catchup
     private final AtomicBoolean changeAccessMode = new AtomicBoolean(false);
 
     public AxolotlService(Account account, XmppConnectionService connectionService) {
@@ -156,7 +154,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         for (Jid jid : jids) {
             if (deviceIds.get(jid) != null) {
                 for (Integer foreignId : this.deviceIds.get(jid)) {
-                    SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId);
+                    SignalProtocolAddress address =
+                            new SignalProtocolAddress(jid.toString(), foreignId);
                     if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) {
                         return true;
                     }
@@ -167,11 +166,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     public void preVerifyFingerprint(Contact contact, String fingerprint) {
-        axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint);
+        axolotlStore.preVerifyFingerprint(
+                contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint);
     }
 
     public void preVerifyFingerprint(Account account, String fingerprint) {
-        axolotlStore.preVerifyFingerprint(account, account.getJid().asBareJid().toString(), fingerprint);
+        axolotlStore.preVerifyFingerprint(
+                account, account.getJid().asBareJid().toString(), fingerprint);
     }
 
     public boolean hasVerifiedKeys(String name) {
@@ -184,11 +185,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     public String getOwnFingerprint() {
-        return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
+        return CryptoHelper.bytesToHex(
+                axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
     }
 
     public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
-        return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status);
+        return axolotlStore.getContactKeysWithTrust(
+                account.getJid().asBareJid().toString(), status);
     }
 
     public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
@@ -226,21 +229,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
     public Collection<XmppAxolotlSession> findOwnSessions() {
         SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid());
-        ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values());
+        ArrayList<XmppAxolotlSession> s =
+                new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values());
         Collections.sort(s);
         return s;
     }
 
     public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
         SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
-        ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
+        ArrayList<XmppAxolotlSession> s =
+                new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
         Collections.sort(s);
         return s;
     }
 
     private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
         if (conversation.getContact().isSelf()) {
-            //will be added in findOwnSessions()
+            // will be added in findOwnSessions()
             return Collections.emptySet();
         }
         HashSet<XmppAxolotlSession> sessions = new HashSet<>();
@@ -280,7 +285,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     public void destroy() {
-        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use");
+        Log.d(
+                Config.LOGTAG,
+                account.getJid().asBareJid()
+                        + ": destroying old axolotl service. no longer in use");
         mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
     }
 
@@ -306,7 +314,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         final boolean me = jid.asBareJid().equals(account.getJid().asBareJid());
         if (me) {
             if (hash != 0 && hash == this.lastDeviceListNotificationHash) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring duplicate own device id list");
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid() + ": ignoring duplicate own device id list");
                 return;
             }
             this.lastDeviceListNotificationHash = hash;
@@ -315,10 +325,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         if (me) {
             deviceIds.remove(getOwnDeviceId());
         }
-        final Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString()));
+        final Set<Integer> expiredDevices =
+                new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString()));
         expiredDevices.removeAll(deviceIds);
         for (Integer deviceId : expiredDevices) {
-            SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+            SignalProtocolAddress address =
+                    new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
             XmppAxolotlSession session = sessions.get(address);
             if (session != null && session.getFingerprint() != null) {
                 if (session.getTrust().isActive()) {
@@ -328,11 +340,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         }
         final Set<Integer> newDevices = ImmutableSet.copyOf(deviceIds);
         for (final Integer deviceId : newDevices) {
-            SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+            SignalProtocolAddress address =
+                    new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
             XmppAxolotlSession session = sessions.get(address);
             if (session != null && session.getFingerprint() != null) {
                 if (!session.getTrust().isActive()) {
-                    Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint());
+                    Log.d(
+                            Config.LOGTAG,
+                            "reactivating device with fingerprint " + session.getFingerprint());
                     session.setTrust(session.getTrust().toActive());
                 }
             }
@@ -343,7 +358,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             }
             needsPublishing |= this.changeAccessMode.get();
             for (final Integer deviceId : deviceIds) {
-                SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
+                SignalProtocolAddress ownDeviceAddress =
+                        new SignalProtocolAddress(jid.asBareJid().toString(), deviceId);
                 if (sessions.get(ownDeviceAddress) == null) {
                     FetchStatus status = fetchStatusMap.get(ownDeviceAddress);
                     if (status == null || status == FetchStatus.TIMEOUT) {
@@ -363,7 +379,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         final boolean changed = oldSet == null || oldSet.hashCode() != hash;
         this.deviceIds.put(jid, deviceIds);
         if (changed) {
-            mXmppConnectionService.updateConversationUi(); //update the lock icon
+            mXmppConnectionService.updateConversationUi(); // update the lock icon
             mXmppConnectionService.keyStatusUpdated(null);
             if (me) {
                 mXmppConnectionService.updateAccountUi();
@@ -375,7 +391,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
     public void wipeOtherPepDevices() {
         if (pepBroken) {
-            Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
+            Log.d(
+                    Config.LOGTAG,
+                    getLogprefix(account)
+                            + "wipeOtherPepDevices called, but PEP is broken. Ignoring... ");
             return;
         }
         Set<Integer> deviceIds = new HashSet<>();
@@ -391,22 +410,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
     private void publishOwnDeviceIdIfNeeded() {
         if (pepBroken) {
-            Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
+            Log.d(
+                    Config.LOGTAG,
+                    getLogprefix(account)
+                            + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... ");
             return;
         }
-        Iq packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid());
-        mXmppConnectionService.sendIqPacket(account, packet, response -> {
-            if (response.getType() == Iq.Type.TIMEOUT) {
-                Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids.");
-            } else {
-                //TODO consider calling registerDevices only after item-not-found to account for broken PEPs
-                final Element item = IqParser.getItem(response);
-                final Set<Integer> deviceIds = IqParser.deviceIds(item);
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds);
-                registerDevices(account.getJid().asBareJid(), deviceIds);
-            }
-
-        });
+        Iq packet =
+                mXmppConnectionService
+                        .getIqGenerator()
+                        .retrieveDeviceIds(account.getJid().asBareJid());
+        mXmppConnectionService.sendIqPacket(
+                account,
+                packet,
+                response -> {
+                    if (response.getType() == Iq.Type.TIMEOUT) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getLogprefix(account)
+                                        + "Timeout received while retrieving own Device Ids.");
+                    } else {
+                        // TODO consider calling registerDevices only after item-not-found to
+                        // account for broken PEPs
+                        // TODO use new API
+                        final Element item = IqParser.getItem(response);
+                        final Set<Integer> deviceIds = IqParser.deviceIds(item);
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": retrieved own device list: "
+                                        + deviceIds);
+                        registerDevices(account.getJid().asBareJid(), deviceIds);
+                    }
+                });
     }
 
     private Set<Integer> getExpiredDevices() {
@@ -415,16 +451,34 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
             if (session.getTrust().isActive()) {
                 long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
                 if (diff > Config.OMEMO_AUTO_EXPIRY) {
-                    long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint());
+                    long lastMessageDiff =
+                            System.currentTimeMillis()
+                                    - mXmppConnectionService.databaseBackend
+                                            .getLastTimeFingerprintUsed(
+                                                    account, session.getFingerprint());
                     long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0));
                     if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
                         devices.add(session.getRemoteAddress().getDeviceId());
                         session.setTrust(session.getTrust().toInactive());
-                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago");
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": added own device "
+                                        + session.getFingerprint()
+                                        + " to list of expired devices. Last message received "
+                                        + hours
+                                        + " hours ago");
                     } else {
-                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago");
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": own device "
+                                        + session.getFingerprint()
+                                        + " was active "
+                                        + hours
+                                        + " hours ago");
                     }
-                } //TODO print last activation diff
+                } // TODO print last activation diff
             }
         }
         return devices;
@@ -435,12 +489,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids");
         if (deviceIdsCopy.isEmpty()) {
             if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
-                Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
+                Log.w(
+                        Config.LOGTAG,
+                        getLogprefix(account)
+                                + "Own device publish attempt threshold exceeded, aborting...");
                 pepBroken = true;
                 return;
             } else {
                 numPublishTriesOnEmptyPep++;
-                Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
+                Log.w(
+                        Config.LOGTAG,
+                        getLogprefix(account)
+                                + "Own device list empty, attempting to publish (try "
+                                + numPublishTriesOnEmptyPep
+                                + ")");
             }
         } else {
             numPublishTriesOnEmptyPep = 0;
@@ -453,74 +515,136 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
         publishDeviceIdsAndRefineAccessModel(ids, true);
     }
 
-    private void publishDeviceIdsAndRefineAccessModel(final Set<Integer> ids, final boolean firstAttempt) {
-        final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
-        final var publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
-        mXmppConnectionService.sendIqPacket(account, publish, response -> {
-            final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null;
-            final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response);
-            if (firstAttempt && preConditionNotMet) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration");
-                mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
-                    @Override
-                    public void onPushSucceeded() {
-                        publishDeviceIdsAndRefineAccessModel(ids, false);
-                    }
-
-                    @Override
-                    public void onPushFailed() {
-                        publishDeviceIdsAndRefineAccessModel(ids, false);
+    private void publishDeviceIdsAndRefineAccessModel(
+            final Set<Integer> ids, final boolean firstAttempt) {
+        final Bundle publishOptions =
+                account.getXmppConnection().getFeatures().pepPublishOptions()
+                        ? PublishOptions.openAccess()
+                        : null;
+        final var publish =
+                mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions);
+        mXmppConnectionService.sendIqPacket(
+                account,
+                publish,
+                response -> {
+                    final Element error =
+                            response.getType() == Iq.Type.ERROR
+                                    ? response.findChild("error")
+                                    : null;
+                    final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response);
+                    if (firstAttempt && preConditionNotMet) {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": precondition wasn't met for device list. pushing node"
+                                        + " configuration");
+                        mXmppConnectionService.pushNodeConfiguration(
+                                account,
+                                AxolotlService.PEP_DEVICE_LIST,
+                                publishOptions,
+                                new XmppConnectionService.OnConfigurationPushed() {
+                                    @Override
+                                    public void onPushSucceeded() {
+                                        publishDeviceIdsAndRefineAccessModel(ids, false);
+                                    }
+
+                                    @Override
+                                    public void onPushFailed() {
+                                        publishDeviceIdsAndRefineAccessModel(ids, false);
+                                    }
+                                });
+                    } else {
+                        if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    account.getJid().asBareJid() + ": done changing access mode");
+                            account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
+                            mXmppConnectionService.databaseBackend.updateAccount(account);
+                        }
+                        if (response.getType() == Iq.Type.ERROR) {
+                            if (preConditionNotMet) {
+                                Log.d(
+                                        Config.LOGTAG,
+                                        account.getJid().asBareJid()
+                                                + ": device list pre condition still not met on"
+                                                + " second attempt");
+                            } else if (error != null) {
+                                pepBroken = true;
+                                Log.d(
+                                        Config.LOGTAG,
+                                        getLogprefix(account)
+                                                + "Error received while publishing own device id"
+                                                + response.findChild("error"));
+                            }
+                        }
                     }
                 });
-            } else {
-                if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode");
-                    account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false);
-                    mXmppConnectionService.databaseBackend.updateAccount(account);
-                }
-                if (response.getType() == Iq.Type.ERROR) {
-                    if (preConditionNotMet) {
-                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt");
-                    } else if (error != null) {
-                        pepBroken = true;
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + response.findChild("error"));
-                    }
-
-                }
-            }
-        });
     }
 
-    public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
-                                                   final Set<PreKeyRecord> preKeyRecords,
-                                                   final boolean announceAfter,
-                                                   final boolean wipe) {
+    public void publishDeviceVerificationAndBundle(
+            final SignedPreKeyRecord signedPreKeyRecord,
+            final Set<PreKeyRecord> preKeyRecords,
+            final boolean announceAfter,
+            final boolean wipe) {
         try {
             IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey();
-            PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
-            X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
+            PrivateKey x509PrivateKey =
+                    KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
+            X509Certificate[] chain =
+                    KeyChain.getCertificateChain(
+                            mXmppConnectionService, account.getPrivateKeyAlias());
             Signature verifier = Signature.getInstance("sha256WithRSA");
             verifier.initSign(x509PrivateKey, SECURE_RANDOM);
             verifier.update(axolotlPublicKey.serialize());
             byte[] signature = verifier.sign();
-            final Iq packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
-            Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId());
-            mXmppConnectionService.sendIqPacket(account, packet, response -> {
-                String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
-                mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() {
-                    @Override
-                    public void onPushSucceeded() {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable");
-                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
-                    }
-
-                    @Override
-                    public void onPushFailed() {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node");
-                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe);
-                    }
-                });
-            });
+            final Iq packet =
+                    mXmppConnectionService
+                            .getIqGenerator()
+                            .publishVerification(signature, chain, getOwnDeviceId());
+            Log.d(
+                    Config.LOGTAG,
+                    AxolotlService.getLogprefix(account)
+                            + ": publish verification for device "
+                            + getOwnDeviceId());
+            mXmppConnectionService.sendIqPacket(
+                    account,
+                    packet,
+                    response -> {
+                        String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId();
+                        mXmppConnectionService.pushNodeConfiguration(
+                                account,
+                                node,
+                                PublishOptions.openAccess(),
+                                new XmppConnectionService.OnConfigurationPushed() {
+                                    @Override
+                                    public void onPushSucceeded() {
+                                        Log.d(
+                                                Config.LOGTAG,
+                                                getLogprefix(account)
+                                                        + "configured verification node to be world"
+                                                        + " readable");
+                                        publishDeviceBundle(
+                                                signedPreKeyRecord,
+                                                preKeyRecords,
+                                                announceAfter,
+                                                wipe);
+                                    }
+
+                                    @Override
+                                    public void onPushFailed() {
+                                        Log.d(
+                                                Config.LOGTAG,
+                                                getLogprefix(account)
+                                                        + "unable to set access model on"
+                                                        + " verification node");
+                                        publishDeviceBundle(
+                                                signedPreKeyRecord,
+                                                preKeyRecords,
+                                                announceAfter,
+                                                wipe);
+                                    }
+                                });
+                    });
         } catch (Exception e) {
             e.printStackTrace();
         }
@@ -528,175 +652,310 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
     public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) {
         if (pepBroken) {
-            Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
+            Log.d(
+                    Config.LOGTAG,
+                    getLogprefix(account)
+                            + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... ");
             return;
         }
 
         if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
-            this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE));
+            this.changeAccessMode.set(
+                    account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE));
         } else {
             if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server doesn’t support publish-options. setting for later access mode change");
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": server doesn’t support publish-options. setting for later"
+                                + " access mode change");
                 mXmppConnectionService.databaseBackend.updateAccount(account);
             }
         }
         if (this.changeAccessMode.get()) {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model");
-        }
-        final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId());
-        mXmppConnectionService.sendIqPacket(account, packet, response -> {
-
-            if (response.getType() == Iq.Type.TIMEOUT) {
-                return; //ignore timeout. do nothing
-            }
-
-            if (response.getType() == Iq.Type.ERROR) {
-                Element error = response.findChild("error");
-                if (error == null || !error.hasChild("item-not-found")) {
-                    pepBroken = true;
-                    Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + response);
-                    return;
-                }
-            }
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + ": server gained publish-options capabilities. changing access"
+                            + " model");
+        }
+        final Iq packet =
+                mXmppConnectionService
+                        .getIqGenerator()
+                        .retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId());
+        mXmppConnectionService.sendIqPacket(
+                account,
+                packet,
+                response -> {
+                    if (response.getType() == Iq.Type.TIMEOUT) {
+                        return; // ignore timeout. do nothing
+                    }
 
-            PreKeyBundle bundle = IqParser.bundle(response);
-            final Map<Integer, ECPublicKey> keys = IqParser.preKeyPublics(response);
-            boolean flush = false;
-            if (bundle == null) {
-                Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + response);
-                bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
-                flush = true;
-            }
-            if (keys == null) {
-                Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + response);
-            }
-            try {
-                boolean changed = false;
-                // Validate IdentityKey
-                IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
-                if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
-                    Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
-                    changed = true;
-                }
+                    if (response.getType() == Iq.Type.ERROR) {
+                        Element error = response.findChild("error");
+                        if (error == null || !error.hasChild("item-not-found")) {
+                            pepBroken = true;
+                            Log.w(
+                                    Config.LOGTAG,
+                                    AxolotlService.getLogprefix(account)
+                                            + "request for device bundles came back with something"
+                                            + " other than item-not-found"
+                                            + response);
+                            return;
+                        }
+                    }
 
-                // Validate signedPreKeyRecord + ID
-                SignedPreKeyRecord signedPreKeyRecord;
-                int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
-                try {
-                    signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
-                    if (flush
-                            || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
-                            || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
-                        Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
-                        signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
-                        axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
-                        changed = true;
+                    PreKeyBundle bundle = IqParser.bundle(response);
+                    final Map<Integer, ECPublicKey> keys = IqParser.preKeyPublics(response);
+                    boolean flush = false;
+                    if (bundle == null) {
+                        Log.w(
+                                Config.LOGTAG,
+                                AxolotlService.getLogprefix(account)
+                                        + "Received invalid bundle:"
+                                        + response);
+                        bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
+                        flush = true;
                     }
-                } catch (InvalidKeyIdException e) {
-                    Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
-                    signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
-                    axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
-                    changed = true;
-                }
+                    if (keys == null) {
+                        Log.w(
+                                Config.LOGTAG,
+                                AxolotlService.getLogprefix(account)
+                                        + "Received invalid prekeys:"
+                                        + response);
+                    }
+                    try {
+                        boolean changed = false;
+                        // Validate IdentityKey
+                        IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
+                        if (flush
+                                || !identityKeyPair
+                                        .getPublicKey()
+                                        .equals(bundle.getIdentityKey())) {
+                            Log.i(
+                                    Config.LOGTAG,
+                                    AxolotlService.getLogprefix(account)
+                                            + "Adding own IdentityKey "
+                                            + identityKeyPair.getPublicKey()
+                                            + " to PEP.");
+                            changed = true;
+                        }
 
-                // Validate PreKeys
-                Set<PreKeyRecord> preKeyRecords = new HashSet<>();
-                if (keys != null) {
-                    for (Integer id : keys.keySet()) {
+                        // Validate signedPreKeyRecord + ID
+                        SignedPreKeyRecord signedPreKeyRecord;
+                        int numSignedPreKeys = axolotlStore.getSignedPreKeysCount();
                         try {
-                            PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
-                            if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
-                                preKeyRecords.add(preKeyRecord);
+                            signedPreKeyRecord =
+                                    axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
+                            if (flush
+                                    || !bundle.getSignedPreKey()
+                                            .equals(signedPreKeyRecord.getKeyPair().getPublicKey())
+                                    || !Arrays.equals(
+                                            bundle.getSignedPreKeySignature(),
+                                            signedPreKeyRecord.getSignature())) {
+                                Log.i(
+                                        Config.LOGTAG,
+                                        AxolotlService.getLogprefix(account)
+                                                + "Adding new signedPreKey with ID "
+                                                + (numSignedPreKeys + 1)
+                                                + " to PEP.");
+                                signedPreKeyRecord =
+                                        KeyHelper.generateSignedPreKey(
+                                                identityKeyPair, numSignedPreKeys + 1);
+                                axolotlStore.storeSignedPreKey(
+                                        signedPreKeyRecord.getId(), signedPreKeyRecord);
+                                changed = true;
                             }
-                        } catch (InvalidKeyIdException ignored) {
+                        } catch (InvalidKeyIdException e) {
+                            Log.i(
+                                    Config.LOGTAG,
+                                    AxolotlService.getLogprefix(account)
+                                            + "Adding new signedPreKey with ID "
+                                            + (numSignedPreKeys + 1)
+                                            + " to PEP.");
+                            signedPreKeyRecord =
+                                    KeyHelper.generateSignedPreKey(
+                                            identityKeyPair, numSignedPreKeys + 1);
+                            axolotlStore.storeSignedPreKey(
+                                    signedPreKeyRecord.getId(), signedPreKeyRecord);
+                            changed = true;
                         }
-                    }
-                }
-                int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
-                if (newKeys > 0) {
-                    List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
-                            axolotlStore.getCurrentPreKeyId() + 1, newKeys);
-                    preKeyRecords.addAll(newRecords);
-                    for (PreKeyRecord record : newRecords) {
-                        axolotlStore.storePreKey(record.getId(), record);
-                    }
-                    changed = true;
-                    Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
-                }
 
+                        // Validate PreKeys
+                        Set<PreKeyRecord> preKeyRecords = new HashSet<>();
+                        if (keys != null) {
+                            for (Integer id : keys.keySet()) {
+                                try {
+                                    PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
+                                    if (preKeyRecord
+                                            .getKeyPair()
+                                            .getPublicKey()
+                                            .equals(keys.get(id))) {
+                                        preKeyRecords.add(preKeyRecord);
+                                    }
+                                } catch (InvalidKeyIdException ignored) {
+                                }
+                            }
+                        }
+                        int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
+                        if (newKeys > 0) {
+                            List<PreKeyRecord> newRecords =
+                                    KeyHelper.generatePreKeys(
+                                            axolotlStore.getCurrentPreKeyId() + 1, newKeys);
+                            preKeyRecords.addAll(newRecords);
+                            for (PreKeyRecord record : newRecords) {
+                                axolotlStore.storePreKey(record.getId(), record);
+                            }
+                            changed = true;
+                            Log.i(
+                                    Config.LOGTAG,
+                                    AxolotlService.getLogprefix(account)
+                                            + "Adding "
+                                            + newKeys
+                                            + " new preKeys to PEP.");
+                        }
 
-                if (changed || changeAccessMode.get()) {
-                    if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
-                        mXmppConnectionService.publishDisplayName(account);
-                        publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
-                    } else {
-                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe);
-                    }
-                } else {
-                    Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current");
-                    if (wipe) {
-                        wipeOtherPepDevices();
-                    } else if (announce) {
-                        Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
-                        publishOwnDeviceIdIfNeeded();
+                        if (changed || changeAccessMode.get()) {
+                            if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) {
+                                mXmppConnectionService.publishDisplayName(account);
+                                publishDeviceVerificationAndBundle(
+                                        signedPreKeyRecord, preKeyRecords, announce, wipe);
+                            } else {
+                                publishDeviceBundle(
+                                        signedPreKeyRecord, preKeyRecords, announce, wipe);
+                            }
+                        } else {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    getLogprefix(account)
+                                            + "Bundle "
+                                            + getOwnDeviceId()
+                                            + " in PEP was current");
+                            if (wipe) {
+                                wipeOtherPepDevices();
+                            } else if (announce) {
+                                Log.d(
+                                        Config.LOGTAG,
+                                        getLogprefix(account)
+                                                + "Announcing device "
+                                                + getOwnDeviceId());
+                                publishOwnDeviceIdIfNeeded();
+                            }
+                        }
+                    } catch (InvalidKeyException e) {
+                        Log.e(
+                                Config.LOGTAG,
+                                AxolotlService.getLogprefix(account)
+                                        + "Failed to publish bundle "
+                                        + getOwnDeviceId()
+                                        + ", reason: "
+                                        + e.getMessage());
                     }
-                }
-            } catch (InvalidKeyException e) {
-                Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
-            }
-        });
+                });
     }
 
-    private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord,
-                                     Set<PreKeyRecord> preKeyRecords,
-                                     final boolean announceAfter,
-                                     final boolean wipe) {
+    private void publishDeviceBundle(
+            SignedPreKeyRecord signedPreKeyRecord,
+            Set<PreKeyRecord> preKeyRecords,
+            final boolean announceAfter,
+            final boolean wipe) {
         publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true);
     }
 
-    private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord,
-                                     final Set<PreKeyRecord> preKeyRecords,
-                                     final boolean announceAfter,
-                                     final boolean wipe,
-                                     final boolean firstAttempt) {
-        final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
-        final Iq publish = mXmppConnectionService.getIqGenerator().publishBundles(
-                signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
-                preKeyRecords, getOwnDeviceId(), publishOptions);
-        Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing...");
-        mXmppConnectionService.sendIqPacket(account, publish, response -> {
-            final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response);
-            if (firstAttempt && preconditionNotMet) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration");
-                final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
-                mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() {
-                    @Override
-                    public void onPushSucceeded() {
-                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
-                    }
-
-                    @Override
-                    public void onPushFailed() {
-                        publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false);
+    private void publishDeviceBundle(
+            final SignedPreKeyRecord signedPreKeyRecord,
+            final Set<PreKeyRecord> preKeyRecords,
+            final boolean announceAfter,
+            final boolean wipe,
+            final boolean firstAttempt) {
+        final Bundle publishOptions =
+                account.getXmppConnection().getFeatures().pepPublishOptions()
+                        ? PublishOptions.openAccess()
+                        : null;
+        final Iq publish =
+                mXmppConnectionService
+                        .getIqGenerator()
+                        .publishBundles(
+                                signedPreKeyRecord,
+                                axolotlStore.getIdentityKeyPair().getPublicKey(),
+                                preKeyRecords,
+                                getOwnDeviceId(),
+                                publishOptions);
+        Log.d(
+                Config.LOGTAG,
+                AxolotlService.getLogprefix(account)
+                        + ": Bundle "
+                        + getOwnDeviceId()
+                        + " in PEP not current. Publishing...");
+        mXmppConnectionService.sendIqPacket(
+                account,
+                publish,
+                response -> {
+                    final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response);
+                    if (firstAttempt && preconditionNotMet) {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": precondition wasn't met for bundle. pushing node"
+                                        + " configuration");
+                        final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
+                        mXmppConnectionService.pushNodeConfiguration(
+                                account,
+                                node,
+                                publishOptions,
+                                new XmppConnectionService.OnConfigurationPushed() {
+                                    @Override
+                                    public void onPushSucceeded() {
+                                        publishDeviceBundle(
+                                                signedPreKeyRecord,
+                                                preKeyRecords,
+                                                announceAfter,
+                                                wipe,
+                                                false);
+                                    }
+
+                                    @Override
+                                    public void onPushFailed() {
+                                        publishDeviceBundle(
+                                                signedPreKeyRecord,
+                                                preKeyRecords,
+                                                announceAfter,
+                                                wipe,
+                                                false);
+                                    }
+                                });
+                    } else if (response.getType() == Iq.Type.RESULT) {
+                        Log.d(
+                                Config.LOGTAG,
+                                AxolotlService.getLogprefix(account)
+                                        + "Successfully published bundle. ");
+                        if (wipe) {
+                            wipeOtherPepDevices();
+                        } else if (announceAfter) {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    getLogprefix(account)
+                                            + "Announcing device "
+                                            + getOwnDeviceId());
+                            publishOwnDeviceIdIfNeeded();
+                        }
+                    } else if (response.getType() == Iq.Type.ERROR) {
+                        if (preconditionNotMet) {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    getLogprefix(account)
+                                            + "bundle precondition still not met after second"
+                                            + " attempt");
+                        } else {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    getLogprefix(account)
+                                            + "Error received while publishing bundle: "
+                                            + response.toString());
+                        }
+                        pepBroken = true;
                     }
                 });
-            } else if (response.getType() == Iq.Type.RESULT) {
-                Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. ");
-                if (wipe) {
-                    wipeOtherPepDevices();
-                } else if (announceAfter) {
-                    Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
-                    publishOwnDeviceIdIfNeeded();
-                }
-            } else if (response.getType() == Iq.Type.ERROR) {
-                if (preconditionNotMet) {
-                    Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt");
-                } else {
-                    Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + response.toString());
-                }
-                pepBroken = true;
-            }
-        });
     }
 
     public void deleteOmemoIdentity() {

src/main/java/eu/siacs/conversations/entities/Bookmark.java πŸ”—

@@ -10,6 +10,9 @@ import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.bookmark2.Conference;
+import im.conversations.android.xmpp.model.pubsub.PubSub;
 import java.lang.ref.WeakReference;
 import java.util.Collections;
 import java.util.HashMap;
@@ -36,7 +39,8 @@ public class Bookmark extends Element implements ListItem {
         this.account = account;
     }
 
-    public static Map<Jid, Bookmark> parseFromStorage(Element storage, Account account) {
+    public static Map<Jid, Bookmark> parseFromStorage(
+            final Storage storage, final Account account) {
         if (storage == null) {
             return Collections.emptyMap();
         }
@@ -57,24 +61,24 @@ public class Bookmark extends Element implements ListItem {
         return bookmarks;
     }
 
-    public static Map<Jid, Bookmark> parseFromPubSub(final Element pubSub, final Account account) {
+    public static Map<Jid, Bookmark> parseFromPubSub(final PubSub pubSub, final Account account) {
         if (pubSub == null) {
             return Collections.emptyMap();
         }
-        final Element items = pubSub.findChild("items");
-        if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) {
-            final Map<Jid, Bookmark> bookmarks = new HashMap<>();
-            for (Element item : items.getChildren()) {
-                if (item.getName().equals("item")) {
-                    final Bookmark bookmark = Bookmark.parseFromItem(item, account);
-                    if (bookmark != null) {
-                        bookmarks.put(bookmark.jid, bookmark);
-                    }
-                }
+        final var items = pubSub.getItems();
+        if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) {
+            return Collections.emptyMap();
+        }
+        final Map<Jid, Bookmark> bookmarks = new HashMap<>();
+        for (final var item : items.getItemMap(Conference.class).entrySet()) {
+            final Bookmark bookmark =
+                    Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
+            if (bookmark == null) {
+                continue;
             }
-            return bookmarks;
+            bookmarks.put(bookmark.jid, bookmark);
         }
-        return Collections.emptyMap();
+        return bookmarks;
     }
 
     public static Bookmark parse(Element element, Account account) {
@@ -88,13 +92,13 @@ public class Bookmark extends Element implements ListItem {
         return bookmark;
     }
 
-    public static Bookmark parseFromItem(Element item, Account account) {
-        final Element conference = item.findChild("conference", Namespace.BOOKMARKS2);
-        if (conference == null) {
+    public static Bookmark parseFromItem(
+            final String id, final Conference conference, final Account account) {
+        if (id == null || conference == null) {
             return null;
         }
         final Bookmark bookmark = new Bookmark(account);
-        bookmark.jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id"));
+        bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id));
         // TODO verify that we only use bare jids and ignore full jids
         if (bookmark.jid == null) {
             return null;

src/main/java/eu/siacs/conversations/parser/MessageParser.java πŸ”—

@@ -2,6 +2,7 @@ package eu.siacs.conversations.parser;
 
 import android.util.Log;
 import android.util.Pair;
+import androidx.annotation.NonNull;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 import eu.siacs.conversations.AppSettings;
@@ -37,15 +38,23 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.avatar.Metadata;
+import im.conversations.android.xmpp.model.axolotl.DeviceList;
 import im.conversations.android.xmpp.model.axolotl.Encrypted;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.bookmark2.Conference;
 import im.conversations.android.xmpp.model.carbons.Received;
 import im.conversations.android.xmpp.model.carbons.Sent;
 import im.conversations.android.xmpp.model.correction.Replace;
 import im.conversations.android.xmpp.model.forward.Forwarded;
 import im.conversations.android.xmpp.model.markers.Displayed;
+import im.conversations.android.xmpp.model.nick.Nick;
 import im.conversations.android.xmpp.model.occupant.OccupantId;
 import im.conversations.android.xmpp.model.oob.OutOfBandData;
+import im.conversations.android.xmpp.model.pubsub.Items;
+import im.conversations.android.xmpp.model.pubsub.event.Delete;
 import im.conversations.android.xmpp.model.pubsub.event.Event;
+import im.conversations.android.xmpp.model.pubsub.event.Purge;
 import im.conversations.android.xmpp.model.reactions.Reactions;
 import im.conversations.android.xmpp.model.receipts.Request;
 import im.conversations.android.xmpp.model.unique.StanzaId;
@@ -53,6 +62,7 @@ import java.text.SimpleDateFormat;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -244,11 +254,13 @@ public class MessageParser extends AbstractParser
         return null;
     }
 
-    private void parseEvent(final Event event, final Jid from, final Account account) {
-        final Element items = event.findChild("items");
-        final String node = items == null ? null : items.getAttribute("node");
+    private void parseEvent(final Items items, final Jid from, final Account account) {
+        final String node = items.getNode();
         if ("urn:xmpp:avatar:metadata".equals(node)) {
-            Avatar avatar = Avatar.parseMetadata(items);
+            // TODO support retract
+            final var entry = items.getFirstItemWithId(Metadata.class);
+            final var avatar =
+                    entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue());
             if (avatar != null) {
                 avatar.owner = from.asBareJid();
                 if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
@@ -274,24 +286,27 @@ public class MessageParser extends AbstractParser
                 }
             }
         } else if (Namespace.NICK.equals(node)) {
-            final Element i = items.findChild("item");
-            final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK);
+            final var nickItem = items.getFirstItem(Nick.class);
+            final String nick = nickItem == null ? null : nickItem.getContent();
             if (nick != null) {
                 setNick(account, from, nick);
             }
         } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
-            Element item = items.findChild("item");
-            final Set<Integer> deviceIds = IqParser.deviceIds(item);
-            Log.d(
-                    Config.LOGTAG,
-                    AxolotlService.getLogprefix(account)
-                            + "Received PEP device list "
-                            + deviceIds
-                            + " update from "
-                            + from
-                            + ", processing... ");
-            final AxolotlService axolotlService = account.getAxolotlService();
-            axolotlService.registerDevices(from, deviceIds);
+            final var deviceList = items.getFirstItem(DeviceList.class);
+            if (deviceList != null) {
+                final Set<Integer> deviceIds = deviceList.getDeviceIds();
+                Log.d(
+                        Config.LOGTAG,
+                        AxolotlService.getLogprefix(account)
+                                + "Received PEP device list "
+                                + deviceIds
+                                + " update from "
+                                + from
+                                + ", processing... ");
+                final AxolotlService axolotlService = account.getAxolotlService();
+                axolotlService.registerDevices(from, new HashSet<>(deviceIds));
+            }
+
         } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) {
             final var connection = account.getXmppConnection();
             if (connection.getFeatures().bookmarksConversion()) {
@@ -302,9 +317,7 @@ public class MessageParser extends AbstractParser
                                     + ": received storage:bookmark notification even though we"
                                     + " opted into bookmarks:1");
                 }
-                final Element i = items.findChild("item");
-                final Element storage =
-                        i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
+                final var storage = items.getFirstItem(Storage.class);
                 final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
                 mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
                 Log.d(
@@ -318,17 +331,19 @@ public class MessageParser extends AbstractParser
                                 + " not detected");
             }
         } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
-            final Element item = items.findChild("item");
-            final Element retract = items.findChild("retract");
-            if (item != null) {
-                final Bookmark bookmark = Bookmark.parseFromItem(item, account);
-                if (bookmark != null) {
-                    account.putBookmark(bookmark);
-                    mXmppConnectionService.processModifiedBookmark(bookmark);
-                    mXmppConnectionService.updateConversationUi();
+            final var retractions = items.getRetractions();
+            ;
+            for (final var item : items.getItemMap(Conference.class).entrySet()) {
+                final Bookmark bookmark =
+                        Bookmark.parseFromItem(item.getKey(), item.getValue(), account);
+                if (bookmark == null) {
+                    continue;
                 }
+                account.putBookmark(bookmark);
+                mXmppConnectionService.processModifiedBookmark(bookmark);
+                mXmppConnectionService.updateConversationUi();
             }
-            if (retract != null) {
+            for (final var retract : retractions) {
                 final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id"));
                 if (id != null) {
                     account.removeBookmark(id);
@@ -342,35 +357,37 @@ public class MessageParser extends AbstractParser
         } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION
                 && Namespace.MDS_DISPLAYED.equals(node)
                 && account.getJid().asBareJid().equals(from)) {
-            final Element item = items.findChild("item");
-            mXmppConnectionService.processMdsItem(account, item);
-        } else {
-            Log.d(
-                    Config.LOGTAG,
-                    account.getJid().asBareJid()
-                            + " received pubsub notification for node="
-                            + node);
+            for (final var item :
+                    items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class)
+                            .entrySet()) {
+                mXmppConnectionService.processMdsItem(account, item);
+            }
         }
     }
 
-    private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
-        final Element delete = event.findChild("delete");
-        final String node = delete == null ? null : delete.getAttribute("node");
+    private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) {
+        final String node = delete.getNode();
         if (Namespace.NICK.equals(node)) {
-            Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
             setNick(account, from, null);
         } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
             deleteAllBookmarks(account);
-        } else if (Namespace.AVATAR_METADATA.equals(node)
-                && account.getJid().asBareJid().equals(from)) {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node");
+        } else if (Namespace.AVATAR_METADATA.equals(node)) {
+            final boolean isAccount = account.getJid().asBareJid().equals(from);
+            if (isAccount) {
+                account.setAvatar(null);
+                mXmppConnectionService.databaseBackend.updateAccount(account);
+                mXmppConnectionService.getAvatarService().clear(account);
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid() + ": deleted avatar metadata node");
+            }
         }
     }
 
-    private void parsePurgeEvent(final Element event, final Jid from, final Account account) {
-        final Element purge = event.findChild("purge");
-        final String node = purge == null ? null : purge.getAttribute("node");
+    private void parsePurgeEvent(
+            @NonNull final Purge purge, final Jid from, final Account account) {
+        final String node = purge.getNode();
         if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
             deleteAllBookmarks(account);
@@ -1394,12 +1411,20 @@ public class MessageParser extends AbstractParser
 
         final var event = original.getExtension(Event.class);
         if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) {
-            if (event.hasChild("items")) {
-                parseEvent(event, original.getFrom(), account);
-            } else if (event.hasChild("delete")) {
-                parseDeleteEvent(event, original.getFrom(), account);
-            } else if (event.hasChild("purge")) {
-                parsePurgeEvent(event, original.getFrom(), account);
+            final var action = event.getAction();
+            final var node = action == null ? null : action.getNode();
+            if (node == null) {
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": no node found in PubSub event from "
+                                + original.getFrom());
+            } else if (action instanceof Items items) {
+                parseEvent(items, original.getFrom(), account);
+            } else if (action instanceof Purge purge) {
+                parsePurgeEvent(purge, original.getFrom(), account);
+            } else if (action instanceof Delete delete) {
+                parseDeleteEvent(delete, from, account);
             }
         }
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -137,7 +137,12 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 import eu.siacs.conversations.xmpp.mam.MamReference;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
+import im.conversations.android.xmpp.model.avatar.Metadata;
+import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.mds.Displayed;
+import im.conversations.android.xmpp.model.pubsub.PubSub;
 import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.storage.PrivateStorage;
 import java.io.File;
 import java.security.Security;
 import java.security.cert.CertificateException;
@@ -2093,14 +2098,17 @@ public class XmppConnectionService extends Service {
 
     public void fetchBookmarks(final Account account) {
         final Iq iqPacket = new Iq(Iq.Type.GET);
-        final Element query = iqPacket.query("jabber:iq:private");
-        query.addChild("storage", Namespace.BOOKMARKS);
+        iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage());
         final Consumer<Iq> callback =
                 (response) -> {
                     if (response.getType() == Iq.Type.RESULT) {
-                        final Element query1 = response.query();
-                        final Element storage = query1.findChild("storage", "storage:bookmarks");
-                        Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
+                        final var privateStorage = response.getExtension(PrivateStorage.class);
+                        if (privateStorage == null) {
+                            return;
+                        }
+                        final var bookmarkStorage = privateStorage.getExtension(Storage.class);
+                        Map<Jid, Bookmark> bookmarks =
+                                Bookmark.parseFromStorage(bookmarkStorage, account);
                         processBookmarksInitial(account, bookmarks, false);
                     } else {
                         Log.d(
@@ -2118,7 +2126,7 @@ public class XmppConnectionService extends Service {
                 retrieve,
                 (response) -> {
                     if (response.getType() == Iq.Type.RESULT) {
-                        final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
+                        final var pubsub = response.getExtension(PubSub.class);
                         final Map<Jid, Bookmark> bookmarks =
                                 Bookmark.parseFromPubSub(pubsub, account);
                         processBookmarksInitial(account, bookmarks, true);
@@ -2136,30 +2144,34 @@ public class XmppConnectionService extends Service {
                     if (response.getType() != Iq.Type.RESULT) {
                         return;
                     }
-                    final var pubSub = response.findChild("pubsub", Namespace.PUBSUB);
-                    final Element items = pubSub == null ? null : pubSub.findChild("items");
-                    if (items == null
-                            || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) {
+                    final var pubsub = response.getExtension(PubSub.class);
+                    if (pubsub == null) {
                         return;
                     }
-                    for (final Element child : items.getChildren()) {
-                        if ("item".equals(child.getName())) {
-                            processMdsItem(account, child);
+                    final var items = pubsub.getItems();
+                    if (items == null) {
+                        return;
+                    }
+                    if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
+                        for (final var item :
+                                items.getItemMap(
+                                                im.conversations.android.xmpp.model.mds.Displayed
+                                                        .class)
+                                        .entrySet()) {
+                            processMdsItem(account, item);
                         }
                     }
                 });
     }
 
-    public void processMdsItem(final Account account, final Element item) {
-        final Jid jid =
-                item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id"));
+    public void processMdsItem(final Account account, final Map.Entry<String, Displayed> item) {
+        final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
         if (jid == null) {
             return;
         }
-        final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED);
-        final Element stanzaId =
-                displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS);
-        final String id = stanzaId == null ? null : stanzaId.getAttribute("id");
+        final var displayed = item.getValue();
+        final var stanzaId = displayed.getStanzaId();
+        final String id = stanzaId == null ? null : stanzaId.getId();
         final Conversation conversation = find(account, jid);
         if (id != null && conversation != null) {
             conversation.setDisplayState(id);
@@ -4920,16 +4932,20 @@ public class XmppConnectionService extends Service {
                 packet,
                 new Consumer<Iq>() {
 
-                    private Avatar parseAvatar(Iq packet) {
-                        Element pubsub =
-                                packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
-                        if (pubsub != null) {
-                            Element items = pubsub.findChild("items");
-                            if (items != null) {
-                                return Avatar.parseMetadata(items);
-                            }
+                    private Avatar parseAvatar(final Iq packet) {
+                        final var pubsub = packet.getExtension(PubSub.class);
+                        if (pubsub == null) {
+                            return null;
+                        }
+                        final var items = pubsub.getItems();
+                        if (items == null) {
+                            return null;
                         }
-                        return null;
+                        final var item = items.getFirstItemWithId(Metadata.class);
+                        if (item == null) {
+                            return null;
+                        }
+                        return Avatar.parseMetadata(item.getKey(), item.getValue());
                     }
 
                     private boolean errorIsItemNotFound(Iq packet) {
@@ -5164,30 +5180,39 @@ public class XmppConnectionService extends Service {
                 account,
                 packet,
                 response -> {
-                    if (response.getType() == Iq.Type.RESULT) {
-                        Element pubsub =
-                                response.findChild("pubsub", "http://jabber.org/protocol/pubsub");
-                        if (pubsub != null) {
-                            Element items = pubsub.findChild("items");
-                            if (items != null) {
-                                Avatar avatar = Avatar.parseMetadata(items);
-                                if (avatar != null) {
-                                    avatar.owner = account.getJid().asBareJid();
-                                    if (fileBackend.isAvatarCached(avatar)) {
-                                        if (account.setAvatar(avatar.getFilename())) {
-                                            databaseBackend.updateAccount(account);
-                                        }
-                                        getAvatarService().clear(account);
-                                        callback.success(avatar);
-                                    } else {
-                                        fetchAvatarPep(account, avatar, callback);
-                                    }
-                                    return;
-                                }
-                            }
+                    if (response.getType() != Iq.Type.RESULT) {
+                        callback.error(0, null);
+                    }
+                    final var pubsub = packet.getExtension(PubSub.class);
+                    if (pubsub == null) {
+                        callback.error(0, null);
+                        return;
+                    }
+                    final var items = pubsub.getItems();
+                    if (items == null) {
+                        callback.error(0, null);
+                        return;
+                    }
+                    final var item = items.getFirstItemWithId(Metadata.class);
+                    if (item == null) {
+                        callback.error(0, null);
+                        return;
+                    }
+                    final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
+                    if (avatar == null) {
+                        callback.error(0, null);
+                        return;
+                    }
+                    avatar.owner = account.getJid().asBareJid();
+                    if (fileBackend.isAvatarCached(avatar)) {
+                        if (account.setAvatar(avatar.getFilename())) {
+                            databaseBackend.updateAccount(account);
                         }
+                        getAvatarService().clear(account);
+                        callback.success(avatar);
+                    } else {
+                        fetchAvatarPep(account, avatar, callback);
                     }
-                    callback.error(0, null);
                 });
     }
 

src/main/java/eu/siacs/conversations/xml/Namespace.java πŸ”—

@@ -112,4 +112,5 @@ public final class Namespace {
 
     public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps";
     public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps";
+    public static final String PRIVATE_XML_STORAGE = "jabber:iq:private";
 }

src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java πŸ”—

@@ -1,102 +1,95 @@
 package eu.siacs.conversations.xmpp.pep;
 
 import android.util.Base64;
-
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.avatar.Metadata;
 
 public class Avatar {
 
-	public enum Origin { PEP, VCARD }
+    public enum Origin {
+        PEP,
+        VCARD
+    }
 
     public String type;
-	public String sha1sum;
-	public String image;
-	public int height;
-	public int width;
-	public long size;
-	public Jid owner;
-	public Origin origin = Origin.PEP; //default to maintain compat
+    public String sha1sum;
+    public String image;
+    public int height;
+    public int width;
+    public long size;
+    public Jid owner;
+    public Origin origin = Origin.PEP; // default to maintain compat
 
-	public byte[] getImageAsBytes() {
-		return Base64.decode(image, Base64.DEFAULT);
-	}
+    public byte[] getImageAsBytes() {
+        return Base64.decode(image, Base64.DEFAULT);
+    }
 
-	public String getFilename() {
-		return sha1sum;
-	}
+    public String getFilename() {
+        return sha1sum;
+    }
 
-	public static Avatar parseMetadata(Element items) {
-		Element item = items.findChild("item");
-		if (item == null) {
-			return null;
-		}
-		Element metadata = item.findChild("metadata");
-		if (metadata == null) {
-			return null;
-		}
-		String primaryId = item.getAttribute("id");
-		if (primaryId == null) {
-			return null;
-		}
-		for (Element child : metadata.getChildren()) {
-			if (child.getName().equals("info")
-					&& primaryId.equals(child.getAttribute("id"))) {
-				Avatar avatar = new Avatar();
-				String height = child.getAttribute("height");
-				String width = child.getAttribute("width");
-				String size = child.getAttribute("bytes");
-				try {
-					if (height != null) {
-						avatar.height = Integer.parseInt(height);
-					}
-					if (width != null) {
-						avatar.width = Integer.parseInt(width);
-					}
-					if (size != null) {
-						avatar.size = Long.parseLong(size);
-					}
-				} catch (NumberFormatException e) {
-					return null;
-				}
-				avatar.type = child.getAttribute("type");
-				String hash = child.getAttribute("id");
-				if (!isValidSHA1(hash)) {
-					return null;
-				}
-				avatar.sha1sum = hash;
-				avatar.origin = Origin.PEP;
-				return avatar;
-			}
-		}
-		return null;
-	}
+    public static Avatar parseMetadata(final String primaryId, final Metadata metadata) {
+        if (primaryId == null || metadata == null) {
+            return null;
+        }
+        for (Element child : metadata.getChildren()) {
+            if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) {
+                Avatar avatar = new Avatar();
+                String height = child.getAttribute("height");
+                String width = child.getAttribute("width");
+                String size = child.getAttribute("bytes");
+                try {
+                    if (height != null) {
+                        avatar.height = Integer.parseInt(height);
+                    }
+                    if (width != null) {
+                        avatar.width = Integer.parseInt(width);
+                    }
+                    if (size != null) {
+                        avatar.size = Long.parseLong(size);
+                    }
+                } catch (NumberFormatException e) {
+                    return null;
+                }
+                avatar.type = child.getAttribute("type");
+                String hash = child.getAttribute("id");
+                if (!isValidSHA1(hash)) {
+                    return null;
+                }
+                avatar.sha1sum = hash;
+                avatar.origin = Origin.PEP;
+                return avatar;
+            }
+        }
+        return null;
+    }
 
-	@Override
-	public boolean equals(Object object) {
-		if (object != null && object instanceof Avatar) {
-			Avatar other = (Avatar) object;
-			return other.getFilename().equals(this.getFilename());
-		} else {
-			return false;
-		}
-	}
+    @Override
+    public boolean equals(Object object) {
+        if (object != null && object instanceof Avatar) {
+            Avatar other = (Avatar) object;
+            return other.getFilename().equals(this.getFilename());
+        } else {
+            return false;
+        }
+    }
 
-	public static Avatar parsePresence(Element x) {
-		String hash = x == null ? null : x.findChildContent("photo");
-		if (hash == null) {
-			return null;
-		}
-		if (!isValidSHA1(hash)) {
-			return null;
-		}
-		Avatar avatar = new Avatar();
-		avatar.sha1sum = hash;
-		avatar.origin = Origin.VCARD;
-		return avatar;
-	}
+    public static Avatar parsePresence(Element x) {
+        String hash = x == null ? null : x.findChildContent("photo");
+        if (hash == null) {
+            return null;
+        }
+        if (!isValidSHA1(hash)) {
+            return null;
+        }
+        Avatar avatar = new Avatar();
+        avatar.sha1sum = hash;
+        avatar.origin = Origin.VCARD;
+        return avatar;
+    }
 
-	private static boolean isValidSHA1(String s) {
-		return s != null && s.matches("[a-fA-F0-9]{40}");
-	}
+    private static boolean isValidSHA1(String s) {
+        return s != null && s.matches("[a-fA-F0-9]{40}");
+    }
 }

src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java πŸ”—

@@ -1,32 +1,10 @@
 package im.conversations.android.xmpp.model.bookmark;
 
-import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;
 
-@XmlElement
 public class Conference extends Extension {
 
     public Conference() {
         super(Conference.class);
     }
-
-    public boolean isAutoJoin() {
-        return this.getAttributeAsBoolean("autojoin");
-    }
-
-    public String getConferenceName() {
-        return this.getAttribute("name");
-    }
-
-    public void setAutoJoin(boolean autoJoin) {
-        setAttribute("autojoin", autoJoin);
-    }
-
-    public Nick getNick() {
-        return this.getExtension(Nick.class);
-    }
-
-    public Extensions getExtensions() {
-        return this.getExtension(Extensions.class);
-    }
 }

src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java πŸ”—

@@ -0,0 +1,32 @@
+package im.conversations.android.xmpp.model.bookmark2;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Conference extends Extension {
+
+    public Conference() {
+        super(Conference.class);
+    }
+
+    public boolean isAutoJoin() {
+        return this.getAttributeAsBoolean("autojoin");
+    }
+
+    public String getConferenceName() {
+        return this.getAttribute("name");
+    }
+
+    public void setAutoJoin(boolean autoJoin) {
+        setAttribute("autojoin", autoJoin);
+    }
+
+    public Nick getNick() {
+        return this.getExtension(Nick.class);
+    }
+
+    public Extensions getExtensions() {
+        return this.getExtension(Extensions.class);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java β†’ src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java πŸ”—

@@ -1,4 +1,4 @@
-package im.conversations.android.xmpp.model.bookmark;
+package im.conversations.android.xmpp.model.bookmark2;
 
 import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;

src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java πŸ”—

@@ -3,10 +3,15 @@ package im.conversations.android.xmpp.model.mds;
 import eu.siacs.conversations.xml.Namespace;
 import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.unique.StanzaId;
 
 @XmlElement(namespace = Namespace.MDS_DISPLAYED)
 public class Displayed extends Extension {
     public Displayed() {
         super(Displayed.class);
     }
+
+    public StanzaId getStanzaId() {
+        return this.getOnlyExtension(StanzaId.class);
+    }
 }

src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java πŸ”—

@@ -45,6 +45,11 @@ public interface Items {
         return Iterables.getFirst(map.values(), null);
     }
 
+    default <T extends Extension> Map.Entry<String, T> getFirstItemWithId(final Class<T> clazz) {
+        final var entries = getItemMap(clazz).entrySet();
+        return Iterables.getFirst(entries, null);
+    }
+
     default <T extends Extension> T getOnlyItem(final Class<T> clazz) {
         final var map = getItemMap(clazz);
         return Iterables.getOnlyElement(map.values());

src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java πŸ”—

@@ -0,0 +1,14 @@
+package im.conversations.android.xmpp.model.pubsub.event;
+
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class Action extends Extension {
+
+    public Action(Class<? extends Action> clazz) {
+        super(clazz);
+    }
+
+    public String getNode() {
+        return this.getAttribute("node");
+    }
+}

src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java πŸ”—

@@ -12,25 +12,17 @@ public class Event extends Extension {
         super(Event.class);
     }
 
-    public Items getItems() {
-        return this.getExtension(ItemsWrapper.class);
-    }
-
-    public Purge getPurge() {
-        return this.getExtension(Purge.class);
+    public Action getAction() {
+        return this.getOnlyExtension(Action.class);
     }
 
     @XmlElement(name = "items")
-    public static class ItemsWrapper extends Extension implements Items {
+    public static class ItemsWrapper extends Action implements Items {
 
         public ItemsWrapper() {
             super(ItemsWrapper.class);
         }
 
-        public String getNode() {
-            return this.getAttribute("node");
-        }
-
         public Collection<? extends im.conversations.android.xmpp.model.pubsub.Item> getItems() {
             return this.getExtensions(Item.class);
         }

src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java πŸ”—

@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.storage;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "query", namespace = Namespace.PRIVATE_XML_STORAGE)
+public class PrivateStorage extends Extension {
+
+    public PrivateStorage() {
+        super(PrivateStorage.class);
+    }
+}