do roster and blocklist modifications via managers

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/entities/Blockable.java             |  16 
src/main/java/eu/siacs/conversations/entities/Contact.java               |  15 
src/main/java/eu/siacs/conversations/entities/Conversation.java          |   1 
src/main/java/eu/siacs/conversations/entities/RawBlockable.java          |   4 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |  25 
src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java    |  47 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java          |   6 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java | 126 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java      |  34 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  12 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java   |   2 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                |  14 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java            |   2 
src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java   |  85 
src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java   |  41 
src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java     | 103 
src/main/java/im/conversations/android/xmpp/model/blocking/Item.java     |   4 
src/main/java/im/conversations/android/xmpp/model/nick/Nick.java         |   5 
src/main/java/im/conversations/android/xmpp/model/reporting/Report.java  |  16 
src/main/java/im/conversations/android/xmpp/model/roster/Group.java      |   5 
src/main/java/im/conversations/android/xmpp/model/roster/Item.java       |  25 
src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java   |  39 
src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java   |   5 
src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java |   2 
24 files changed, 403 insertions(+), 231 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Blockable.java 🔗

@@ -1,11 +1,17 @@
 package eu.siacs.conversations.entities;
 
+import androidx.annotation.NonNull;
 import eu.siacs.conversations.xmpp.Jid;
 
 public interface Blockable {
-	boolean isBlocked();
-	boolean isDomainBlocked();
-	Jid getBlockedJid();
-	Jid getJid();
-	Account getAccount();
+    boolean isBlocked();
+
+    boolean isDomainBlocked();
+
+    @NonNull
+    Jid getBlockedJid();
+
+    Jid getJid();
+
+    Account getAccount();
 }

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -316,7 +316,7 @@ public class Contact implements ListItem, Blockable {
         this.systemAccount = lookupUri;
     }
 
