diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 5f38e1a51c50f17f24bd1a622660f953ece6e593..b1cd4e395137fb0a34bfc6cf612f5dd452a09b84 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -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 blocklist = new CopyOnWriteArraySet<>(); public final Set pendingConferenceJoins = new HashSet<>(); public final Set pendingConferenceLeaves = new HashSet<>(); public final Set 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 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 getBlocklist() { - return this.blocklist; - } - - public void clearBlocklist() { - getBlocklist().clear(); + public Set getBlocklist() { + final var connection = this.xmppConnection; + if (connection == null) { + return Collections.emptySet(); + } + return connection.getManager(BlockingManager.class).getBlocklist(); } public boolean isOnlineAndConnected() { diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 2d939d153d600958bc5ecb9769132f0859062281..2dbed2c1470e2713377aa973a9204ef1096d29fc 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -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 { diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java index bc4c6b9747c05c8d21c45a4f4623210a991a38b1..c1dc4bfe5a28d375b989b04e56b0c765c1f959c7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ b/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -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 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 getWithSystemAccounts(Class clazz) { - int option = Contact.getOption(clazz); - List with = getContacts(); - for(Iterator iterator = with.iterator(); iterator.hasNext();) { - Contact contact = iterator.next(); - if (!contact.getOption(option)) { - iterator.remove(); - } - } - return with; - } - - public List 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 getContacts(); - public void setVersion(String version) { - this.version = version; - } + List getWithSystemAccounts(Class 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); } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5f3173f87ea2b7813e43a9ce017bf2f215e9659e..163250e4cf7ece3676c2771981798b891eb9fccb 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 7f2d0d94fd378819522f0d7b7e1290b3173b1d03..2277d9d3ec10b576c648e1fd500fbce83936c3da 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -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 { super(service, connection); } - public static List items(final Iq packet) { - ArrayList 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 { @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 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 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 items = - packet.findChild("unblock", Namespace.BLOCKING).getChildren(); - if (items.isEmpty()) { - // No children to unblock == unblock all - account.getBlocklist().clear(); - } else { - final Collection 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()); } } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index c287a927cd08c1419bf923320a5ea79e75e8a42f..35725518c712f3f97ef0ed4f206e65f9cba67f74 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -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); } } diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index c56262ed1a475c8423e2522b2635bcdb3e7afbc0..3cdd86b600c44c668869dba930ef07f694a5bb62 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/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.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)) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 3673e6dcf6585a329b4d66ecfb98d36b469a8f06..9da8e6bc8e5b2853b5eb1b4ae487a787d6ae92df 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -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 readRoster(final Account account) { + final var builder = new ImmutableList.Builder(); 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 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( diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 24aaf1f4279b04abd32fbf3da615283c0dda3a32..5f5de553ddbad9f24306f35fb933a5df59223100 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -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)) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index b090d622fd97e79bb347da656e16903a5af80105..00e353bc802ee07f508c1595d24763b2c8befdce 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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 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 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 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 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) { diff --git a/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java b/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java deleted file mode 100644 index 396bfa3ece0259adfba748a3dd3b85e511b1406b..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/utils/ReplacingTaskManager.java +++ /dev/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 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); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 353898c56b72e48c5b80c6b02869afd4e0ee7872..6ee41f5f709049ffb11e66fc1d6b536ccdbfae4b 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -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"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index 84e0225e53773b73c08b43fa34e32cd88eebdffb..767234a04b54b7dc078191462ddd16e68faddc03 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -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 get( - final Context context, final XmppConnection connection) { + final XmppConnectionService context, final XmppConnection connection) { return new ImmutableClassToInstanceMap.Builder() + .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(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d9e79c35f96ac24b69a7acd35c22510a566ad048..3e052ebf8e0643ae0bd335553923ff97efd9c57f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -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 getMucServersWithholdAccount() { final List 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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index d01880ab8c67be468d956f85cae7d948aa11ed8b..9d2ff4434a513d6a9013c61733a4ee1fdf230dd7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java index 8cebf080fd14835dde8d5a36002b929a63514f98..a0e778d5f93839c8848a00af8d57e5c52b3e73cc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java @@ -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 candidateFuture = SettableFuture.create(); xmppConnection.sendIqPacket( diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java index b5c129e36dcb79aacb246ceffaeeadbaf5d0f924..c7c12d55ad41db26cba3a84080db7c5d794e969d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java @@ -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); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..89d21602fec1084581b455c0781a85a7e3bc3295 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/BlockingManager.java @@ -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 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 addresses) { + var removed = false; + for (final Jid address : addresses) { + removed |= service.removeBlockedConversations(getAccount(), address); + } + if (removed) { + service.updateConversationUi(); + } + } + + public ImmutableSet getBlocklist() { + synchronized (this.blocklist) { + return ImmutableSet.copyOf(this.blocklist); + } + } + + private void setBlocklist(final Collection addresses) { + synchronized (this.blocklist) { + this.blocklist.clear(); + this.blocklist.addAll(addresses); + } + } + + public boolean hasFeature() { + return getManager(DiscoManager.class).hasServerFeature(Namespace.BLOCKING); + } + + private static Set itemsAsAddresses(final Collection items) { + final var builder = new ImmutableSet.Builder(); + for (final var item : items) { + final var jid = Jid.Invalid.getNullForInvalid(item.getJid()); + if (jid == null) { + continue; + } + builder.add(jid); + } + return builder.build(); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java index 3c7c6215e5d949b9a1e246f540f7c1ee6ce09fef..cb143bb7b96c585dabb5126f2a3c54097645696d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -71,7 +71,7 @@ public class DiscoManager extends AbstractManager { Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION); private final List 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 VOIP_NAMESPACES = Arrays.asList( diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f180c485ee546b919869d446fdd825b1fa783d3b --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/EntityTimeManager.java @@ -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); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java index f162a04d6797d7adc9d296d5b54877de31d0dd69..d16ccc41c57ec4bd559ec278749ba5976868e01e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java @@ -44,4 +44,8 @@ public class PingManager extends AbstractManager { }, MoreExecutors.directExecutor()); } + + public void pong(final Iq packet) { + this.connection.sendResultFor(packet); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java new file mode 100644 index 0000000000000000000000000000000000000000..ddf5051c0d32d8a33eaa31a710985eebd2e1a94d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/RosterManager.java @@ -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 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 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 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 getContacts() { + synchronized (this.contacts) { + return ImmutableList.copyOf(this.contacts); + } + } + + @Override + public ImmutableList getWithSystemAccounts( + final Class 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 contacts; + final String version; + synchronized (this.contacts) { + contacts = ImmutableList.copyOf(this.contacts); + version = this.version; + } + getDatabase().writeRoster(account, version, contacts); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java new file mode 100644 index 0000000000000000000000000000000000000000..d9ae28f0e78b1d64bb1913598487d87fb2713d97 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/UnifiedPushManager.java @@ -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()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java index 6f5d00b3e566d1651059d75fb350c2a233959439..18f1158e1f8d5ed1d611174f7f651dfe7b9ee03f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java @@ -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 getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java index a56662d77152bf2d22bc11d76f8d01cb90f30c52..799ec4df75250d89e60f54cbd9336590d93353b5 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java @@ -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 getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java index 90cec110c050788661af7afbae475d68a7906d26..e04c00f9ddd4076f0a0852275ea861c41defe340 100644 --- a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java @@ -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 getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/Close.java b/src/main/java/im/conversations/android/xmpp/model/ibb/Close.java new file mode 100644 index 0000000000000000000000000000000000000000..f4e63148be66d7e69a52add6e5d8ac9c1e712a02 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/Close.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/Data.java b/src/main/java/im/conversations/android/xmpp/model/ibb/Data.java new file mode 100644 index 0000000000000000000000000000000000000000..e7998fc5a5cc3db439a8acda09eee7b654ed60c6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/Data.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java b/src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java new file mode 100644 index 0000000000000000000000000000000000000000..e1e73c014e5a592506482baf88b8211a5ae24f40 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/InBandByteStream.java @@ -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 clazz) { + super(clazz); + } + + public String getSid() { + return this.getAttribute("sid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/Open.java b/src/main/java/im/conversations/android/xmpp/model/ibb/Open.java new file mode 100644 index 0000000000000000000000000000000000000000..7759a7ff0f89dfd2916ab1be7d9b8cc1531e0253 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/Open.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java b/src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..38affe359a288329b3473597fbccb2bb72e2210b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ibb/package-info.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java index 616f6ae0b68cd6a13fdc9096d18df47f5852a8fb..745905995b0a0f8f7f530dcfb5d4a2c8ee9d27f9 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java @@ -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 getItems() { + return this.getExtensions(Item.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/time/Time.java b/src/main/java/im/conversations/android/xmpp/model/time/Time.java new file mode 100644 index 0000000000000000000000000000000000000000..6066e5d955f31df53bde32923a12dbe7974c7834 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/Time.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java b/src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java new file mode 100644 index 0000000000000000000000000000000000000000..1be62ea9774d7043d3347984993d939fff199ec9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/TimeZoneOffset.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java b/src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java new file mode 100644 index 0000000000000000000000000000000000000000..d1d773a69d4d1884d98598d0035c58773445cbb9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/UniversalTime.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/time/package-info.java b/src/main/java/im/conversations/android/xmpp/model/time/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d86a2b9940e98832fcad1850a4e8e66ff0e0aa12 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/time/package-info.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/model/up/Push.java b/src/main/java/im/conversations/android/xmpp/model/up/Push.java new file mode 100644 index 0000000000000000000000000000000000000000..ab1365328c5962348f93a18ea4c382d47c3576f4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/up/Push.java @@ -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); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/up/package-info.java b/src/main/java/im/conversations/android/xmpp/model/up/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..fd230e3265d800d23ee2c933083f6d8333009c51 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/up/package-info.java @@ -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; diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 093ba8ce0e772901fc2a2be89d75aa76b61683a1..6092b0e8f406a6b979f3b88618efaa0ef114e7c2 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -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); diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index 54cfc5ee60f7357956aeb584b9cff6cdd8df4272..c144c628c2a8c8ea0692a93b1a41b54ec1a6bcf6 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -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 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 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; diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java index 021978dfa354e0edb4e3debbda0c89d7978d267e..7ea843ddebce14de07aa642c8571c8911b4b9f31 100644 --- a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -82,7 +82,7 @@ public class EntityCapabilitiesTest { } @Test - public void entityCapsOpenFire() throws IOException { + public void entityCapsOpenFireOrg() throws IOException { final String xml = """ @@ -206,6 +206,104 @@ public class EntityCapabilitiesTest { Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var); } + @Test + public void entityCapsOpenFireTestServer() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://jabber.org/network/serverinfo + + + xmpp:admin@example.org + mailto:admin@example.com + + + + + urn:xmpp:dataforms:softwareinfo + + + Linux + + + 6.8.0-59-generic amd64 - Java 21.0.7 + + + Openfire + + + 5.0.0 Alpha + + + + +"""; + 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 =