Detailed changes
@@ -27,15 +27,17 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
import org.json.JSONException;
import org.json.JSONObject;
@@ -77,10 +79,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
public static final String KEY_SOS_URL = "sos_url";
public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
-
protected final JSONObject keys;
- private final Roster roster = new Roster(this);
- private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
@@ -550,11 +549,10 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public void initAccountServices(final XmppConnectionService context) {
+ this.xmppConnection = new XmppConnection(this, context);
this.axolotlService = new AxolotlService(this, context);
this.pgpDecryptionService = new PgpDecryptionService(context);
- if (xmppConnection != null) {
- xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
- }
+ this.xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
}
public PgpDecryptionService getPgpDecryptionService() {
@@ -565,16 +563,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
return this.xmppConnection;
}
- public void setXmppConnection(final XmppConnection connection) {
- this.xmppConnection = connection;
- }
-
public String getRosterVersion() {
- if (this.rosterVersion == null) {
- return "";
- } else {
- return this.rosterVersion;
- }
+ return Strings.emptyToNull(this.rosterVersion);
}
public void setRosterVersion(final String version) {
@@ -648,7 +638,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public Roster getRoster() {
- return this.roster;
+ if (xmppConnection != null) {
+ return xmppConnection.getManager(RosterManager.class);
+ }
+ // TODO either return stub or always put XmppConnection into Account
+ return null;
}
public Collection<Bookmark> getBookmarks() {
@@ -767,20 +761,22 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public boolean isBlocked(final ListItem contact) {
final Jid jid = contact.getJid();
+ final var blocklist = getBlocklist();
return jid != null
&& (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
}
public boolean isBlocked(final Jid jid) {
+ final var blocklist = getBlocklist();
return jid != null && blocklist.contains(jid.asBareJid());
}
- public Collection<Jid> getBlocklist() {
- return this.blocklist;
- }
-
- public void clearBlocklist() {
- getBlocklist().clear();
+ public Set<Jid> getBlocklist() {
+ final var connection = this.xmppConnection;
+ if (connection == null) {
+ return Collections.emptySet();
+ }
+ return connection.getManager(BlockingManager.class).getBlocklist();
}
public boolean isOnlineAndConnected() {
@@ -883,7 +883,9 @@ public class MucOptions {
public Contact getContact() {
if (fullJid != null) {
- return getAccount().getRoster().getContactFromContactList(realJid);
+ return realJid == null
+ ? null
+ : getAccount().getRoster().getContactFromContactList(realJid);
} else if (realJid != null) {
return getAccount().getRoster().getContact(realJid);
} else {
@@ -1,98 +1,17 @@
package eu.siacs.conversations.entities;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-
+import androidx.annotation.NonNull;
import eu.siacs.conversations.android.AbstractPhoneContact;
import eu.siacs.conversations.xmpp.Jid;
+import java.util.List;
+public interface Roster {
-public class Roster {
- private final Account account;
- private final HashMap<Jid, Contact> contacts = new HashMap<>();
- private String version = null;
-
- public Roster(Account account) {
- this.account = account;
- }
-
- public Contact getContactFromContactList(Jid jid) {
- if (jid == null) {
- return null;
- }
- synchronized (this.contacts) {
- Contact contact = contacts.get(jid.asBareJid());
- if (contact != null && contact.showInContactList()) {
- return contact;
- } else {
- return null;
- }
- }
- }
-
- public Contact getContact(final Jid jid) {
- synchronized (this.contacts) {
- if (!contacts.containsKey(jid.asBareJid())) {
- Contact contact = new Contact(jid.asBareJid());
- contact.setAccount(account);
- contacts.put(contact.getJid().asBareJid(), contact);
- return contact;
- }
- return contacts.get(jid.asBareJid());
- }
- }
-
- public void clearPresences() {
- for (Contact contact : getContacts()) {
- contact.clearPresences();
- }
- }
-
- public void markAllAsNotInRoster() {
- for (Contact contact : getContacts()) {
- contact.resetOption(Contact.Options.IN_ROSTER);
- }
- }
-
- public List<Contact> getWithSystemAccounts(Class<?extends AbstractPhoneContact> clazz) {
- int option = Contact.getOption(clazz);
- List<Contact> with = getContacts();
- for(Iterator<Contact> iterator = with.iterator(); iterator.hasNext();) {
- Contact contact = iterator.next();
- if (!contact.getOption(option)) {
- iterator.remove();
- }
- }
- return with;
- }
-
- public List<Contact> getContacts() {
- synchronized (this.contacts) {
- return new ArrayList<>(this.contacts.values());
- }
- }
-
- public void initContact(final Contact contact) {
- if (contact == null) {
- return;
- }
- contact.setAccount(account);
- synchronized (this.contacts) {
- contacts.put(contact.getJid().asBareJid(), contact);
- }
- }
+ List<Contact> getContacts();
- public void setVersion(String version) {
- this.version = version;
- }
+ List<Contact> getWithSystemAccounts(Class<? extends AbstractPhoneContact> clazz);
- public String getVersion() {
- return this.version;
- }
+ Contact getContact(@NonNull final Jid jid);
- public Account getAccount() {
- return this.account;
- }
+ Contact getContactFromContactList(@NonNull final Jid jid);
}
@@ -23,9 +23,7 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
-import java.util.TimeZone;
import java.util.UUID;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
@@ -38,26 +36,6 @@ public class IqGenerator extends AbstractGenerator {
super(service);
}
- public Iq entityTimeResponse(final Iq request) {
- final Iq packet = request.generateResponse(Iq.Type.RESULT);
- Element time = packet.addChild("time", "urn:xmpp:time");
- final long now = System.currentTimeMillis();
- time.addChild("utc").setContent(getTimestamp(now));
- TimeZone ourTimezone = TimeZone.getDefault();
- long offsetSeconds = ourTimezone.getOffset(now) / 1000;
- long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
- long offsetHours = offsetSeconds / 3600;
- String hours;
- if (offsetHours < 0) {
- hours = String.format(Locale.US, "%03d", offsetHours);
- } else {
- hours = String.format(Locale.US, "%02d", offsetHours);
- }
- String minutes = String.format(Locale.US, "%02d", offsetMinutes);
- time.addChild("tzo").setContent(hours + ":" + minutes);
- return packet;
- }
-
public static Iq purgeOfflineMessages() {
final Iq packet = new Iq(Iq.Type.SET);
packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
@@ -338,13 +316,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq generateGetBlockList() {
- final Iq iq = new Iq(Iq.Type.GET);
- iq.addChild("blocklist", Namespace.BLOCKING);
-
- return iq;
- }
-
public Iq generateSetBlockRequest(
final Jid jid, final boolean reportSpam, final String serverMsgId) {
final Iq iq = new Iq(Iq.Type.SET);
@@ -7,24 +7,33 @@ import com.google.common.base.CharMatcher;
import com.google.common.io.BaseEncoding;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.Jid;
-import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
+import eu.siacs.conversations.xmpp.manager.PingManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
+import im.conversations.android.xmpp.model.blocking.Block;
+import im.conversations.android.xmpp.model.blocking.Unblock;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.ibb.InBandByteStream;
+import im.conversations.android.xmpp.model.ping.Ping;
+import im.conversations.android.xmpp.model.roster.Query;
import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.time.Time;
+import im.conversations.android.xmpp.model.up.Push;
import im.conversations.android.xmpp.model.version.Version;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -43,76 +52,6 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
super(service, connection);
}
- public static List<Jid> items(final Iq packet) {
- ArrayList<Jid> items = new ArrayList<>();
- final Element query = packet.findChild("query", Namespace.DISCO_ITEMS);
- if (query == null) {
- return items;
- }
- for (Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- Jid jid = child.getAttributeAsJid("jid");
- if (jid != null) {
- items.add(jid);
- }
- }
- }
- return items;
- }
-
- private void rosterItems(final Account account, final Element query) {
- final String version = query.getAttribute("ver");
- if (version != null) {
- account.getRoster().setVersion(version);
- }
- for (final Element item : query.getChildren()) {
- if (item.getName().equals("item")) {
- final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid == null) {
- continue;
- }
- final String name = item.getAttribute("name");
- final String subscription = item.getAttribute("subscription");
- final Contact contact = account.getRoster().getContact(jid);
- boolean bothPre =
- contact.getOption(Contact.Options.TO)
- && contact.getOption(Contact.Options.FROM);
- if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
- contact.setServerName(name);
- contact.parseGroupsFromElement(item);
- }
- if ("remove".equals(subscription)) {
- contact.resetOption(Contact.Options.IN_ROSTER);
- contact.resetOption(Contact.Options.DIRTY_DELETE);
- contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
- } else {
- contact.setOption(Contact.Options.IN_ROSTER);
- contact.resetOption(Contact.Options.DIRTY_PUSH);
- contact.parseSubscriptionFromElement(item);
- }
- boolean both =
- contact.getOption(Contact.Options.TO)
- && contact.getOption(Contact.Options.FROM);
- if ((both != bothPre) && both) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": gained mutual presence subscription with "
- + contact.getJid());
- AxolotlService axolotlService = account.getAxolotlService();
- if (axolotlService != null) {
- axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
- }
- }
- mXmppConnectionService.getAvatarService().clear(contact);
- }
- }
- mXmppConnectionService.updateConversationUi();
- mXmppConnectionService.updateRosterUi();
- mXmppConnectionService.getShortcutService().refresh();
- mXmppConnectionService.syncRoster(account);
- }
-
public static String avatarData(final Iq packet) {
final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB);
if (pubsub == null) {
@@ -381,139 +320,47 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
@Override
public void accept(final Iq packet) {
- final var account = getAccount();
- final boolean isGet = packet.getType() == Iq.Type.GET;
- if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
- return;
+ final var type = packet.getType();
+ switch (type) {
+ case SET -> acceptPush(packet);
+ case GET -> acceptRequest(packet);
+ default ->
+ throw new AssertionError(
+ "IQ results and errors should are handled in callbacks");
}
- if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) {
- final Element query = packet.findChild("query");
- // If this is in response to a query for the whole roster:
- if (packet.getType() == Iq.Type.RESULT) {
- account.getRoster().markAllAsNotInRoster();
- }
- this.rosterItems(account, query);
- } else if ((packet.hasChild("block", Namespace.BLOCKING)
- || packet.hasChild("blocklist", Namespace.BLOCKING))
- && packet.fromServer(account)) {
- // Block list or block push.
- Log.d(Config.LOGTAG, "Received blocklist update from server");
- final Element blocklist = packet.findChild("blocklist", Namespace.BLOCKING);
- final Element block = packet.findChild("block", Namespace.BLOCKING);
- final Collection<Element> items =
- blocklist != null
- ? blocklist.getChildren()
- : (block != null ? block.getChildren() : null);
- // If this is a response to a blocklist query, clear the block list and replace with the
- // new one.
- // Otherwise, just update the existing blocklist.
- if (packet.getType() == Iq.Type.RESULT) {
- account.clearBlocklist();
- connection.getFeatures().setBlockListRequested(true);
- }
- if (items != null) {
- final Collection<Jid> jids = new ArrayList<>(items.size());
- // Create a collection of Jids from the packet
- for (final Element item : items) {
- if (item.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid != null) {
- jids.add(jid);
- }
- }
- }
- account.getBlocklist().addAll(jids);
- if (packet.getType() == Iq.Type.SET) {
- boolean removed = false;
- for (Jid jid : jids) {
- removed |= mXmppConnectionService.removeBlockedConversations(account, jid);
- }
- if (removed) {
- mXmppConnectionService.updateConversationUi();
- }
- }
- }
- // Update the UI
- mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
- if (packet.getType() == Iq.Type.SET) {
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- }
- } else if (packet.hasChild("unblock", Namespace.BLOCKING)
- && packet.fromServer(account)
- && packet.getType() == Iq.Type.SET) {
- Log.d(Config.LOGTAG, "Received unblock update from server");
- final Collection<Element> items =
- packet.findChild("unblock", Namespace.BLOCKING).getChildren();
- if (items.isEmpty()) {
- // No children to unblock == unblock all
- account.getBlocklist().clear();
- } else {
- final Collection<Jid> jids = new ArrayList<>(items.size());
- for (final Element item : items) {
- if (item.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
- if (jid != null) {
- jids.add(jid);
- }
- }
- }
- account.getBlocklist().removeAll(jids);
- }
- mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
- || packet.hasChild("data", "http://jabber.org/protocol/ibb")
- || packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
- mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet);
- } else if (packet.hasExtension(InfoQuery.class) && isGet) {
+ }
+
+ private void acceptPush(final Iq packet) {
+ if (packet.hasExtension(Query.class)) {
+ this.getManager(RosterManager.class).push(packet);
+ } else if (packet.hasExtension(Block.class)) {
+ this.getManager(BlockingManager.class).pushBlock(packet);
+ } else if (packet.hasExtension(Unblock.class)) {
+ this.getManager(BlockingManager.class).pushUnblock(packet);
+ } else if (packet.hasExtension(InBandByteStream.class)) {
+ mXmppConnectionService
+ .getJingleConnectionManager()
+ .deliverIbbPacket(getAccount(), packet);
+ } else if (packet.hasExtension(Push.class)) {
+ this.getManager(UnifiedPushManager.class).push(packet);
+ } else {
+ this.connection.sendErrorFor(
+ packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
+ }
+ }
+
+ private void acceptRequest(final Iq packet) {
+ if (packet.hasExtension(InfoQuery.class)) {
this.getManager(DiscoManager.class).handleInfoQuery(packet);
- } else if (packet.hasExtension(Version.class) && isGet) {
+ } else if (packet.hasExtension(Version.class)) {
this.getManager(DiscoManager.class).handleVersionRequest(packet);
- } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
- final Iq response = packet.generateResponse(Iq.Type.RESULT);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) {
- final Iq response;
- if (mXmppConnectionService.useTorToConnect() || account.isOnion()) {
- response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas");
- } else {
- response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
- }
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH)
- && packet.getType() == Iq.Type.SET) {
- final Jid transport = packet.getFrom();
- final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH);
- final boolean success =
- push != null
- && mXmppConnectionService.processUnifiedPushMessage(
- account, transport, push);
- final Iq response;
- if (success) {
- response = packet.generateResponse(Iq.Type.RESULT);
- } else {
- response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.setAttribute("code", "404");
- error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas");
- }
- mXmppConnectionService.sendIqPacket(account, response, null);
+ } else if (packet.hasExtension(Time.class)) {
+ this.getManager(EntityTimeManager.class).request(packet);
+ } else if (packet.hasExtension(Ping.class)) {
+ this.getManager(PingManager.class).pong(packet);
} else {
- if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) {
- final Iq response = packet.generateResponse(Iq.Type.ERROR);
- final Element error = response.addChild("error");
- error.setAttribute("type", "cancel");
- error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas");
- connection.sendIqPacket(response, null);
- }
+ this.connection.sendErrorFor(
+ packet, Error.Type.CANCEL, new Condition.FeatureNotImplemented());
}
}
}
@@ -37,6 +37,7 @@ import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.avatar.Metadata;
@@ -276,7 +277,7 @@ public class MessageParser extends AbstractParser
} else {
final Contact contact = account.getRoster().getContact(from);
if (contact.setAvatar(avatar)) {
- mXmppConnectionService.syncRoster(account);
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi();
@@ -410,7 +411,7 @@ public class MessageParser extends AbstractParser
} else {
Contact contact = account.getRoster().getContact(user);
if (contact.setPresenceName(nick)) {
- mXmppConnectionService.syncRoster(account);
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
}
}
@@ -1435,7 +1436,7 @@ public class MessageParser extends AbstractParser
}
final Contact contact = account.getRoster().getContact(from);
if (contact.setPresenceName(nick)) {
- mXmppConnectionService.syncRoster(account);
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
}
}
@@ -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.RosterManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.occupant.OccupantId;
@@ -172,8 +173,9 @@ public class PresenceParser extends AbstractParser
.getRoster()
.getContact(user.getRealJid());
if (c.setAvatar(avatar)) {
- mXmppConnectionService.syncRoster(
- conversation.getAccount());
+ connection
+ .getManager(RosterManager.class)
+ .writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(c);
}
mXmppConnectionService.updateRosterUi();
@@ -351,7 +353,7 @@ public class PresenceParser extends AbstractParser
mXmppConnectionService.updateAccountUi();
} else {
if (contact.setAvatar(avatar)) {
- mXmppConnectionService.syncRoster(account);
+ connection.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi();
@@ -371,8 +373,7 @@ public class PresenceParser extends AbstractParser
contact.updatePresence(resource, packet);
final var nodeHash = packet.getCapabilities();
- final var connection = account.getXmppConnection();
- if (nodeHash != null && connection != null) {
+ if (nodeHash != null) {
final var discoFuture =
this.getManager(DiscoManager.class)
.infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash);
@@ -410,7 +411,7 @@ public class PresenceParser extends AbstractParser
+ contact.getJid()
+ " "
+ OpenPgpUtils.convertKeyIdToHex(keyId));
- mXmppConnectionService.syncRoster(account);
+ this.connection.getManager(RosterManager.class).writeToDatabaseAsync();
}
}
boolean online = sizeBefore < contact.getPresences().size();
@@ -440,7 +441,7 @@ public class PresenceParser extends AbstractParser
return;
}
if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) {
- mXmppConnectionService.syncRoster(account);
+ this.getManager(RosterManager.class).writeToDatabaseAsync();
mXmppConnectionService.getAvatarService().clear(contact);
}
if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
@@ -11,6 +11,7 @@ import android.os.SystemClock;
import android.util.Base64;
import android.util.Log;
import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableList;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
@@ -20,7 +21,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate;
-import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.ShortcutService;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -1809,23 +1809,29 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return rows == 1;
}
- public void readRoster(Roster roster) {
+ public List<Contact> readRoster(final Account account) {
+ final var builder = new ImmutableList.Builder<Contact>();
final SQLiteDatabase db = this.getReadableDatabase();
- final String[] args = {roster.getAccount().getUuid()};
+ final String[] args = {account.getUuid()};
try (final Cursor cursor =
db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null)) {
while (cursor.moveToNext()) {
- roster.initContact(Contact.fromCursor(cursor));
+ final var contact = Contact.fromCursor(cursor);
+ if (contact != null) {
+ contact.setAccount(account);
+ builder.add(contact);
+ }
}
}
+ return builder.build();
}
- public void writeRoster(final Roster roster) {
- long start = SystemClock.elapsedRealtime();
- final Account account = roster.getAccount();
+ public void writeRoster(
+ final Account account, final String version, final List<Contact> contacts) {
+ final long start = SystemClock.elapsedRealtime();
final SQLiteDatabase db = this.getWritableDatabase();
db.beginTransaction();
- for (Contact contact : roster.getContacts()) {
+ for (final Contact contact : contacts) {
if (contact.getOption(Contact.Options.IN_ROSTER)
|| contact.hasAvatarOrPresenceName()
|| contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
@@ -1838,7 +1844,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
db.setTransactionSuccessful();
db.endTransaction();
- account.setRosterVersion(roster.getVersion());
+ account.setRosterVersion(version);
updateAccount(account);
long duration = SystemClock.elapsedRealtime() - start;
Log.d(
@@ -31,6 +31,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Presence;
+import im.conversations.android.xmpp.model.up.Push;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.List;
@@ -320,8 +321,7 @@ public class UnifiedPushBroker {
service.sendBroadcast(intent);
}
- public boolean processPushMessage(
- final Account account, final Jid transport, final Element push) {
+ public boolean processPushMessage(final Account account, final Jid transport, final Push push) {
final String instance = push.getAttribute("instance");
final String application = push.getAttribute("application");
if (Strings.isNullOrEmpty(instance) || Strings.isNullOrEmpty(application)) {
@@ -112,7 +112,6 @@ import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.QuickLoader;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
-import eu.siacs.conversations.utils.ReplacingTaskManager;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.utils.StringUtils;
@@ -139,6 +138,7 @@ 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.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
import im.conversations.android.xmpp.Entity;
@@ -149,6 +149,7 @@ 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 im.conversations.android.xmpp.model.up.Push;
import java.io.File;
import java.security.Security;
import java.security.cert.CertificateException;
@@ -228,7 +229,6 @@ public class XmppConnectionService extends Service {
new SerialSingleThreadExecutor("DatabaseReader");
private final SerialSingleThreadExecutor mNotificationExecutor =
new SerialSingleThreadExecutor("NotificationExecutor");
- private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
private final IBinder mBinder = new XmppConnectionBinder();
private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
private final IqGenerator mIqGenerator = new IqGenerator(this);
@@ -1173,7 +1173,7 @@ public class XmppConnectionService extends Service {
}
public boolean processUnifiedPushMessage(
- final Account account, final Jid transport, final Element push) {
+ final Account account, final Jid transport, final Push push) {
return unifiedPushBroker.processPushMessage(account, transport, push);
}
@@ -1750,7 +1750,7 @@ public class XmppConnectionService extends Service {
int activeAccounts = 0;
for (final Account account : accounts) {
if (account.isConnectionEnabled()) {
- databaseBackend.writeRoster(account.getRoster());
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabase();
activeAccounts++;
}
if (account.getXmppConnection() != null) {
@@ -2529,10 +2529,9 @@ public class XmppConnectionService extends Service {
}
Log.d(Config.LOGTAG, "restoring roster...");
for (final Account account : accounts) {
- databaseBackend.readRoster(account.getRoster());
account.initAccountServices(
- XmppConnectionService
- .this); // roster needs to be loaded at this stage
+ this); // roster needs to be loaded at this stage
+ account.getXmppConnection().getManager(RosterManager.class).restore();
}
getBitmapCache().evictAll();
loadPhoneContacts();
@@ -2583,9 +2582,13 @@ public class XmppConnectionService extends Service {
() -> {
final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
Log.d(Config.LOGTAG, "start merging phone contacts with roster");
+ // TODO if we do this merge this only on enabled accounts we need to trigger
+ // this upon enable
for (final Account account : accounts) {
- final List<Contact> withSystemAccounts =
- account.getRoster().getWithSystemAccounts(JabberIdContact.class);
+ final var remaining =
+ new ArrayList<>(
+ account.getRoster()
+ .getWithSystemAccounts(JabberIdContact.class));
for (final JabberIdContact jidContact : contacts.values()) {
final Contact contact =
account.getRoster().getContact(jidContact.getJid());
@@ -2593,9 +2596,9 @@ public class XmppConnectionService extends Service {
if (needsCacheClean) {
getAvatarService().clear(contact);
}
- withSystemAccounts.remove(contact);
+ remaining.remove(contact);
}
- for (final Contact contact : withSystemAccounts) {
+ for (final Contact contact : remaining) {
boolean needsCacheClean =
contact.unsetPhoneContact(JabberIdContact.class);
if (needsCacheClean) {
@@ -2611,11 +2614,6 @@ public class XmppConnectionService extends Service {
});
}
- public void syncRoster(final Account account) {
- mRosterSyncTaskManager.execute(
- account, () -> databaseBackend.writeRoster(account.getRoster()));
- }
-
public List<Conversation> getConversations() {
return this.conversations;
}
@@ -3260,7 +3258,6 @@ public class XmppConnectionService extends Service {
if (CallIntegration.hasSystemFeature(this)) {
CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
}
- this.mRosterSyncTaskManager.clear(account);
updateAccountUi();
mNotificationService.updateErrorNotification();
syncEnabledAccountSetting();
@@ -4693,6 +4690,7 @@ 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)) {
@@ -4741,7 +4739,7 @@ public class XmppConnectionService extends Service {
account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
}
} else {
- syncRoster(contact.getAccount());
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
}
}
@@ -5127,7 +5125,9 @@ public class XmppConnectionService extends Service {
final Contact contact =
account.getRoster().getContact(avatar.owner);
contact.setAvatar(avatar);
- syncRoster(account);
+ account.getXmppConnection()
+ .getManager(RosterManager.class)
+ .writeToDatabaseAsync();
getAvatarService().clear(contact);
updateConversationUi();
updateRosterUi();
@@ -5202,7 +5202,9 @@ public class XmppConnectionService extends Service {
final Contact contact =
account.getRoster().getContact(avatar.owner);
contact.setAvatar(avatar, previouslyOmittedPepFetch);
- syncRoster(account);
+ account.getXmppConnection()
+ .getManager(RosterManager.class)
+ .writeToDatabaseAsync();
getAvatarService().clear(contact);
updateRosterUi();
}
@@ -5227,7 +5229,9 @@ public class XmppConnectionService extends Service {
account.getRoster()
.getContact(user.getRealJid());
contact.setAvatar(avatar);
- syncRoster(account);
+ account.getXmppConnection()
+ .getManager(RosterManager.class)
+ .writeToDatabaseAsync();
getAvatarService().clear(contact);
updateRosterUi();
}
@@ -5329,16 +5333,7 @@ public class XmppConnectionService extends Service {
private void reconnectAccount(
final Account account, final boolean force, final boolean interactive) {
synchronized (account) {
- final XmppConnection existingConnection = account.getXmppConnection();
- final XmppConnection connection;
- if (existingConnection != null) {
- connection = existingConnection;
- } else if (account.isConnectionEnabled()) {
- connection = createConnection(account);
- account.setXmppConnection(connection);
- } else {
- return;
- }
+ final XmppConnection connection = account.getXmppConnection();
final boolean hasInternet = hasInternetConnection();
if (account.isConnectionEnabled() && hasInternet) {
if (!force) {
@@ -5352,7 +5347,7 @@ public class XmppConnectionService extends Service {
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
} else {
disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
- account.getRoster().clearPresences();
+ connection.getManager(RosterManager.class).clearPresences();
connection.resetEverything();
final AxolotlService axolotlService = account.getAxolotlService();
if (axolotlService != null) {
@@ -1,57 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.utils;
-
-import java.util.HashMap;
-
-import eu.siacs.conversations.entities.Account;
-
-public class ReplacingTaskManager {
-
- private final HashMap<Account, ReplacingSerialSingleThreadExecutor> executors = new HashMap<>();
-
- public void execute(final Account account, Runnable runnable) {
- ReplacingSerialSingleThreadExecutor executor;
- synchronized (this.executors) {
- executor = this.executors.get(account);
- if (executor == null) {
- executor = new ReplacingSerialSingleThreadExecutor(ReplacingTaskManager.class.getSimpleName());
- this.executors.put(account, executor);
- }
- executor.execute(runnable);
- }
- }
-
- public void clear(Account account) {
- synchronized (this.executors) {
- this.executors.remove(account);
- }
- }
-}
@@ -41,6 +41,7 @@ public final class Namespace {
public static final String SASL_2 = "urn:xmpp:sasl:2";
public static final String CHANNEL_BINDING = "urn:xmpp:sasl-cb:0";
public static final String FAST = "urn:xmpp:fast:0";
+ public static final String TIME = "urn:xmpp:time";
public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls";
public static final String PUBSUB = "http://jabber.org/protocol/pubsub";
public static final String PUBSUB_EVENT = PUBSUB + "#event";
@@ -1,13 +1,17 @@
package eu.siacs.conversations.xmpp;
-import android.content.Context;
import com.google.common.collect.ClassToInstanceMap;
import com.google.common.collect.ImmutableClassToInstanceMap;
+import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.manager.AbstractManager;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.EntityTimeManager;
import eu.siacs.conversations.xmpp.manager.PingManager;
import eu.siacs.conversations.xmpp.manager.PresenceManager;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
+import eu.siacs.conversations.xmpp.manager.UnifiedPushManager;
public class Managers {
@@ -16,12 +20,16 @@ public class Managers {
}
public static ClassToInstanceMap<AbstractManager> get(
- final Context context, final XmppConnection connection) {
+ final XmppConnectionService context, final XmppConnection connection) {
return new ImmutableClassToInstanceMap.Builder<AbstractManager>()
+ .put(BlockingManager.class, new BlockingManager(context, connection))
.put(CarbonsManager.class, new CarbonsManager(context, connection))
.put(DiscoManager.class, new DiscoManager(context, connection))
+ .put(EntityTimeManager.class, new EntityTimeManager(context, connection))
.put(PingManager.class, new PingManager(context, connection))
.put(PresenceManager.class, new PresenceManager(context, connection))
+ .put(RosterManager.class, new RosterManager(context, connection))
+ .put(UnifiedPushManager.class, new UnifiedPushManager(context, connection))
.build();
}
}
@@ -72,6 +72,7 @@ import eu.siacs.conversations.xmpp.bind.Bind2;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import eu.siacs.conversations.xmpp.manager.AbstractManager;
+import eu.siacs.conversations.xmpp.manager.BlockingManager;
import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
import eu.siacs.conversations.xmpp.manager.PingManager;
@@ -224,7 +225,7 @@ public class XmppConnection implements Runnable {
this.unregisteredIqListener = new IqParser(service, this);
this.messageListener = new MessageParser(service, this);
this.bindListener = new BindProcessor(service, this);
- this.managers = Managers.get(service.getApplicationContext(), this);
+ this.managers = Managers.get(service, this);
}
private static void fixResource(final Context context, final Account account) {
@@ -2314,6 +2315,7 @@ public class XmppConnection implements Runnable {
}
private void discoverCommands() {
+ // TODO move result handling into DiscoManager too
final var future =
getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain()));
Futures.addCallback(
@@ -2357,9 +2359,9 @@ public class XmppConnection implements Runnable {
}
private void enableAdvancedStreamFeatures() {
- if (getFeatures().blocking() && !features.blockListRequested) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list");
- this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener);
+ final var blockingManager = getManager(BlockingManager.class);
+ if (blockingManager.hasFeature()) {
+ blockingManager.request();
}
for (final OnAdvancedStreamFeaturesLoaded listener :
advancedStreamFeaturesLoadedListeners) {
@@ -2751,15 +2753,6 @@ public class XmppConnection implements Runnable {
return Iterables.getFirst(items, null).getKey();
}
- public boolean r() {
- if (getFeatures().sm()) {
- this.tagWriter.writeStanzaAsync(new Request());
- return true;
- } else {
- return false;
- }
- }
-
public List<String> getMucServersWithholdAccount() {
final List<String> servers = getMucServers();
servers.remove(account.getDomain().toString());
@@ -2846,10 +2839,6 @@ public class XmppConnection implements Runnable {
this.mInteractive = interactive;
}
- private IqGenerator getIqGenerator() {
- return mXmppConnectionService.getIqGenerator();
- }
-
public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
if (trackOfflineMessageRetrieval) {
getManager(PingManager.class)
@@ -2871,20 +2860,6 @@ public class XmppConnection implements Runnable {
return this.offlineMessagesRetrieved;
}
- public void fetchRoster() {
- final Iq iqPacket = new Iq(Iq.Type.GET);
- final var version = account.getRosterVersion();
- if (Strings.isNullOrEmpty(account.getRosterVersion())) {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": fetching roster version " + version);
- }
- iqPacket.query(Namespace.ROSTER).setAttribute("ver", version);
- sendIqPacket(iqPacket, unregisteredIqListener);
- }
-
public void triggerConnectionTimeout() {
final var duration = getConnectionDuration();
Log.d(
@@ -2909,6 +2884,15 @@ public class XmppConnection implements Runnable {
return this.features;
}
+ public boolean fromServer(final Stanza stanza) {
+ final var account = getAccount().getJid();
+ final Jid from = stanza.getFrom();
+ return from == null
+ || from.equals(account.getDomain())
+ || from.equals(account.asBareJid())
+ || from.equals(account);
+ }
+
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@@ -898,6 +898,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
}
public void deliverIbbPacket(final Account account, final Iq packet) {
+ // TODO use extensions
final String sid;
final Element payload;
final InbandBytestreamsTransport.PacketType packetType;
@@ -313,6 +313,7 @@ public class SocksByteStreamsTransport implements Transport {
}
final Iq iqRequest = new Iq(Iq.Type.GET);
iqRequest.setTo(streamer);
+ // TODO urgent refactor to extension
iqRequest.query(Namespace.BYTE_STREAMS);
final SettableFuture<Candidate> candidateFuture = SettableFuture.create();
xmppConnection.sendIqPacket(
@@ -6,6 +6,6 @@ import eu.siacs.conversations.xmpp.XmppConnection;
public abstract class AbstractManager extends XmppConnection.Delegate {
protected AbstractManager(final Context context, final XmppConnection connection) {
- super(context, connection);
+ super(context.getApplicationContext(), connection);
}
}
@@ -0,0 +1,144 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+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.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.blocking.Block;
+import im.conversations.android.xmpp.model.blocking.Blocklist;
+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.stanza.Iq;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class BlockingManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ private final HashSet<Jid> blocklist = new HashSet<>();
+
+ public BlockingManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ // TODO find a way to get rid of XmppConnectionService and use context instead
+ this.service = service;
+ }
+
+ public void request() {
+ final var future = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Blocklist()));
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Iq result) {
+ final var blocklist = result.getExtension(Blocklist.class);
+ if (blocklist == null) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": invalid blocklist response");
+ return;
+ }
+ final var addresses = itemsAsAddresses(blocklist.getItems());
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": discovered blocklist with "
+ + addresses.size()
+ + " items");
+ setBlocklist(addresses);
+ removeBlockedConversations(addresses);
+ service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.w(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": could not retrieve blocklist",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public void pushBlock(final Iq request) {
+ if (connection.fromServer(request)) {
+ final var block = request.getExtension(Block.class);
+ final var addresses = itemsAsAddresses(block.getItems());
+ synchronized (this.blocklist) {
+ this.blocklist.addAll(addresses);
+ }
+ this.removeBlockedConversations(addresses);
+ this.service.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
+ this.connection.sendResultFor(request);
+ } else {
+ this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
+ }
+ }
+
+ public void pushUnblock(final Iq request) {
+ if (connection.fromServer(request)) {
+ final var unblock = request.getExtension(Unblock.class);
+ final var address = itemsAsAddresses(unblock.getItems());
+ synchronized (this.blocklist) {
+ this.blocklist.removeAll(address);
+ }
+ this.service.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
+ this.connection.sendResultFor(request);
+ } else {
+ this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
+ }
+ }
+
+ private void removeBlockedConversations(final Collection<Jid> addresses) {
+ var removed = false;
+ for (final Jid address : addresses) {
+ removed |= service.removeBlockedConversations(getAccount(), address);
+ }
+ if (removed) {
+ service.updateConversationUi();
+ }
+ }
+
+ public ImmutableSet<Jid> getBlocklist() {
+ synchronized (this.blocklist) {
+ return ImmutableSet.copyOf(this.blocklist);
+ }
+ }
+
+ private void setBlocklist(final Collection<Jid> addresses) {
+ synchronized (this.blocklist) {
+ this.blocklist.clear();
+ this.blocklist.addAll(addresses);
+ }
+ }
+
+ public boolean hasFeature() {
+ return getManager(DiscoManager.class).hasServerFeature(Namespace.BLOCKING);
+ }
+
+ private static Set<Jid> itemsAsAddresses(final Collection<Item> items) {
+ final var builder = new ImmutableSet.Builder<Jid>();
+ for (final var item : items) {
+ final var jid = Jid.Invalid.getNullForInvalid(item.getJid());
+ if (jid == null) {
+ continue;
+ }
+ builder.add(jid);
+ }
+ return builder.build();
+ }
+}
@@ -71,7 +71,7 @@ public class DiscoManager extends AbstractManager {
Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION);
private final List<String> PRIVACY_SENSITIVE =
Collections.singletonList(
- "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
+ Namespace.TIME // XEP-0202: Entity Time leaks time zone
);
private final List<String> VOIP_NAMESPACES =
Arrays.asList(
@@ -0,0 +1,43 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.generator.AbstractGenerator;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.time.Time;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class EntityTimeManager extends AbstractManager {
+
+ public EntityTimeManager(Context context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public void request(final Iq request) {
+ final var appSettings = new AppSettings(this.context);
+ if (appSettings.isUseTor() || getAccount().isOnion()) {
+ this.connection.sendErrorFor(request, Error.Type.AUTH, new Condition.Forbidden());
+ return;
+ }
+ final var time = new Time();
+ final long now = System.currentTimeMillis();
+ time.setUniversalTime(AbstractGenerator.getTimestamp(now));
+ final TimeZone ourTimezone = TimeZone.getDefault();
+ final long offsetSeconds = ourTimezone.getOffset(now) / 1000;
+ final long offsetMinutes = Math.abs((offsetSeconds % 3600) / 60);
+ final long offsetHours = offsetSeconds / 3600;
+ final String hours;
+ if (offsetHours < 0) {
+ hours = String.format(Locale.US, "%03d", offsetHours);
+ } else {
+ hours = String.format(Locale.US, "%02d", offsetHours);
+ }
+ String minutes = String.format(Locale.US, "%02d", offsetMinutes);
+ time.setTimeZoneOffset(hours + ":" + minutes);
+ this.connection.sendResultFor(request, time);
+ }
+}
@@ -44,4 +44,8 @@ public class PingManager extends AbstractManager {
},
MoreExecutors.directExecutor());
}
+
+ public void pong(final Iq packet) {
+ this.connection.sendResultFor(packet);
+ }
}
@@ -0,0 +1,276 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+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.android.AbstractPhoneContact;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Roster;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.roster.Item;
+import im.conversations.android.xmpp.model.roster.Query;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+public class RosterManager extends AbstractManager implements Roster {
+
+ private final ReplacingSerialSingleThreadExecutor dbExecutor =
+ new ReplacingSerialSingleThreadExecutor(RosterManager.class.getName());
+
+ private final List<Contact> contacts = new ArrayList<>();
+ private String version;
+
+ private final XmppConnectionService service;
+
+ public RosterManager(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.version = getAccount().getRosterVersion();
+ ;
+ this.service = service;
+ }
+
+ public void request() {
+ final var iq = new Iq(Iq.Type.GET);
+ final var query = iq.addExtension(new Query());
+ final var version = this.version;
+ if (version != null) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": requesting roster version " + version);
+ query.setVersion(version);
+ } else {
+ Log.d(Config.LOGTAG, getAccount().getJid().asBareJid() + " requesting roster");
+ }
+ final var future = connection.sendIqPacket(iq);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Iq result) {
+ final var query = result.getExtension(Query.class);
+ if (query == null) {
+ // No query in result means further modifications are sent via pushes
+ return;
+ }
+ final var version = query.getVersion();
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": received full roster (version="
+ + version
+ + ")");
+ final var items = query.getItems();
+ // In a roster result (Section 2.1.4), the client MUST ignore values of the
+ // 'subscription'
+ // attribute other than "none", "to", "from", or "both".
+ final var validItems =
+ Collections2.filter(
+ items,
+ i ->
+ Item.RESULT_SUBSCRIPTIONS.contains(
+ i.getSubscription())
+ && Objects.nonNull(i.getJid()));
+
+ setRosterItems(version, validItems);
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": could not fetch roster",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private void setRosterItems(final String version, final Collection<Item> items) {
+ synchronized (this.contacts) {
+ markAllAsNotInRoster();
+ for (final var item : items) {
+ processRosterItem(item);
+ }
+ this.version = version;
+ }
+ this.triggerUiUpdates();
+ this.writeToDatabaseAsync();
+ }
+
+ private void modifyRosterItems(final String version, final Collection<Item> items) {
+ synchronized (this.contacts) {
+ for (final var item : items) {
+ processRosterItem(item);
+ }
+ this.version = version;
+ }
+ this.triggerUiUpdates();
+ this.writeToDatabaseAsync();
+ }
+
+ private void triggerUiUpdates() {
+ this.service.updateConversationUi();
+ this.service.updateRosterUi();
+ this.service.getShortcutService().refresh();
+ }
+
+ public void push(final Iq packet) {
+ if (connection.fromServer(packet)) {
+ final var query = packet.getExtension(Query.class);
+ final var version = query.getVersion();
+ modifyRosterItems(version, query.getItems());
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid() + ": received roster push (version=" + version + ")");
+ } else {
+ connection.sendErrorFor(packet, Error.Type.AUTH, new Condition.Forbidden());
+ }
+ }
+
+ private void processRosterItem(final Item item) {
+ // this is verbatim the original code from IqParser.
+ // TODO there are likely better ways to handle roster management
+ final Jid jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("jid"));
+ if (jid == null) {
+ return;
+ }
+ final var name = item.getItemName();
+ final var subscription = item.getSubscription();
+ // getContactInternal is not synchronized because all access to processRosterItem is
+ final var contact = getContactInternal(jid);
+ boolean bothPre =
+ contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
+ if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
+ contact.setServerName(name);
+ contact.parseGroupsFromElement(item);
+ }
+ if (subscription == Item.Subscription.REMOVE) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_DELETE);
+ contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
+ } else {
+ contact.setOption(Contact.Options.IN_ROSTER);
+ contact.resetOption(Contact.Options.DIRTY_PUSH);
+ // TODO use subscription; and set asking separately
+ contact.parseSubscriptionFromElement(item);
+ }
+ boolean both =
+ contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
+ if ((both != bothPre) && both) {
+ final var account = getAccount();
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": gained mutual presence subscription with "
+ + contact.getJid());
+ final var axolotlService = account.getAxolotlService();
+ if (axolotlService != null) {
+ axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
+ }
+ }
+ service.getAvatarService().clear(contact);
+ }
+
+ @Override
+ @NonNull
+ public Contact getContact(@NonNull final Jid jid) {
+ synchronized (this.contacts) {
+ return this.getContactInternal(jid);
+ }
+ }
+
+ @NonNull
+ public Contact getContactInternal(@NonNull final Jid jid) {
+ final var existing =
+ Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()), null);
+ if (existing != null) {
+ return existing;
+ }
+ final var contact = new Contact(jid.asBareJid());
+ contact.setAccount(getAccount());
+ this.contacts.add(contact);
+ return contact;
+ }
+
+ @Override
+ @Nullable
+ public Contact getContactFromContactList(@NonNull final Jid jid) {
+ synchronized (this.contacts) {
+ final var contact =
+ Iterables.find(this.contacts, c -> c.getJid().equals(jid.asBareJid()));
+ if (contact != null && contact.showInContactList()) {
+ return contact;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public List<Contact> getContacts() {
+ synchronized (this.contacts) {
+ return ImmutableList.copyOf(this.contacts);
+ }
+ }
+
+ @Override
+ public ImmutableList<Contact> getWithSystemAccounts(
+ final Class<? extends AbstractPhoneContact> clazz) {
+ final int option = Contact.getOption(clazz);
+ synchronized (this.contacts) {
+ return ImmutableList.copyOf(
+ Collections2.filter(this.contacts, c -> c.getOption(option)));
+ }
+ }
+
+ public void clearPresences() {
+ synchronized (this.contacts) {
+ for (final var contact : this.contacts) {
+ contact.clearPresences();
+ }
+ }
+ }
+
+ private void markAllAsNotInRoster() {
+ for (final var contact : this.contacts) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ }
+ }
+
+ public void restore() {
+ synchronized (this.contacts) {
+ this.contacts.clear();
+ this.contacts.addAll(getDatabase().readRoster(getAccount()));
+ }
+ }
+
+ public void writeToDatabaseAsync() {
+ this.dbExecutor.execute(this::writeToDatabase);
+ }
+
+ public void writeToDatabase() {
+ final var account = getAccount();
+ final List<Contact> contacts;
+ final String version;
+ synchronized (this.contacts) {
+ contacts = ImmutableList.copyOf(this.contacts);
+ version = this.version;
+ }
+ getDatabase().writeRoster(account, version, contacts);
+ }
+}
@@ -0,0 +1,34 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.up.Push;
+
+public class UnifiedPushManager extends AbstractManager {
+
+ private final XmppConnectionService service;
+
+ public UnifiedPushManager(
+ final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
+ this.service = service;
+ }
+
+ public void push(final Iq packet) {
+ final Jid transport = packet.getFrom();
+ final var push = packet.getOnlyExtension(Push.class);
+ if (push == null || transport == null) {
+ connection.sendErrorFor(packet, Error.Type.MODIFY, new Condition.BadRequest());
+ return;
+ }
+ if (service.processUnifiedPushMessage(getAccount(), transport, push)) {
+ connection.sendResultFor(packet);
+ } else {
+ connection.sendErrorFor(packet, Error.Type.CANCEL, new Condition.ItemNotFound());
+ }
+ }
+}
@@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.blocking;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement
public class Block extends Extension {
@@ -9,4 +10,8 @@ public class Block extends Extension {
public Block() {
super(Block.class);
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -2,10 +2,15 @@ package im.conversations.android.xmpp.model.blocking;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement
public class Blocklist extends Extension {
public Blocklist() {
super(Blocklist.class);
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.blocking;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement
public class Unblock extends Extension {
@@ -9,4 +10,8 @@ public class Unblock extends Extension {
public Unblock() {
super(Unblock.class);
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Close extends InBandByteStream {
+
+ public Close() {
+ super(Close.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+
+@XmlElement
+public class Data extends InBandByteStream implements ByteContent {
+
+ public Data() {
+ super(Data.class);
+ }
+}
@@ -0,0 +1,14 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.xmpp.model.Extension;
+
+public abstract class InBandByteStream extends Extension {
+
+ public InBandByteStream(Class<? extends InBandByteStream> clazz) {
+ super(clazz);
+ }
+
+ public String getSid() {
+ return this.getAttribute("sid");
+ }
+}
@@ -0,0 +1,11 @@
+package im.conversations.android.xmpp.model.ibb;
+
+import im.conversations.android.annotation.XmlElement;
+
+@XmlElement
+public class Open extends InBandByteStream {
+
+ public Open() {
+ super(Open.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.IBB)
+package im.conversations.android.xmpp.model.ibb;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -3,6 +3,7 @@ package im.conversations.android.xmpp.model.roster;
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import java.util.Collection;
@XmlElement(name = "query", namespace = Namespace.ROSTER)
public class Query extends Extension {
@@ -18,4 +19,8 @@ public class Query extends Extension {
public String getVersion() {
return this.getAttribute("ver");
}
+
+ public Collection<Item> getItems() {
+ return this.getExtensions(Item.class);
+ }
}
@@ -0,0 +1,20 @@
+package im.conversations.android.xmpp.model.time;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Time extends Extension {
+
+ public Time() {
+ super(Time.class);
+ }
+
+ public void setTimeZoneOffset(final String tzo) {
+ this.addExtension(new TimeZoneOffset()).setContent(tzo);
+ }
+
+ public void setUniversalTime(final String utc) {
+ this.addExtension(new UniversalTime()).setContent(utc);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.time;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "tzo")
+public class TimeZoneOffset extends Extension {
+
+ public TimeZoneOffset() {
+ super(TimeZoneOffset.class);
+ }
+}
@@ -0,0 +1,12 @@
+package im.conversations.android.xmpp.model.time;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement(name = "utc")
+public class UniversalTime extends Extension {
+
+ public UniversalTime() {
+ super(UniversalTime.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.TIME)
+package im.conversations.android.xmpp.model.time;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -0,0 +1,13 @@
+package im.conversations.android.xmpp.model.up;
+
+import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.ByteContent;
+import im.conversations.android.xmpp.model.Extension;
+
+@XmlElement
+public class Push extends Extension implements ByteContent {
+
+ public Push() {
+ super(Push.class);
+ }
+}
@@ -0,0 +1,5 @@
+@XmlPackage(namespace = Namespace.UNIFIED_PUSH)
+package im.conversations.android.xmpp.model.up;
+
+import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.annotation.XmlPackage;
@@ -7,6 +7,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.model.stanza.Iq;
public class BindProcessor extends XmppConnection.Delegate implements Runnable {
@@ -49,7 +50,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
}
}
- account.getRoster().clearPresences();
+ connection.getManager(RosterManager.class).clearPresences();
synchronized (account.inProgressConferenceJoins) {
account.inProgressConferenceJoins.clear();
}
@@ -59,7 +60,7 @@ public class BindProcessor extends XmppConnection.Delegate implements Runnable {
service.getJingleConnectionManager().notifyRebound(account);
service.getQuickConversationsService().considerSyncBackground(false);
- connection.fetchRoster();
+ getManager(RosterManager.class).request();
if (features.bookmarks2()) {
service.fetchBookmarks2(account);
@@ -26,6 +26,7 @@ import eu.siacs.conversations.utils.TLSSocketFactory;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.RosterManager;
import im.conversations.android.xmpp.model.stanza.Iq;
import io.michaelrocks.libphonenumber.android.Phonenumber;
import java.io.BufferedWriter;
@@ -407,7 +408,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
refresh(account, contacts.values());
if (!considerSync(account, contacts, forced)) {
- service.syncRoster(account);
+ account.getXmppConnection().getManager(RosterManager.class).writeToDatabaseAsync();
}
}
}
@@ -422,7 +423,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService
}
private void refresh(Account account, Collection<PhoneNumberContact> contacts) {
- for (Contact contact :
+ for (final var contact :
account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) {
final Uri uri = contact.getSystemAccount();
if (uri == null) {
@@ -498,9 +499,11 @@ public class QuickConversationsService extends AbstractQuickConversationsService
final Element phoneBook =
response.findChild("phone-book", Namespace.SYNCHRONIZATION);
if (phoneBook != null) {
- final List<Contact> withSystemAccounts =
- account.getRoster()
- .getWithSystemAccounts(PhoneNumberContact.class);
+ final var remaining =
+ new ArrayList<>(
+ account.getRoster()
+ .getWithSystemAccounts(
+ PhoneNumberContact.class));
for (Entry entry : Entry.ofPhoneBook(phoneBook)) {
final PhoneNumberContact phoneContact =
contacts.get(entry.getNumber());
@@ -514,10 +517,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService
if (needsCacheClean) {
service.getAvatarService().clear(contact);
}
- withSystemAccounts.remove(contact);
+ remaining.remove(contact);
}
}
- for (final Contact contact : withSystemAccounts) {
+ for (final Contact contact : remaining) {
final boolean needsCacheClean =
contact.unsetPhoneContact(PhoneNumberContact.class);
if (needsCacheClean) {
@@ -539,7 +542,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService
+ ": failed to sync contact list with api server");
}
mRunningSyncJobs.decrementAndGet();
- service.syncRoster(account);
+ account.getXmppConnection()
+ .getManager(RosterManager.class)
+ .writeToDatabaseAsync();
service.updateRosterUi();
});
return true;
@@ -82,7 +82,7 @@ public class EntityCapabilitiesTest {
}
@Test
- public void entityCapsOpenFire() throws IOException {
+ public void entityCapsOpenFireOrg() throws IOException {
final String xml =
"""
<iq type="result" xmlns="jabber:client" to="inputmice3@igniterealtime.org/Conversations.cI4W" from="igniterealtime.org" id="L3xl8X8_kzvx">
@@ -206,6 +206,104 @@ public class EntityCapabilitiesTest {
Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var);
}
+ @Test
+ public void entityCapsOpenFireTestServer() throws IOException {
+ final String xml =
+ """
+<iq type="result" id="779-6" to="jane@example.org" xmlns="jabber:client">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="server" name="Openfire Server" type="im"/>
+ <identity category="pubsub" type="pep"/>
+ <feature var="http://jabber.org/protocol/caps"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-default"/>
+ <feature var="http://jabber.org/protocol/pubsub#purge-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscription-options"/>
+ <feature var="http://jabber.org/protocol/pubsub#outcast-affiliation"/>
+ <feature var="msgoffline"/>
+ <feature var="jabber:iq:register"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#config-node"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-items"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-create"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-items"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="http://jabber.org/protocol/pubsub#persistent-items"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-and-configure"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations"/>
+ <feature var="urn:xmpp:time"/>
+ <feature var="http://jabber.org/protocol/pubsub#manage-subscriptions"/>
+ <feature var="urn:xmpp:bookmarks-conversion:0"/>
+ <feature var="http://jabber.org/protocol/offline"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-subscribe"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish-options"/>
+ <feature var="urn:xmpp:carbons:2"/>
+ <feature var="http://jabber.org/protocol/address"/>
+ <feature var="http://jabber.org/protocol/pubsub#collections"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions"/>
+ <feature var="vcard-temp"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscribe"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#get-pending"/>
+ <feature var="urn:xmpp:blocking"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-subscribe"/>
+ <feature var="http://jabber.org/protocol/pubsub#presence-notifications"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="http://jabber.org/protocol/pubsub#filtered-notifications"/>
+ <feature var="http://jabber.org/protocol/pubsub#item-ids"/>
+ <feature var="http://jabber.org/protocol/pubsub#meta-data"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-items"/>
+ <feature var="jabber:iq:roster"/>
+ <feature var="http://jabber.org/protocol/pubsub#instant-nodes"/>
+ <feature var="http://jabber.org/protocol/pubsub#modify-affiliations"/>
+ <feature var="http://jabber.org/protocol/pubsub"/>
+ <feature var="http://jabber.org/protocol/pubsub#publisher-affiliation"/>
+ <feature var="http://jabber.org/protocol/pubsub#access-open"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="http://jabber.org/protocol/pubsub#retract-items"/>
+ <feature var="jabber:iq:privacy"/>
+ <feature var="jabber:iq:last"/>
+ <feature var="http://jabber.org/protocol/commands"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:private"/>
+ <feature var="http://jabber.org/protocol/rsm"/>
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>http://jabber.org/network/serverinfo</value>
+ </field>
+ <field var="admin-addresses" type="list-multi">
+ <value>xmpp:admin@example.org</value>
+ <value>mailto:admin@example.com</value>
+ </field>
+ </x>
+ <x xmlns="jabber:x:data" type="result">
+ <field var="FORM_TYPE" type="hidden">
+ <value>urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field type="text-single" var="os">
+ <value>Linux</value>
+ </field>
+ <field type="text-single" var="os_version">
+ <value>6.8.0-59-generic amd64 - Java 21.0.7</value>
+ </field>
+ <field type="text-single" var="software">
+ <value>Openfire</value>
+ </field>
+ <field type="text-single" var="software_version">
+ <value>5.0.0 Alpha</value>
+ </field>
+ </x>
+ </query>
+</iq>
+""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(Iq.class));
+ final var iq = (Iq) element;
+ final InfoQuery info = iq.getExtension(InfoQuery.class);
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("3wkXXN9QL/i/AyVoHaqaiTT8BFA=", var);
+ }
+
@Test
public void caps2() throws IOException {
final String xml =