-    private Collection<String> getGroups(final boolean unique) {
+    public Collection<String> getGroups(final boolean unique) {
         final Collection<String> groups = unique ? new HashSet<>() : new ArrayList<>();
         for (int i = 0; i < this.groups.length(); ++i) {
             try {
@@ -428,18 +428,6 @@ public class Contact implements ListItem, Blockable {
         }
     }
 
-    public Element asElement() {
-        final Element item = new Element("item");
-        item.setAttribute("jid", this.jid);
-        if (this.serverName != null) {
-            item.setAttribute("name", this.serverName);
-        }
-        for (String group : getGroups(false)) {
-            item.addChild("group").setContent(group);
-        }
-        return item;
-    }
-
     @Override
     public int compareTo(@NonNull final ListItem another) {
         return this.getDisplayName().compareToIgnoreCase(another.getDisplayName());
@@ -490,6 +478,7 @@ public class Contact implements ListItem, Blockable {
     }
 
     @Override
+    @NonNull
     public Jid getBlockedJid() {
         if (isDomainBlocked()) {
             return getJid().getDomain();

src/main/java/eu/siacs/conversations/entities/RawBlockable.java 🔗

@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
 
 import android.content.Context;
 import android.text.TextUtils;
+import androidx.annotation.NonNull;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.xmpp.Jid;
 import java.util.Collections;
@@ -13,7 +14,7 @@ public class RawBlockable implements ListItem, Blockable {
     private final Account account;
     private final Jid jid;
 
-    public RawBlockable(Account account, Jid jid) {
+    public RawBlockable(@NonNull Account account, @NonNull Jid jid) {
         this.account = account;
         this.jid = jid;
     }
@@ -29,6 +30,7 @@ public class RawBlockable implements ListItem, Blockable {
     }
 
     @Override
+    @NonNull
     public Jid getBlockedJid() {
         return this.jid;
     }

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -316,31 +316,6 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public Iq generateSetBlockRequest(
-            final Jid jid, final boolean reportSpam, final String serverMsgId) {
-        final Iq iq = new Iq(Iq.Type.SET);
-        final Element block = iq.addChild("block", Namespace.BLOCKING);
-        final Element item = block.addChild("item").setAttribute("jid", jid);
-        if (reportSpam) {
-            final Element report = item.addChild("report", Namespace.REPORTING);
-            report.setAttribute("reason", Namespace.REPORTING_REASON_SPAM);
-            if (serverMsgId != null) {
-                final Element stanzaId = report.addChild("stanza-id", Namespace.STANZA_IDS);
-                stanzaId.setAttribute("by", jid);
-                stanzaId.setAttribute("id", serverMsgId);
-            }
-        }
-        Log.d(Config.LOGTAG, iq.toString());
-        return iq;
-    }
-
-    public Iq generateSetUnblockRequest(final Jid jid) {
-        final Iq iq = new Iq(Iq.Type.SET);
-        final Element block = iq.addChild("unblock", Namespace.BLOCKING);
-        block.addChild("item").setAttribute("jid", jid);
-        return iq;
-    }
-
     public Iq generateSetPassword(final Account account, final String newPassword) {
         final Iq packet = new Iq(Iq.Type.SET);
         packet.setTo(account.getDomain());

src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java 🔗

@@ -1,11 +1,8 @@
 package eu.siacs.conversations.generator;
 
-import android.text.TextUtils;
 import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import im.conversations.android.xmpp.model.stanza.Presence;
 
@@ -15,50 +12,6 @@ public class PresenceGenerator extends AbstractGenerator {
         super(service);
     }
 
-    private im.conversations.android.xmpp.model.stanza.Presence subscription(
-            String type, Contact contact) {
-        im.conversations.android.xmpp.model.stanza.Presence packet =
-                new im.conversations.android.xmpp.model.stanza.Presence();
-        packet.setAttribute("type", type);
-        packet.setTo(contact.getJid());
-        packet.setFrom(contact.getAccount().getJid().asBareJid());
-        return packet;
-    }
-
-    public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
-            final Contact contact) {
-        return requestPresenceUpdatesFrom(contact, null);
-    }
-
-    public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
-            final Contact contact, final String preAuth) {
-        im.conversations.android.xmpp.model.stanza.Presence packet =
-                subscription("subscribe", contact);
-        String displayName = contact.getAccount().getDisplayName();
-        if (!TextUtils.isEmpty(displayName)) {
-            packet.addChild("nick", Namespace.NICK).setContent(displayName);
-        }
-        if (preAuth != null) {
-            packet.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth);
-        }
-        return packet;
-    }
-
-    public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(
-            Contact contact) {
-        return subscription("unsubscribe", contact);
-    }
-
-    public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(
-            Contact contact) {
-        return subscription("unsubscribed", contact);
-    }
-
-    public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(
-            Contact contact) {
-        return subscription("subscribed", contact);
-    }
-
     public im.conversations.android.xmpp.model.stanza.Presence selfPresence(
             Account account, Presence.Availability status) {
         return selfPresence(account, status, true);

src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -24,6 +24,7 @@ import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import eu.siacs.conversations.xmpp.manager.RosterManager;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import im.conversations.android.xmpp.Entity;
@@ -445,8 +446,9 @@ public class PresenceParser extends AbstractParser
                 mXmppConnectionService.getAvatarService().clear(contact);
             }
             if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
-                mXmppConnectionService.sendPresencePacket(
-                        account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
+                connection
+                        .getManager(PresenceManager.class)
+                        .subscribed(contact.getJid().asBareJid());
             } else {
                 contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
                 final Conversation conversation =

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -137,7 +137,9 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 import eu.siacs.conversations.xmpp.mam.MamReference;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
 import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import eu.siacs.conversations.xmpp.manager.RosterManager;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xmpp.pep.PublishOptions;
@@ -1902,7 +1904,7 @@ public class XmppConnectionService extends Service {
                                 + ": adding "
                                 + contact.getJid()
                                 + " on sending message");
-                createContact(contact, true);
+                createContact(contact);
             }
         }
 
@@ -3022,10 +3024,13 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void stopPresenceUpdatesTo(Contact contact) {
+    public void stopPresenceUpdatesTo(final Contact contact) {
         Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
-        sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
         contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
+        contact.getAccount()
+                .getXmppConnection()
+                .getManager(PresenceManager.class)
+                .unsubscribed(contact.getJid().asBareJid());
     }
 
     public void createAccount(final Account account) {
@@ -4690,57 +4695,20 @@ public class XmppConnectionService extends Service {
         updateConversationUi();
     }
 
-    // TODO move this to RosterManager
-    public void syncDirtyContacts(Account account) {
-        for (Contact contact : account.getRoster().getContacts()) {
-            if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
-                pushContactToServer(contact);
-            }
-            if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
-                deleteContactOnServer(contact);
-            }
-        }
+    public void createContact(final Contact contact) {
+        createContact(contact, null);
     }
 
-    public void createContact(final Contact contact, final boolean autoGrant) {
-        createContact(contact, autoGrant, null);
+    public void createContact(final Contact contact, final String preAuth) {
+        contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
+        contact.setOption(Contact.Options.ASKING);
+        final var connection = contact.getAccount().getXmppConnection();
+        connection.getManager(RosterManager.class).addRosterItem(contact, preAuth);
     }
 
-    public void createContact(
-            final Contact contact, final boolean autoGrant, final String preAuth) {
-        if (autoGrant) {
-            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
-            contact.setOption(Contact.Options.ASKING);
-        }
-        pushContactToServer(contact, preAuth);
-    }
-
-    public void pushContactToServer(final Contact contact) {
-        pushContactToServer(contact, null);
-    }
-
-    private void pushContactToServer(final Contact contact, final String preAuth) {
-        contact.resetOption(Contact.Options.DIRTY_DELETE);
-        contact.setOption(Contact.Options.DIRTY_PUSH);
-        final Account account = contact.getAccount();
-        if (account.getStatus() == Account.State.ONLINE) {
-            final boolean ask = contact.getOption(Contact.Options.ASKING);
-            final boolean sendUpdates =
-                    contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
-                            && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
-            final Iq iq = new Iq(Iq.Type.SET);
-            iq.query(Namespace.ROSTER).addChild(contact.asElement());
-            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
-            if (sendUpdates) {
-                sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
-            }
-            if (ask) {
-                sendPresencePacket(
-                        account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
-            }
-        } else {
-            account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
-        }
+    public void deleteContactOnServer(final Contact contact) {
+        final var connection = contact.getAccount().getXmppConnection();
+        connection.getManager(RosterManager.class).deleteRosterItem(contact);
     }
 
     public void publishMucAvatar(
@@ -5312,20 +5280,6 @@ public class XmppConnectionService extends Service {
         }
     }
 
-    public void deleteContactOnServer(Contact contact) {
-        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
-        contact.resetOption(Contact.Options.DIRTY_PUSH);
-        contact.setOption(Contact.Options.DIRTY_DELETE);
-        Account account = contact.getAccount();
-        if (account.getStatus() == Account.State.ONLINE) {
-            final Iq iq = new Iq(Iq.Type.SET);
-            Element item = iq.query(Namespace.ROSTER).addChild("item");
-            item.setAttribute("jid", contact.getJid());
-            item.setAttribute("subscription", "remove");
-            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
-        }
-    }
-
     public void updateConversation(final Conversation conversation) {
         mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
     }
@@ -6145,29 +6099,11 @@ public class XmppConnectionService extends Service {
 
     public boolean sendBlockRequest(
             final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
-        if (blockable != null && blockable.getBlockedJid() != null) {
-            final var account = blockable.getAccount();
-            final Jid jid = blockable.getBlockedJid();
-            this.sendIqPacket(
-                    account,
-                    getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
-                    (response) -> {
-                        if (response.getType() == Iq.Type.RESULT) {
-                            account.getBlocklist().add(jid);
-                            updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
-                        }
-                    });
-            if (blockable.getBlockedJid().isFullJid()) {
-                return false;
-            } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
-                updateConversationUi();
-                return true;
-            } else {
-                return false;
-            }
-        } else {
-            return false;
-        }
+        final var account = blockable.getAccount();
+        final var connection = account.getXmppConnection();
+        return connection
+                .getManager(BlockingManager.class)
+                .block(blockable, reportSpam, serverMsgId);
     }
 
     public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
@@ -6202,19 +6138,9 @@ public class XmppConnectionService extends Service {
     }
 
     public void sendUnblockRequest(final Blockable blockable) {
-        if (blockable != null && blockable.getJid() != null) {
-            final var account = blockable.getAccount();
-            final Jid jid = blockable.getBlockedJid();
-            this.sendIqPacket(
-                    account,
-                    getIqGenerator().generateSetUnblockRequest(jid),
-                    response -> {
-                        if (response.getType() == Iq.Type.RESULT) {
-                            account.getBlocklist().remove(jid);
-                            updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
-                        }
-                    });
-        }
+        final var account = blockable.getAccount();
+        final var connection = account.getXmppConnection();
+        connection.getManager(BlockingManager.class).unblock(blockable);
     }
 
     public void publishDisplayName(final Account account) {

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -70,6 +70,8 @@ import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
 import im.conversations.android.xmpp.model.stanza.Presence;
 import java.util.Collection;
 import java.util.Collections;
@@ -109,11 +111,10 @@ public class ContactDetailsActivity extends OmemoActivity
                         }
                     } else {
                         contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
-                        xmppConnectionService.sendPresencePacket(
-                                contact.getAccount(),
-                                xmppConnectionService
-                                        .getPresenceGenerator()
-                                        .stopPresenceUpdatesTo(contact));
+                        final var connection = contact.getAccount().getXmppConnection();
+                        connection
+                                .getManager(PresenceManager.class)
+                                .unsubscribed(contact.getJid().asBareJid());
                     }
                 }
             };
@@ -122,18 +123,15 @@ public class ContactDetailsActivity extends OmemoActivity
 
                 @Override
                 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+                    final var connection = contact.getAccount().getXmppConnection();
                     if (isChecked) {
-                        xmppConnectionService.sendPresencePacket(
-                                contact.getAccount(),
-                                xmppConnectionService
-                                        .getPresenceGenerator()
-                                        .requestPresenceUpdatesFrom(contact));
+                        connection
+                                .getManager(PresenceManager.class)
+                                .subscribe(contact.getJid().asBareJid());
                     } else {
-                        xmppConnectionService.sendPresencePacket(
-                                contact.getAccount(),
-                                xmppConnectionService
-                                        .getPresenceGenerator()
-                                        .stopPresenceUpdatesFrom(contact));
+                        connection
+                                .getManager(PresenceManager.class)
+                                .unsubscribe(contact.getJid().asBareJid());
                     }
                 }
             };
@@ -343,8 +341,10 @@ public class ContactDetailsActivity extends OmemoActivity
                             R.string.contact_name,
                             value -> {
                                 contact.setServerName(value);
-                                ContactDetailsActivity.this.xmppConnectionService
-                                        .pushContactToServer(contact);
+                                final var connection = contact.getAccount().getXmppConnection();
+                                connection
+                                        .getManager(RosterManager.class)
+                                        .addRosterItem(contact, null);
                                 populateView();
                                 return null;
                             },

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -127,6 +127,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection;
 import eu.siacs.conversations.xmpp.jingle.Media;
 import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
 import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import im.conversations.android.xmpp.model.stanza.Presence;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -446,7 +447,7 @@ public class ConversationFragment extends XmppFragment
                 public void onClick(View v) {
                     final Contact contact = conversation == null ? null : conversation.getContact();
                     if (contact != null) {
-                        activity.xmppConnectionService.createContact(contact, true);
+                        activity.xmppConnectionService.createContact(contact);
                         activity.switchToContactDetails(contact);
                     }
                 }
@@ -458,11 +459,10 @@ public class ConversationFragment extends XmppFragment
                 public void onClick(View v) {
                     final Contact contact = conversation == null ? null : conversation.getContact();
                     if (contact != null) {
-                        activity.xmppConnectionService.sendPresencePacket(
-                                contact.getAccount(),
-                                activity.xmppConnectionService
-                                        .getPresenceGenerator()
-                                        .sendPresenceUpdatesTo(contact));
+                        final var connection = contact.getAccount().getXmppConnection();
+                        connection
+                                .getManager(PresenceManager.class)
+                                .subscribed(contact.getJid().asBareJid());
                         hideSnackbar();
                     }
                 }

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -647,7 +647,7 @@ public class StartConversationActivity extends XmppActivity
                                 invite == null
                                         ? null
                                         : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
-                        xmppConnectionService.createContact(contact, true, preAuth);
+                        xmppConnectionService.createContact(contact, preAuth);
                         if (invite != null && invite.hasFingerprints()) {
                             xmppConnectionService.verifyFingerprints(
                                     contact, invite.getFingerprints());

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -85,6 +85,7 @@ import eu.siacs.conversations.utils.SignupUtils;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
@@ -894,7 +895,7 @@ public abstract class XmppActivity extends ActionBarActivity {
         builder.setNegativeButton(getString(R.string.cancel), null);
         builder.setPositiveButton(
                 getString(R.string.add_contact),
-                (dialog, which) -> xmppConnectionService.createContact(contact, true));
+                (dialog, which) -> xmppConnectionService.createContact(contact));
         builder.create().show();
     }
 
@@ -906,13 +907,10 @@ public abstract class XmppActivity extends ActionBarActivity {
         builder.setPositiveButton(
                 R.string.request_now,
                 (dialog, which) -> {
-                    if (xmppConnectionServiceBound) {
-                        xmppConnectionService.sendPresencePacket(
-                                contact.getAccount(),
-                                xmppConnectionService
-                                        .getPresenceGenerator()
-                                        .requestPresenceUpdatesFrom(contact));
-                    }
+                    final var connection = contact.getAccount().getXmppConnection();
+                    connection
+                            .getManager(PresenceManager.class)
+                            .subscribe(contact.getJid().asBareJid());
                 });
         builder.create().show();
     }

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -3071,7 +3071,7 @@ public class XmppConnection implements Runnable {
         }
 
         public boolean blocking() {
-            return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
+            return connection.getManager(BlockingManager.class).hasFeature();
         }
 
         public boolean spamReporting() {

src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java 🔗

@@ -2,11 +2,13 @@ package eu.siacs.conversations.xmpp.manager;
 
 import android.util.Log;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.MoreExecutors;
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Blockable;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
@@ -18,7 +20,9 @@ import im.conversations.android.xmpp.model.blocking.Item;
 import im.conversations.android.xmpp.model.blocking.Unblock;
 import im.conversations.android.xmpp.model.error.Condition;
 import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.reporting.Report;
 import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.unique.StanzaId;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
@@ -141,4 +145,85 @@ public class BlockingManager extends AbstractManager {
         }
         return builder.build();
     }
+
+    public boolean block(
+            @NonNull final Blockable blockable,
+            final boolean reportSpam,
+            @Nullable final String serverMsgId) {
+        final var address = blockable.getBlockedJid();
+        final var iq = new Iq(Iq.Type.SET);
+        final var block = iq.addExtension(new Block());
+        final var item = block.addExtension(new Item());
+        item.setJid(address);
+        if (reportSpam) {
+            final var report = item.addExtension(new Report());
+            report.setReason(Namespace.REPORTING_REASON_SPAM);
+            if (serverMsgId != null) {
+                // XEP has a 'by' attribute that is the same as reported jid but that doesn't make
+                // sense this the 'by' attribute in the stanza-id refers to the arriving entity
+                // (usually the account or the MUC)
+                report.addExtension(new StanzaId(serverMsgId));
+            }
+        }
+        final var future = this.connection.sendIqPacket(iq);
+        Futures.addCallback(
+                future,
+                new FutureCallback<>() {
+                    @Override
+                    public void onSuccess(Iq result) {
+                        synchronized (blocklist) {
+                            blocklist.add(address);
+                        }
+                        service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable throwable) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getAccount().getJid().asBareJid() + ": could not block " + address,
+                                throwable);
+                    }
+                },
+                MoreExecutors.directExecutor());
+        if (address.isFullJid()) {
+            return false;
+        } else if (service.removeBlockedConversations(getAccount(), address)) {
+            service.updateConversationUi();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void unblock(@NonNull final Blockable blockable) {
+        final var address = blockable.getBlockedJid();
+        final var iq = new Iq(Iq.Type.SET);
+        final var unblock = iq.addExtension(new Unblock());
+        final var item = unblock.addExtension(new Item());
+        item.setJid(address);
+        final var future = this.connection.sendIqPacket(iq);
+        Futures.addCallback(
+                future,
+                new FutureCallback<Iq>() {
+                    @Override
+                    public void onSuccess(Iq result) {
+                        synchronized (blocklist) {
+                            blocklist.remove(address);
+                        }
+                        service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getAccount().getJid().asBareJid()
+                                        + ": could not unblock "
+                                        + address,
+                                t);
+                    }
+                },
+                MoreExecutors.directExecutor());
+    }
 }

src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java 🔗

@@ -1,12 +1,16 @@
 package eu.siacs.conversations.xmpp.manager;
 
 import android.content.Context;
+import com.google.common.base.Strings;
+import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.XmppConnection;
 import im.conversations.android.xmpp.EntityCapabilities;
 import im.conversations.android.xmpp.EntityCapabilities2;
 import im.conversations.android.xmpp.ServiceDescription;
 import im.conversations.android.xmpp.model.capabilties.Capabilities;
 import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities;
+import im.conversations.android.xmpp.model.nick.Nick;
+import im.conversations.android.xmpp.model.pars.PreAuth;
 import im.conversations.android.xmpp.model.pgp.Signed;
 import im.conversations.android.xmpp.model.stanza.Presence;
 import java.util.HashMap;
@@ -21,6 +25,43 @@ public class PresenceManager extends AbstractManager {
         super(context, connection);
     }
 
+    public void subscribe(final Jid address) {
+        subscribe(address, null);
+    }
+
+    public void subscribe(final Jid address, final String preAuth) {
+
+        var presence = new Presence(Presence.Type.SUBSCRIBE);
+        presence.setTo(address);
+
+        final var displayName = getAccount().getDisplayName();
+        if (!Strings.isNullOrEmpty(displayName)) {
+            presence.addExtension(new Nick(displayName));
+        }
+        if (preAuth != null) {
+            presence.addExtension(new PreAuth()).setToken(preAuth);
+        }
+        this.connection.sendPresencePacket(presence);
+    }
+
+    public void unsubscribe(final Jid address) {
+        var presence = new Presence(Presence.Type.UNSUBSCRIBE);
+        presence.setTo(address);
+        this.connection.sendPresencePacket(presence);
+    }
+
+    public void unsubscribed(final Jid address) {
+        var presence = new Presence(Presence.Type.UNSUBSCRIBED);
+        presence.setTo(address);
+        this.connection.sendPresencePacket(presence);
+    }
+
+    public void subscribed(final Jid address) {
+        var presence = new Presence(Presence.Type.SUBSCRIBED);
+        presence.setTo(address);
+        this.connection.sendPresencePacket(presence);
+    }
+
     public Presence getPresence(final Presence.Availability availability, final boolean personal) {
         final var account = connection.getAccount();
         final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription();

src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java 🔗

@@ -212,7 +212,7 @@ public class RosterManager extends AbstractManager implements Roster {
     public Contact getContactFromContactList(@NonNull final Jid jid) {
         synchronized (this.contacts) {
             final var contact =
-                    Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()));
+                    Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()), null);
             if (contact != null && contact.showInContactList()) {
                 return contact;
             } else {
@@ -273,4 +273,105 @@ public class RosterManager extends AbstractManager implements Roster {
         }
         getDatabase().writeRoster(account, version, contacts);
     }
+
+    public void syncDirtyContacts() {
+        synchronized (this.contacts) {
+            for (final var contact : this.contacts) {
+                if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
+                    addRosterItem(contact, null);
+                }
+                if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
+                    deleteRosterItem(contact);
+                }
+            }
+        }
+    }
+
+    public void addRosterItem(final Contact contact, final String preAuth) {
+        final var address = contact.getJid().asBareJid();
+        contact.resetOption(Contact.Options.DIRTY_DELETE);
+        contact.setOption(Contact.Options.DIRTY_PUSH);
+        // sync the 'dirty push' flag to disk in case we are offline
+        this.writeToDatabaseAsync();
+        final boolean ask = contact.getOption(Contact.Options.ASKING);
+        final boolean sendUpdates =
+                contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
+                        && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
+        final Iq iq = new Iq(Iq.Type.SET);
+        final var query = iq.addExtension(new Query());
+        final var item = query.addExtension(new Item());
+        item.setJid(address);
+        final var serverName = contact.getServerName();
+        if (serverName != null) {
+            item.setItemName(serverName);
+        }
+        item.setGroups(contact.getGroups(false));
+        final var future = this.connection.sendIqPacket(iq);
+        Futures.addCallback(
+                future,
+                new FutureCallback<Iq>() {
+                    @Override
+                    public void onSuccess(Iq result) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getAccount().getJid().asBareJid()
+                                        + ": pushed roster item "
+                                        + address);
+                    }
+
+                    @Override
+                    public void onFailure(@NonNull Throwable t) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getAccount().getJid().asBareJid()
+                                        + ": could not push roster item "
+                                        + address,
+                                t);
+                    }
+                },
+                MoreExecutors.directExecutor());
+        if (sendUpdates) {
+            getManager(PresenceManager.class).subscribed(contact.getJid().asBareJid());
+        }
+        if (ask) {
+            getManager(PresenceManager.class).subscribe(contact.getJid().asBareJid(), preAuth);
+        }
+    }
+
+    public void deleteRosterItem(final Contact contact) {
+        final var address = contact.getJid().asBareJid();
+        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+        contact.resetOption(Contact.Options.DIRTY_PUSH);
+        contact.setOption(Contact.Options.DIRTY_DELETE);
+        this.writeToDatabaseAsync();
+        final Iq iq = new Iq(Iq.Type.SET);
+        final var query = iq.addExtension(new Query());
+        final var item = query.addExtension(new Item());
+        item.setJid(address);
+        item.setSubscription(Item.Subscription.REMOVE);
+        final var future = this.connection.sendIqPacket(iq);
+        Futures.addCallback(
+                future,
+                new FutureCallback<Iq>() {
+                    @Override
+                    public void onSuccess(final Iq result) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getAccount().getJid().asBareJid()
+                                        + ": removed roster item "
+                                        + address);
+                    }
+
+                    @Override
+                    public void onFailure(final @NonNull Throwable t) {
+                        Log.d(
+                                Config.LOGTAG,
+                                getAccount().getJid().asBareJid()
+                                        + ": could not remove roster item "
+                                        + address,
+                                t);
+                    }
+                },
+                MoreExecutors.directExecutor());
+    }
 }

src/main/java/im/conversations/android/xmpp/model/reporting/Report.java 🔗

@@ -0,0 +1,16 @@
+package im.conversations.android.xmpp.model.reporting;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(namespace = Namespace.REPORTING)
+public class Report extends Extension {
+    public Report() {
+        super(Report.class);
+    }
+
+    public void setReason(final String reason) {
+        this.setAttribute("reason", reason);
+    }
+}

src/main/java/im/conversations/android/xmpp/model/roster/Item.java 🔗

@@ -1,13 +1,10 @@
 package im.conversations.android.xmpp.model.roster;
 
 import com.google.common.collect.Collections2;
-
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.Jid;
-
 import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;
-
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
@@ -28,10 +25,18 @@ public class Item extends Extension {
         return getAttributeAsJid("jid");
     }
 
+    public void setJid(final Jid jid) {
+        this.setAttribute("jid", jid);
+    }
+
     public String getItemName() {
         return this.getAttribute("name");
     }
 
+    public void setItemName(final String serverName) {
+        this.setAttribute("name", serverName);
+    }
+
     public boolean isPendingOut() {
         return "subscribe".equalsIgnoreCase(this.getAttribute("ask"));
     }
@@ -45,12 +50,26 @@ public class Item extends Extension {
         }
     }
 
+    public void setSubscription(final Subscription subscription) {
+        if (subscription == null) {
+            this.removeAttribute("subscription");
+        } else {
+            this.setAttribute("subscription", subscription.toString().toLowerCase(Locale.ROOT));
+        }
+    }
+
     public Collection<String> getGroups() {
         return Collections2.filter(
                 Collections2.transform(getExtensions(Group.class), Element::getContent),
                 Objects::nonNull);
     }
 
+    public void setGroups(final Collection<String> groups) {
+        for (final String group : groups) {
+            this.addExtension(new Group());
+        }
+    }
+
     public enum Subscription {
         NONE,
         TO,

src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java 🔗

@@ -5,6 +5,7 @@ import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
 import im.conversations.android.xmpp.model.jabber.Show;
 import im.conversations.android.xmpp.model.jabber.Status;
+import java.util.Locale;
 
 @XmlElement
 public class Presence extends Stanza implements EntityCapabilities {
@@ -13,6 +14,11 @@ public class Presence extends Stanza implements EntityCapabilities {
         super(Presence.class);
     }
 
+    public Presence(final Type type) {
+        this();
+        this.setType(type);
+    }
+
     public Availability getAvailability() {
         final var show = getExtension(Show.class);
         if (show == null) {
@@ -28,6 +34,18 @@ public class Presence extends Stanza implements EntityCapabilities {
         this.addExtension(new Show()).setContent(availability.toShowString());
     }
 
+    public void setType(final Type type) {
+        if (type == null) {
+            this.removeAttribute("type");
+        } else {
+            this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT));
+        }
+    }
+
+    public Type getType() {
+        return Type.valueOfOrNull(this.getAttribute("type"));
+    }
+
     public void setStatus(final String status) {
         if (Strings.isNullOrEmpty(status)) {
             return;
@@ -40,6 +58,27 @@ public class Presence extends Stanza implements EntityCapabilities {
         return status == null ? null : status.getContent();
     }
 
+    public enum Type {
+        ERROR,
+        PROBE,
+        SUBSCRIBE,
+        SUBSCRIBED,
+        UNAVAILABLE,
+        UNSUBSCRIBE,
+        UNSUBSCRIBED;
+
+        public static Type valueOfOrNull(final String type) {
+            if (Strings.isNullOrEmpty(type)) {
+                return null;
+            }
+            try {
+                return valueOf(type.toUpperCase(Locale.ROOT));
+            } catch (final IllegalArgumentException e) {
+                return null;
+            }
+        }
+    }
+
     public enum Availability {
         CHAT,
         ONLINE,

src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java 🔗

@@ -98,7 +98,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
             service.getPushManagementService().registerPushTokenOnServer(account);
         }
         service.connectMultiModeConversations(account);
-        service.syncDirtyContacts(account);
+        connection.getManager(RosterManager.class).syncDirtyContacts();
 
         service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account);
     }