From 30ead505ce2e7dc3ea178dcbe20491f42cd5e94f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 May 2025 14:28:41 +0200 Subject: [PATCH 1/5] respond to disco#info queries via DiscoManager --- conversations.doap | 7 + .../eu/siacs/conversations/AppSettings.java | 12 ++ .../java/eu/siacs/conversations/Config.java | 2 + .../generator/AbstractGenerator.java | 114 +-------------- .../conversations/generator/IqGenerator.java | 16 --- .../generator/PresenceGenerator.java | 24 +--- .../siacs/conversations/parser/IqParser.java | 13 +- .../services/XmppConnectionService.java | 9 +- .../conversations/xmpp/XmppConnection.java | 35 +++++ .../xmpp/manager/DiscoManager.java | 131 +++++++++++++++++- .../xmpp/manager/PresenceManager.java | 58 ++++++++ .../android/xmpp/ServiceDescription.java | 49 +++++++ src/main/res/values-sw600dp/defaults.xml | 3 +- src/main/res/values-sw600dp/device.xml | 4 + src/main/res/values/defaults.xml | 1 - src/main/res/values/device.xml | 4 + 16 files changed, 318 insertions(+), 164 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java create mode 100644 src/main/java/im/conversations/android/xmpp/ServiceDescription.java create mode 100644 src/main/res/values-sw600dp/device.xml create mode 100644 src/main/res/values/device.xml diff --git a/conversations.doap b/conversations.doap index e402ff875d9811b2e758457e9aceaa4b5a441ef4..71f508bc889eab383b735fad06a9134fef5a019b 100644 --- a/conversations.doap +++ b/conversations.doap @@ -399,6 +399,13 @@ 0.4.0 + + + + complete + 0.3.2 + + diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index 40aad2758609f26e4397e3cbed172d18b5cc2026..7318091ea0d97689fa4b2824f03070435a39b434 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/src/main/java/eu/siacs/conversations/AppSettings.java @@ -130,6 +130,18 @@ public class AppSettings { return getBooleanPreference(ALIGN_START, R.bool.align_start); } + public boolean isConfirmMessages() { + return getBooleanPreference(CONFIRM_MESSAGES, R.bool.confirm_messages); + } + + public boolean isAllowMessageCorrection() { + return getBooleanPreference(ALLOW_MESSAGE_CORRECTION, R.bool.allow_message_correction); + } + + public boolean isBroadcastLastActivity() { + return getBooleanPreference(BROADCAST_LAST_ACTIVITY, R.bool.last_activity); + } + public boolean isUseTor() { return QuickConversationsService.isConversations() && getBooleanPreference(USE_TOR, R.bool.use_tor); diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 628b3c3531b389c3f6dcd623bd241f2d61e1e519..571c02c348de4da5bcac6dea2c1e0a52323f47a0 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -114,6 +114,8 @@ public final class Config { public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true; public static final boolean USE_JINGLE_MESSAGE_INIT = true; + public static final boolean ENABLE_CAPS_CACHE = true; + public static final boolean DISABLE_HTTP_UPLOAD = false; public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts public static final boolean BACKGROUND_STANZA_LOGGING = diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index de092f5978126a75e42ae4298b6c95bb712e4f58..34018f6b85f7a91119ac236057c84f4a30287f7d 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -1,61 +1,15 @@ package eu.siacs.conversations.generator; -import android.util.Base64; import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.XmppConnection; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Locale; import java.util.TimeZone; public abstract class AbstractGenerator { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - private final String[] STATIC_FEATURES = { - Namespace.JINGLE, - Namespace.JINGLE_APPS_FILE_TRANSFER, - Namespace.JINGLE_TRANSPORTS_S5B, - Namespace.JINGLE_TRANSPORTS_IBB, - Namespace.JINGLE_ENCRYPTED_TRANSPORT, - Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, - "http://jabber.org/protocol/muc", - "jabber:x:conference", - Namespace.OOB, - "http://jabber.org/protocol/caps", - "http://jabber.org/protocol/disco#info", - "urn:xmpp:avatar:metadata+notify", - Namespace.NICK + "+notify", - "urn:xmpp:ping", - "jabber:iq:version", - "http://jabber.org/protocol/chatstates", - Namespace.REACTIONS - }; - private final String[] MESSAGE_CONFIRMATION_FEATURES = { - "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" - }; - private final String[] MESSAGE_CORRECTION_FEATURES = {"urn:xmpp:message-correct:0"}; - private final String[] PRIVACY_SENSITIVE = { - "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone - }; - private final String[] VOIP_NAMESPACES = { - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS, - Namespace.JINGLE_MESSAGE - }; + protected XmppConnectionService mXmppConnectionService; AbstractGenerator(XmppConnectionService service) { @@ -70,70 +24,4 @@ public abstract class AbstractGenerator { String getIdentityVersion() { return BuildConfig.VERSION_NAME; } - - String getIdentityName() { - return BuildConfig.APP_NAME; - } - - String getIdentityType() { - if ("chromium".equals(android.os.Build.BRAND)) { - return "pc"; - } else { - return mXmppConnectionService.getString(R.string.default_resource).toLowerCase(); - } - } - - String getCapHash(final Account account) { - StringBuilder s = new StringBuilder(); - s.append("client/") - .append(getIdentityType()) - .append("//") - .append(getIdentityName()) - .append('<'); - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - - for (String feature : getFeatures(account)) { - s.append(feature).append('<'); - } - final byte[] sha1 = md.digest(s.toString().getBytes()); - return Base64.encodeToString(sha1, Base64.NO_WRAP); - } - - public List getFeatures(final Account account) { - final XmppConnection connection = account.getXmppConnection(); - final ArrayList features = new ArrayList<>(Arrays.asList(STATIC_FEATURES)); - if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) { - features.add(Namespace.MDS_DISPLAYED + "+notify"); - } - if (mXmppConnectionService.confirmMessages()) { - features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); - } - if (mXmppConnectionService.allowMessageCorrection()) { - features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); - } - if (Config.supportOmemo()) { - features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY); - } - if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) { - features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); - features.addAll(Arrays.asList(VOIP_NAMESPACES)); - features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL); - } - if (mXmppConnectionService.broadcastLastActivity()) { - features.add(Namespace.IDLE); - } - if (connection != null && connection.getFeatures().bookmarks2()) { - features.add(Namespace.BOOKMARKS2 + "+notify"); - } else { - features.add(Namespace.BOOKMARKS + "+notify"); - } - - Collections.sort(features); - return features; - } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 60da01c49a82514d04274e69dd5e25966b246e9c..e2b2638efbab5563079a606d68afaf4cf05fd7e2 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -39,22 +39,6 @@ public class IqGenerator extends AbstractGenerator { super(service); } - public Iq discoResponse(final Account account, final Iq request) { - final var packet = new Iq(Iq.Type.RESULT); - packet.setId(request.getId()); - packet.setTo(request.getFrom()); - final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); - query.setAttribute("node", request.query().getAttribute("node")); - final Element identity = query.addChild("identity"); - identity.setAttribute("category", "client"); - identity.setAttribute("type", getIdentityType()); - identity.setAttribute("name", getIdentityName()); - for (final String feature : getFeatures(account)) { - query.addChild("feature").setAttribute("var", feature); - } - return packet; - } - public Iq versionResponse(final Iq request) { final var packet = request.generateResponse(Iq.Type.RESULT); Element query = packet.query("jabber:iq:version"); diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 9fca46bd2fca91c8c0d144b0386a8c15ce9888f4..60fa01872680da3e9e2248e6b9056887dbc9a1e3 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -5,8 +5,8 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.model.stanza.Presence; public class PresenceGenerator extends AbstractGenerator { @@ -66,25 +66,11 @@ public class PresenceGenerator extends AbstractGenerator { public im.conversations.android.xmpp.model.stanza.Presence selfPresence( final Account account, final Presence.Availability status, final boolean personal) { - final im.conversations.android.xmpp.model.stanza.Presence packet = - new im.conversations.android.xmpp.model.stanza.Presence(); - if (personal) { - final String sig = account.getPgpSignature(); - final String message = account.getPresenceStatusMessage(); - packet.setAvailability(status); - packet.setStatus(message); - if (sig != null && mXmppConnectionService.getPgpEngine() != null) { - packet.addChild("x", "jabber:x:signed").setContent(sig); - } - } - final String capHash = getCapHash(account); - if (capHash != null) { - Element cap = packet.addChild("c", "http://jabber.org/protocol/caps"); - cap.setAttribute("hash", "sha-1"); - cap.setAttribute("node", "http://conversations.im"); - cap.setAttribute("ver", capHash); + final var connection = account.getXmppConnection(); + if (connection == null) { + return new Presence(); } - return packet; + return connection.getManager(PresenceManager.class).getPresence(status, personal); } public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 7a7f9c3a0aa031e431991ef9398cde8b02e1d9c1..37809e5c4402145ee42ecd54bf0ab329f4b8aefb 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -17,6 +17,8 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Iq; import java.io.ByteArrayInputStream; import java.security.cert.CertificateException; @@ -412,6 +414,7 @@ public class IqParser extends AbstractParser implements Consumer { @Override public void accept(final Iq packet) { + final var connection = account.getXmppConnection(); final boolean isGet = packet.getType() == Iq.Type.GET; if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) { return; @@ -439,7 +442,7 @@ public class IqParser extends AbstractParser implements Consumer { // Otherwise, just update the existing blocklist. if (packet.getType() == Iq.Type.RESULT) { account.clearBlocklist(); - account.getXmppConnection().getFeatures().setBlockListRequested(true); + connection.getFeatures().setBlockListRequested(true); } if (items != null) { final Collection jids = new ArrayList<>(items.size()); @@ -499,10 +502,8 @@ public class IqParser extends AbstractParser implements Consumer { || packet.hasChild("data", "http://jabber.org/protocol/ibb") || packet.hasChild("close", "http://jabber.org/protocol/ibb")) { mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet); - } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - final Iq response = - mXmppConnectionService.getIqGenerator().discoResponse(account, packet); - mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasExtension(InfoQuery.class)) { + connection.getManager(DiscoManager.class).handleInfoQuery(packet); } else if (packet.hasChild("query", "jabber:iq:version") && isGet) { final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet); mXmppConnectionService.sendIqPacket(account, response, null); @@ -545,7 +546,7 @@ public class IqParser extends AbstractParser implements Consumer { final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas"); - account.getXmppConnection().sendIqPacket(response, null); + connection.sendIqPacket(response, null); } } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 5ec73cc419c4ecf4064aa1a9bc789f97ea05afe9..0181a2f76509df3ffd6f63a58c061379cabe5e61 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -5527,11 +5527,11 @@ public class XmppConnectionService extends Service { } public boolean confirmMessages() { - return getBooleanPreference("confirm_messages", R.bool.confirm_messages); + return appSettings.isConfirmMessages(); } public boolean allowMessageCorrection() { - return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction); + return appSettings.isAllowMessageCorrection(); } public boolean sendChatStates() { @@ -5539,12 +5539,11 @@ public class XmppConnectionService extends Service { } public boolean useTorToConnect() { - return QuickConversationsService.isConversations() - && getBooleanPreference("use_tor", R.bool.use_tor); + return appSettings.isUseTor(); } public boolean broadcastLastActivity() { - return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity); + return appSettings.isBroadcastLastActivity(); } public int unreadCount() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 987ffb1b0330f153d7179084ef2f83851c639774..5ddce911ef6ae77c8b8c5809c319c22692b0d910 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -74,10 +74,12 @@ 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.DiscoManager; +import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.AuthenticationFailure; import im.conversations.android.xmpp.model.AuthenticationRequest; import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.StreamElement; import im.conversations.android.xmpp.model.bind2.Bind; import im.conversations.android.xmpp.model.bind2.Bound; @@ -224,6 +226,9 @@ public class XmppConnection implements Runnable { .put( DiscoManager.class, new DiscoManager(service.getApplicationContext(), this)) + .put( + PresenceManager.class, + new PresenceManager(service.getApplicationContext(), this)) .build(); } @@ -2562,6 +2567,36 @@ public class XmppConnection implements Runnable { return packet.getId(); } + public void sendResultFor(final Iq request, final Extension... extensions) { + final var from = request.getFrom(); + final var id = request.getId(); + final var response = new Iq(Iq.Type.RESULT); + response.setTo(from); + response.setId(id); + for (final Extension extension : extensions) { + response.addExtension(extension); + } + this.sendPacket(response); + } + + public void sendErrorFor( + final Iq request, + final im.conversations.android.xmpp.model.error.Error.Type type, + final Condition condition, + final im.conversations.android.xmpp.model.error.Error.Extension... extensions) { + final var from = request.getFrom(); + final var id = request.getId(); + final var response = new Iq(Iq.Type.ERROR); + response.setTo(from); + response.setId(id); + final var error = + response.addExtension(new im.conversations.android.xmpp.model.error.Error()); + error.setType(type); + error.setCondition(condition); + error.addExtensions(extensions); + this.sendPacket(response); + } + public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) { this.sendPacket(packet); } 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 e773d617bcd8c0f0dcab8794fc0a94788b7ad2dd..c615010a58ebeb63af092eadb651d31604a53740 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -4,26 +4,35 @@ import android.content.Context; import android.util.Log; import com.google.common.base.Strings; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.io.BaseEncoding; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.EntityCapabilities; import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.ServiceDescription; import im.conversations.android.xmpp.model.Hash; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.disco.items.Item; import im.conversations.android.xmpp.model.disco.items.ItemsQuery; +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.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +44,43 @@ public class DiscoManager extends AbstractManager { public static final String CAPABILITY_NODE = "http://conversations.im"; + private final List STATIC_FEATURES = + Arrays.asList( + Namespace.JINGLE, + Namespace.JINGLE_APPS_FILE_TRANSFER, + Namespace.JINGLE_TRANSPORTS_S5B, + Namespace.JINGLE_TRANSPORTS_IBB, + Namespace.JINGLE_ENCRYPTED_TRANSPORT, + Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO, + "http://jabber.org/protocol/muc", + "jabber:x:conference", + Namespace.OOB, + Namespace.ENTITY_CAPABILITIES, + Namespace.ENTITY_CAPABILITIES_2, + Namespace.DISCO_INFO, + "urn:xmpp:avatar:metadata+notify", + Namespace.NICK + "+notify", + Namespace.PING, + Namespace.VERSION, + Namespace.CHAT_STATES, + Namespace.REACTIONS); + private final List MESSAGE_CONFIRMATION_FEATURES = + Arrays.asList(Namespace.CHAT_MARKERS, Namespace.DELIVERY_RECEIPTS); + private final List MESSAGE_CORRECTION_FEATURES = + Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION); + private final List PRIVACY_SENSITIVE = + Collections.singletonList( + "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone + ); + private final List VOIP_NAMESPACES = + Arrays.asList( + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_FEATURE_AUDIO, + Namespace.JINGLE_FEATURE_VIDEO, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS, + Namespace.JINGLE_MESSAGE); + // this is the runtime cache that stores disco information for all entities seen during a // session @@ -92,7 +138,7 @@ public class DiscoManager extends AbstractManager { public ListenableFuture infoOrCache( final Entity entity, final String node, final EntityCapabilities.Hash hash) { final var cached = getDatabase().getInfoQuery(hash); - if (cached != null) { + if (cached != null && Config.ENABLE_CAPS_CACHE) { if (node == null || hash != null) { this.put(entity.address, cached); } @@ -109,7 +155,7 @@ public class DiscoManager extends AbstractManager { public ListenableFuture info( final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) { - final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node; + final var requestNode = hash != null ? hash.capabilityNode(node) : node; final var iqRequest = new Iq(Iq.Type.GET); iqRequest.setTo(entity.address); final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery()); @@ -260,6 +306,87 @@ public class DiscoManager extends AbstractManager { MoreExecutors.directExecutor()); } + ServiceDescription getServiceDescription() { + final var appSettings = new AppSettings(context); + final var account = connection.getAccount(); + final ImmutableList.Builder features = ImmutableList.builder(); + if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) { + features.add(Namespace.MDS_DISPLAYED + "+notify"); + } + if (appSettings.isConfirmMessages()) { + features.addAll(MESSAGE_CONFIRMATION_FEATURES); + } + if (appSettings.isAllowMessageCorrection()) { + features.addAll(MESSAGE_CORRECTION_FEATURES); + } + if (Config.supportOmemo()) { + features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY); + } + if (!appSettings.isUseTor() && !account.isOnion()) { + features.addAll(PRIVACY_SENSITIVE); + features.addAll(VOIP_NAMESPACES); + features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL); + } + if (appSettings.isBroadcastLastActivity()) { + features.add(Namespace.IDLE); + } + if (connection.getFeatures().bookmarks2()) { + features.add(Namespace.BOOKMARKS2 + "+notify"); + } else { + features.add(Namespace.BOOKMARKS + "+notify"); + } + return new ServiceDescription( + features.build(), + new ServiceDescription.Identity(BuildConfig.APP_NAME, "client", getIdentityType())); + } + + String getIdentityVersion() { + return BuildConfig.VERSION_NAME; + } + + String getIdentityType() { + if ("chromium".equals(android.os.Build.BRAND)) { + return "pc"; + } else if (context.getResources().getBoolean(R.bool.is_device_table)) { + return "tablet"; + } else { + return "phone"; + } + } + + public void handleInfoQuery(final Iq request) { + final var infoQueryRequest = request.getExtension(InfoQuery.class); + final var nodeRequest = infoQueryRequest.getNode(); + final ServiceDescription serviceDescription; + if (Strings.isNullOrEmpty(nodeRequest)) { + serviceDescription = getServiceDescription(); + Log.d(Config.LOGTAG, "responding to disco request w/o node from " + request.getFrom()); + } else { + final var hash = buildHashFromNode(nodeRequest); + final var cachedServiceDescription = + hash != null + ? getManager(PresenceManager.class).getCachedServiceDescription(hash) + : null; + if (cachedServiceDescription != null) { + Log.d( + Config.LOGTAG, + "responding to disco request from " + + request.getFrom() + + " to node " + + nodeRequest + + " using hash " + + hash.getClass().getName()); + serviceDescription = cachedServiceDescription; + } else { + connection.sendErrorFor(request, Error.Type.CANCEL, new Condition.ItemNotFound()); + return; + } + } + final var infoQuery = serviceDescription.asInfoQuery(); + infoQuery.setNode(nodeRequest); + connection.sendResultFor(request, infoQuery); + } + public Map getServerItems() { final var builder = new ImmutableMap.Builder(); final var domain = connection.getAccount().getDomain(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..14de9f576f8590bb886091717411d5005af247b3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java @@ -0,0 +1,58 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.ServiceDescription; +import im.conversations.android.xmpp.model.capabilties.Capabilities; +import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities; +import im.conversations.android.xmpp.model.pgp.Signed; +import im.conversations.android.xmpp.model.stanza.Presence; +import java.util.HashMap; +import java.util.Map; + +public class PresenceManager extends AbstractManager { + + private final Map serviceDescriptions = + new HashMap<>(); + + public PresenceManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public Presence getPresence(final Presence.Availability availability, final boolean personal) { + final var account = connection.getAccount(); + final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription(); + final var infoQuery = serviceDiscoveryFeatures.asInfoQuery(); + final var capsHash = EntityCapabilities.hash(infoQuery); + final var caps2Hash = EntityCapabilities2.hash(infoQuery); + serviceDescriptions.put(capsHash, serviceDiscoveryFeatures); + serviceDescriptions.put(caps2Hash, serviceDiscoveryFeatures); + final var capabilities = new Capabilities(); + capabilities.setHash(caps2Hash); + final var legacyCapabilities = new LegacyCapabilities(); + legacyCapabilities.setNode(DiscoManager.CAPABILITY_NODE); + legacyCapabilities.setHash(capsHash); + final var presence = new Presence(); + presence.addExtension(capabilities); + presence.addExtension(legacyCapabilities); + + if (personal) { + final String pgpSignature = account.getPgpSignature(); + final String message = account.getPresenceStatusMessage(); + presence.setAvailability(availability); + presence.setStatus(message); + if (pgpSignature != null) { + final var signed = new Signed(); + signed.setContent(pgpSignature); + presence.addExtension(new Signed()); + } + } + return presence; + } + + public ServiceDescription getCachedServiceDescription(final EntityCapabilities.Hash hash) { + return this.serviceDescriptions.get(hash); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/ServiceDescription.java b/src/main/java/im/conversations/android/xmpp/ServiceDescription.java new file mode 100644 index 0000000000000000000000000000000000000000..283a0a615650ffd92d2197733af79bc4d3173082 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/ServiceDescription.java @@ -0,0 +1,49 @@ +package im.conversations.android.xmpp; + +import com.google.common.collect.Collections2; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.util.Collection; +import java.util.List; + +public class ServiceDescription { + public final List features; + public final Identity identity; + + public ServiceDescription(List features, Identity identity) { + this.features = features; + this.identity = identity; + } + + public InfoQuery asInfoQuery() { + final var infoQuery = new InfoQuery(); + final Collection features = + Collections2.transform( + this.features, + sf -> { + final var feature = new Feature(); + feature.setVar(sf); + return feature; + }); + infoQuery.addExtensions(features); + final var identity = + infoQuery.addExtension( + new im.conversations.android.xmpp.model.disco.info.Identity()); + identity.setIdentityName(this.identity.name); + identity.setCategory(this.identity.category); + identity.setType(this.identity.type); + return infoQuery; + } + + public static class Identity { + public final String name; + public final String category; + public final String type; + + public Identity(String name, String category, String type) { + this.name = name; + this.category = category; + this.type = type; + } + } +} diff --git a/src/main/res/values-sw600dp/defaults.xml b/src/main/res/values-sw600dp/defaults.xml index b22a4ca8ab3718760fcdbe08a6b05ef59155fd43..c06e0147ee732340df3dba9567aa90076c64aa47 100644 --- a/src/main/res/values-sw600dp/defaults.xml +++ b/src/main/res/values-sw600dp/defaults.xml @@ -1,5 +1,4 @@ - Tablet false - \ No newline at end of file + diff --git a/src/main/res/values-sw600dp/device.xml b/src/main/res/values-sw600dp/device.xml new file mode 100644 index 0000000000000000000000000000000000000000..76b11950560dd1b7d881600c0372ff3d4ecd7563 --- /dev/null +++ b/src/main/res/values-sw600dp/device.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml index eb01b23a3d1c02cdb2eae30b495dc8e09e15e749..1f1b87c4732421f88da703a160bf1df3275c9094 100644 --- a/src/main/res/values/defaults.xml +++ b/src/main/res/values/defaults.xml @@ -1,6 +1,5 @@ - Phone true false true diff --git a/src/main/res/values/device.xml b/src/main/res/values/device.xml new file mode 100644 index 0000000000000000000000000000000000000000..f99fe3e59674a1876bf0e12233b89077de641667 --- /dev/null +++ b/src/main/res/values/device.xml @@ -0,0 +1,4 @@ + + + false + From 8f3531d24df50054f3ed25e5c6c0d958f6139691 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 May 2025 19:59:30 +0200 Subject: [PATCH 2/5] refactor channel discovery to use new APIs --- .../eu/siacs/conversations/entities/Room.java | 52 +++-- .../conversations/generator/IqGenerator.java | 14 -- .../siacs/conversations/parser/IqParser.java | 35 ---- .../services/ChannelDiscoveryService.java | 178 ++++++++++++------ .../services/XmppConnectionService.java | 3 +- .../android/xmpp/model/data/Data.java | 5 + 6 files changed, 163 insertions(+), 124 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Room.java b/src/main/java/eu/siacs/conversations/entities/Room.java index c702c3189bfde4306b067b8448b718d6ab173731..88b2e7485a5faf0bcf6d47f4bba988c0e1565529 100644 --- a/src/main/java/eu/siacs/conversations/entities/Room.java +++ b/src/main/java/eu/siacs/conversations/entities/Room.java @@ -3,31 +3,42 @@ package eu.siacs.conversations.entities; import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.utils.LanguageUtils; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; public class Room implements AvatarService.Avatarable, Comparable { - public String address; - public String name; - public String description; - public String language; - public int nusers; + public final String address; + public final String name; + public final String description; + public final String language; + public final int numberOfUsers; - public Room(String address, String name, String description, String language, int nusers) { + public Room( + final String address, + final String name, + final String description, + final String language, + final Integer numberOfUsers) { this.address = address; this.name = name; this.description = description; this.language = language; - this.nusers = nusers; + this.numberOfUsers = numberOfUsers == null ? 0 : numberOfUsers; } - public Room() {} - public String getName() { - return name; + if (Strings.isNullOrEmpty(name)) { + final var jid = Jid.ofOrInvalid(address); + return jid.getLocal(); + } else { + return name; + } } public String getDescription() { @@ -81,9 +92,28 @@ public class Room implements AvatarService.Avatarable, Comparable { @Override public int compareTo(Room o) { return ComparisonChain.start() - .compare(o.nusers, nusers) + .compare(o.numberOfUsers, numberOfUsers) .compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name)) .compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address)) .result(); } + + public static Room of(final Jid address, InfoQuery query) { + final var identity = Iterables.getFirst(query.getIdentities(), null); + final var ri = + query.getServiceDiscoveryExtension("http://jabber.org/protocol/muc#roominfo"); + final String name = identity == null ? null : identity.getIdentityName(); + String roomName = ri == null ? null : ri.getValue("muc#roomconfig_roomname"); + String description = ri == null ? null : ri.getValue("muc#roominfo_description"); + String language = ri == null ? null : ri.getValue("muc#roominfo_lang"); + String occupants = ri == null ? null : ri.getValue("muc#roominfo_occupants"); + final Integer numberOfUsers = Ints.tryParse(Strings.nullToEmpty(occupants)); + + return new Room( + address.toString(), + Strings.isNullOrEmpty(roomName) ? name : roomName, + description, + language, + numberOfUsers); + } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index e2b2638efbab5563079a606d68afaf4cf05fd7e2..5d219b52fb5a46b64b165b9ec3465c95147313f4 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -577,18 +577,4 @@ public class IqGenerator extends AbstractGenerator { } return packet; } - - public Iq queryDiscoItems(final Jid jid) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(jid); - packet.addChild("query", Namespace.DISCO_ITEMS); - return packet; - } - - public Iq queryDiscoInfo(final Jid jid) { - final Iq packet = new Iq(Iq.Type.GET); - packet.setTo(jid); - packet.addChild("query", Namespace.DISCO_INFO); - return packet; - } } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 37809e5c4402145ee42ecd54bf0ab329f4b8aefb..f711c3100254632d541bde58da5205ef87d77569 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.parser; -import android.text.TextUtils; import android.util.Log; import android.util.Pair; import androidx.annotation.NonNull; @@ -10,13 +9,11 @@ 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.entities.Room; 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.forms.Data; import eu.siacs.conversations.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Iq; @@ -61,38 +58,6 @@ public class IqParser extends AbstractParser implements Consumer { return items; } - public static Room parseRoom(Iq packet) { - final Element query = packet.findChild("query", Namespace.DISCO_INFO); - if (query == null) { - return null; - } - final Element x = query.findChild("x"); - if (x == null) { - return null; - } - final Element identity = query.findChild("identity"); - Data data = Data.parse(x); - String address = packet.getFrom().toString(); - String name = identity == null ? null : identity.getAttribute("name"); - String roomName = data.getValue("muc#roomconfig_roomname"); - String description = data.getValue("muc#roominfo_description"); - String language = data.getValue("muc#roominfo_lang"); - String occupants = data.getValue("muc#roominfo_occupants"); - int nusers; - try { - nusers = occupants == null ? 0 : Integer.parseInt(occupants); - } catch (NumberFormatException e) { - nusers = 0; - } - - return new Room( - address, - TextUtils.isEmpty(roomName) ? name : roomName, - description, - language, - nusers); - } - private void rosterItems(final Account account, final Element query) { final String version = query.getAttribute("ver"); if (version != null) { diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 9e09e56e962c40cf9c34f069f61bc6902ce16e1a..8d53b61f1a7905e98ce787dd5a943e328a0c013d 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -5,24 +5,33 @@ import androidx.annotation.NonNull; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Ordering; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; -import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.disco.items.Item; +import im.conversations.android.xmpp.model.disco.items.ItemsQuery; import im.conversations.android.xmpp.model.stanza.Iq; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; import retrofit2.Call; @@ -164,7 +173,7 @@ public class ChannelDiscoveryService { private void discoverChannelsLocalServers( final String query, final OnChannelSearchResultsFound listener) { - final Map localMucService = getLocalMucServices(); + final var localMucService = getLocalMucServices(); Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); if (localMucService.isEmpty()) { listener.onChannelSearchResultsFound(Collections.emptyList()); @@ -178,57 +187,104 @@ public class ChannelDiscoveryService { listener.onChannelSearchResultsFound(results); } } - final AtomicInteger queriesInFlight = new AtomicInteger(); - final List rooms = new ArrayList<>(); - for (final Map.Entry entry : localMucService.entrySet()) { - Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); - queriesInFlight.incrementAndGet(); - final var account = entry.getValue(); - service.sendIqPacket( - account, - itemsRequest, - (itemsResponse) -> { - if (itemsResponse.getType() == Iq.Type.RESULT) { - final List items = IqParser.items(itemsResponse); - for (final Jid item : items) { - final Iq infoRequest = - service.getIqGenerator().queryDiscoInfo(item); - queriesInFlight.incrementAndGet(); - service.sendIqPacket( - account, - infoRequest, - infoResponse -> { - if (infoResponse.getType() == Iq.Type.RESULT) { - final Room room = IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - } else { - queriesInFlight.decrementAndGet(); - } - }); + final var roomsRoomsFuture = + Futures.successfulAsList( + Collections2.transform( + localMucService.entrySet(), + e -> discoverRooms(e.getValue(), e.getKey()))); + final var roomsFuture = + Futures.transform( + roomsRoomsFuture, + rooms -> { + final var builder = new ImmutableList.Builder(); + for (final var inner : rooms) { + if (inner == null) { + continue; + } + builder.addAll(inner); } - } - if (queriesInFlight.decrementAndGet() <= 0) { - finishDiscoSearch(rooms, query, listener); - } - }); - } + return builder.build(); + }, + MoreExecutors.directExecutor()); + Futures.addCallback( + roomsFuture, + new FutureCallback<>() { + @Override + public void onSuccess(ImmutableList rooms) { + finishDiscoSearch(rooms, query, listener); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "could not perform room search", throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture> discoverRooms( + final XmppConnection connection, final Jid server) { + final var request = new Iq(Iq.Type.GET); + request.addExtension(new ItemsQuery()); + request.setTo(server); + final ListenableFuture> itemsFuture = + Futures.transform( + connection.sendIqPacket(request), + iq -> { + final var itemsQuery = iq.getExtension(ItemsQuery.class); + if (itemsQuery == null) { + return Collections.emptyList(); + } + final var items = itemsQuery.getExtensions(Item.class); + return Collections2.filter(items, i -> Objects.nonNull(i.getJid())); + }, + MoreExecutors.directExecutor()); + final var roomsFutures = + Futures.transformAsync( + itemsFuture, + items -> { + final var infoFutures = + Collections2.transform( + items, i -> discoverRoom(connection, i.getJid())); + return Futures.successfulAsList(infoFutures); + }, + MoreExecutors.directExecutor()); + return Futures.transform( + roomsFutures, + rooms -> Collections2.filter(rooms, Objects::nonNull), + MoreExecutors.directExecutor()); + } + + private ListenableFuture discoverRoom(final XmppConnection connection, final Jid room) { + final var request = new Iq(Iq.Type.GET); + request.addExtension(new InfoQuery()); + request.setTo(room); + final var infoQueryResponseFuture = connection.sendIqPacket(request); + return Futures.transform( + infoQueryResponseFuture, + result -> { + final var infoQuery = result.getExtension(InfoQuery.class); + if (infoQuery == null) { + return null; + } + return Room.of(room, infoQuery); + }, + MoreExecutors.directExecutor()); } private void finishDiscoSearch( - List rooms, String query, OnChannelSearchResultsFound listener) { - Collections.sort(rooms); - cache.put(key(Method.LOCAL_SERVER, ""), rooms); + final List rooms, + final String query, + final OnChannelSearchResultsFound listener) { + Log.d(Config.LOGTAG, "finishDiscoSearch with " + rooms.size() + " rooms"); + final var sorted = Ordering.natural().sortedCopy(rooms); + cache.put(key(Method.LOCAL_SERVER, ""), sorted); if (query.isEmpty()) { - listener.onChannelSearchResultsFound(rooms); + listener.onChannelSearchResultsFound(sorted); } else { - List results = copyMatching(rooms, query); + List results = copyMatching(sorted, query); cache.put(key(Method.LOCAL_SERVER, query), results); - listener.onChannelSearchResultsFound(rooms); + listener.onChannelSearchResultsFound(sorted); } } @@ -242,23 +298,21 @@ public class ChannelDiscoveryService { return result; } - private Map getLocalMucServices() { - final HashMap localMucServices = new HashMap<>(); - for (Account account : service.getAccounts()) { - if (account.isEnabled()) { - final XmppConnection xmppConnection = account.getXmppConnection(); - if (xmppConnection == null) { - continue; - } - for (final String mucService : xmppConnection.getMucServers()) { - final Jid jid = Jid.of(mucService); - if (!localMucServices.containsKey(jid)) { - localMucServices.put(jid, account); + private Map getLocalMucServices() { + final ImmutableMap.Builder localMucServices = + new ImmutableMap.Builder<>(); + for (final var account : service.getAccounts()) { + final var connection = account.getXmppConnection(); + if (connection != null && account.isEnabled()) { + for (final String mucService : connection.getMucServers()) { + final Jid jid = Jid.ofOrInvalid(mucService); + if (Jid.Invalid.isValid(jid)) { + localMucServices.put(jid, connection); } } } } - return localMucServices; + return localMucServices.buildKeepingLast(); } private static String key(Method method, String query) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 0181a2f76509df3ffd6f63a58c061379cabe5e61..687339108ded64da19dad7423f569918ba82a2d8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4369,7 +4369,6 @@ public class XmppConnectionService extends Service { public void fetchConferenceConfiguration( final Conversation conversation, final OnConferenceConfigurationFetched callback) { - final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); final var account = conversation.getAccount(); final var connection = account.getXmppConnection(); if (connection == null) { @@ -4381,7 +4380,7 @@ public class XmppConnectionService extends Service { .info(Entity.discoItem(conversation.getJid().asBareJid()), null); Futures.addCallback( future, - new FutureCallback() { + new FutureCallback<>() { @Override public void onSuccess(InfoQuery result) { final MucOptions mucOptions = conversation.getMucOptions(); diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index 7fc03360d3d275ec27c20d7dc0df8b1987c4298a..d0d546b37bdf5f9b944966828b9719aeb3db6923 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -33,6 +33,11 @@ public class Data extends Extension { return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null); } + public String getValue(final String name) { + final var field = getFieldByName(name); + return field == null ? null : field.getValue(); + } + private void addField(final String name, final Object value) { addField(name, value, null); } From 041321a00e558bcd1c244192d6b047af9007ded4 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 May 2025 10:30:48 +0200 Subject: [PATCH 3/5] respond to version requests via DiscoManager --- .../conversations/generator/IqGenerator.java | 14 ----------- .../conversations/parser/AbstractParser.java | 8 +++--- .../siacs/conversations/parser/IqParser.java | 17 +++++++------ .../conversations/parser/MessageParser.java | 20 +++++++++------ .../conversations/parser/PresenceParser.java | 25 ++++++++++--------- .../eu/siacs/conversations/xmpp/Managers.java | 23 +++++++++++++++++ .../conversations/xmpp/XmppConnection.java | 20 ++++----------- .../xmpp/manager/DiscoManager.java | 14 +++++++++++ .../android/xmpp/processor/BindProcessor.java | 9 +++---- 9 files changed, 84 insertions(+), 66 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/Managers.java diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5d219b52fb5a46b64b165b9ec3465c95147313f4..5f3173f87ea2b7813e43a9ce017bf2f215e9659e 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -4,7 +4,6 @@ import android.os.Bundle; import android.util.Base64; import android.util.Log; import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; @@ -39,19 +38,6 @@ public class IqGenerator extends AbstractGenerator { super(service); } - public Iq versionResponse(final Iq request) { - final var packet = request.generateResponse(Iq.Type.RESULT); - Element query = packet.query("jabber:iq:version"); - query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name)); - query.addChild("version").setContent(getIdentityVersion()); - if ("chromium".equals(android.os.Build.BRAND)) { - query.addChild("os").setContent("Chrome OS"); - } else { - query.addChild("os").setContent("Android"); - } - return packet; - } - public Iq entityTimeResponse(final Iq request) { final Iq packet = request.generateResponse(Iq.Type.RESULT); Element time = packet.addChild("time", "urn:xmpp:time"); diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 8f5178d8e83ad08b77e63a8f2619afed2302d4ca..b520d29c6efeb85031140820114f51a6ef6e40ce 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -7,6 +7,7 @@ import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.model.stanza.Stanza; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -15,14 +16,13 @@ import java.util.Date; import java.util.List; import java.util.Locale; -public abstract class AbstractParser { +public abstract class AbstractParser extends XmppConnection.Delegate { protected final XmppConnectionService mXmppConnectionService; - protected final Account account; - protected AbstractParser(final XmppConnectionService service, final Account account) { + protected AbstractParser(final XmppConnectionService service, final XmppConnection connection) { + super(service.getApplicationContext(), connection); this.mXmppConnectionService = service; - this.account = account; } public static Long parseTimestamp(Element element, Long d) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index f711c3100254632d541bde58da5205ef87d77569..7f2d0d94fd378819522f0d7b7e1290b3173b1d03 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -14,9 +14,11 @@ 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.DiscoManager; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.version.Version; import java.io.ByteArrayInputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -37,8 +39,8 @@ import org.whispersystems.libsignal.state.PreKeyBundle; public class IqParser extends AbstractParser implements Consumer { - public IqParser(final XmppConnectionService service, final Account account) { - super(service, account); + public IqParser(final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); } public static List items(final Iq packet) { @@ -379,7 +381,7 @@ public class IqParser extends AbstractParser implements Consumer { @Override public void accept(final Iq packet) { - final var connection = account.getXmppConnection(); + final var account = getAccount(); final boolean isGet = packet.getType() == Iq.Type.GET; if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) { return; @@ -467,11 +469,10 @@ public class IqParser extends AbstractParser implements Consumer { || 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)) { - connection.getManager(DiscoManager.class).handleInfoQuery(packet); - } else if (packet.hasChild("query", "jabber:iq:version") && isGet) { - final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet); - mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasExtension(InfoQuery.class) && isGet) { + this.getManager(DiscoManager.class).handleInfoQuery(packet); + } else if (packet.hasExtension(Version.class) && isGet) { + 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); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index ecf20048ce57c285c6be3b369369e7cce7dc0027..c287a927cd08c1419bf923320a5ea79e75e8a42f 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -33,6 +33,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +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; @@ -79,8 +80,8 @@ public class MessageParser extends AbstractParser private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish"); - public MessageParser(final XmppConnectionService service, final Account account) { - super(service, account); + public MessageParser(final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); } private static String extractStanzaId( @@ -489,6 +490,7 @@ public class MessageParser extends AbstractParser @Override public void accept(final im.conversations.android.xmpp.model.stanza.Message original) { + final var account = connection.getAccount(); if (handleErrorMessage(account, original)) { return; } @@ -510,8 +512,7 @@ public class MessageParser extends AbstractParser queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId); - final boolean offlineMessagesRetrieved = - account.getXmppConnection().isOfflineMessagesRetrieved(); + final boolean offlineMessagesRetrieved = connection.isOfflineMessagesRetrieved(); if (query != null && query.validFrom(original.getFrom())) { final var f = getForwardedMessagePacket(original, "result", query.version.namespace); if (f == null) { @@ -692,7 +693,7 @@ public class MessageParser extends AbstractParser final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null - || account.getXmppConnection() + || connection .getMucServersWithholdAccount() .contains(counterpart.getDomain().toString()); final Conversation conversation = @@ -1445,6 +1446,7 @@ public class MessageParser extends AbstractParser final im.conversations.android.xmpp.model.stanza.Message packet, final MessageArchiveService.Query query, final Jid from) { + final var account = this.connection.getAccount(); final var id = received.getId(); if (packet.fromAccount(account)) { if (query != null && id != null && packet.getTo() != null) { @@ -1475,9 +1477,10 @@ public class MessageParser extends AbstractParser final Jid counterpart, final MessageArchiveService.Query query, final boolean isTypeGroupChat, - Conversation conversation, - Element mucUserElement, - Jid from) { + final Conversation conversation, + final Element mucUserElement, + final Jid from) { + final var account = getAccount(); final var id = displayed.getId(); // TODO we don’t even use 'sender' any more. Remove this! final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender")); @@ -1565,6 +1568,7 @@ public class MessageParser extends AbstractParser final Jid counterpart, final Jid mucTrueCounterPart, final im.conversations.android.xmpp.model.stanza.Message packet) { + final var account = getAccount(); final String reactingTo = reactions.getId(); if (conversation != null && reactingTo != null) { if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index d3c269f965ee0dfdc6cf864178cb1a78f4a892dc..57511058efe03628fae9e48d1ec9be6f6c037988 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -22,6 +22,7 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; 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.pep.Avatar; import im.conversations.android.xmpp.Entity; @@ -35,12 +36,13 @@ import org.openintents.openpgp.util.OpenPgpUtils; public class PresenceParser extends AbstractParser implements Consumer { - public PresenceParser(final XmppConnectionService service, final Account account) { - super(service, account); + public PresenceParser(final XmppConnectionService service, final XmppConnection connection) { + super(service, connection); } public void parseConferencePresence( - final im.conversations.android.xmpp.model.stanza.Presence packet, Account account) { + final im.conversations.android.xmpp.model.stanza.Presence packet) { + final var account = getAccount(); final Conversation conversation = packet.getFrom() == null ? null @@ -325,8 +327,8 @@ public class PresenceParser extends AbstractParser } private void parseContactPresence( - final im.conversations.android.xmpp.model.stanza.Presence packet, - final Account account) { + final im.conversations.android.xmpp.model.stanza.Presence packet) { + final var account = getAccount(); final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); final Jid from = packet.getFrom(); if (from == null || from.equals(account.getJid())) { @@ -372,8 +374,7 @@ public class PresenceParser extends AbstractParser final var connection = account.getXmppConnection(); if (nodeHash != null && connection != null) { final var discoFuture = - connection - .getManager(DiscoManager.class) + this.getManager(DiscoManager.class) .infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash); logDiscoFailure(from, discoFuture); @@ -486,14 +487,14 @@ public class PresenceParser extends AbstractParser @Override public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { if (packet.hasChild("x", Namespace.MUC_USER)) { - this.parseConferencePresence(packet, account); + this.parseConferencePresence(packet); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { - this.parseConferencePresence(packet, account); + this.parseConferencePresence(packet); } else if ("error".equals(packet.getAttribute("type")) - && mXmppConnectionService.isMuc(account, packet.getFrom())) { - this.parseConferencePresence(packet, account); + && mXmppConnectionService.isMuc(getAccount(), packet.getFrom())) { + this.parseConferencePresence(packet); } else { - this.parseContactPresence(packet, account); + this.parseContactPresence(packet); } } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java new file mode 100644 index 0000000000000000000000000000000000000000..306d6df593460e58abcd3e924cc00b115be808af --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -0,0 +1,23 @@ +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.xmpp.manager.AbstractManager; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.PresenceManager; + +public class Managers { + + private Managers() { + throw new AssertionError("Do not instantiate me"); + } + + public static ClassToInstanceMap get( + final Context context, final XmppConnection connection) { + return new ImmutableClassToInstanceMap.Builder() + .put(DiscoManager.class, new DiscoManager(context, connection)) + .put(PresenceManager.class, new PresenceManager(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 5ddce911ef6ae77c8b8c5809c319c22692b0d910..f5d34ca26314a7f58bdd53b3060536602e475930 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -20,7 +20,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ClassToInstanceMap; -import com.google.common.collect.ImmutableClassToInstanceMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; @@ -74,7 +73,6 @@ 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.DiscoManager; -import eu.siacs.conversations.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.AuthenticationFailure; import im.conversations.android.xmpp.model.AuthenticationRequest; @@ -217,19 +215,11 @@ public class XmppConnection implements Runnable { this.account = account; this.mXmppConnectionService = service; this.appSettings = mXmppConnectionService.getAppSettings(); - this.presenceListener = new PresenceParser(service, account); - this.unregisteredIqListener = new IqParser(service, account); - this.messageListener = new MessageParser(service, account); - this.bindListener = new BindProcessor(service, account); - this.managers = - new ImmutableClassToInstanceMap.Builder() - .put( - DiscoManager.class, - new DiscoManager(service.getApplicationContext(), this)) - .put( - PresenceManager.class, - new PresenceManager(service.getApplicationContext(), this)) - .build(); + this.presenceListener = new PresenceParser(service, this); + 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); } private static void fixResource(final Context context, final Account account) { 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 c615010a58ebeb63af092eadb651d31604a53740..b5776c0aff718d40bc03bbf22eba5bf1f0b19786 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -30,6 +30,7 @@ import im.conversations.android.xmpp.model.disco.items.ItemsQuery; 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.version.Version; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -354,6 +355,19 @@ public class DiscoManager extends AbstractManager { } } + public void handleVersionRequest(final Iq request) { + final var version = new Version(); + version.setSoftwareName(context.getString(R.string.app_name)); + version.setVersion(getIdentityVersion()); + if ("chromium".equals(android.os.Build.BRAND)) { + version.setOs("Chrome OS"); + } else { + version.setOs("Android"); + } + Log.d(Config.LOGTAG, "responding to version request from " + request.getFrom()); + connection.sendResultFor(request, version); + } + public void handleInfoQuery(final Iq request) { final var infoQueryRequest = request.getExtension(InfoQuery.class); final var nodeRequest = infoQueryRequest.getNode(); 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 3230185b648a92acc122d474f13a4f265572b1cf..093ba8ce0e772901fc2a2be89d75aa76b61683a1 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -9,19 +9,18 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import im.conversations.android.xmpp.model.stanza.Iq; -public class BindProcessor implements Runnable { +public class BindProcessor extends XmppConnection.Delegate implements Runnable { private final XmppConnectionService service; - private final Account account; - public BindProcessor(XmppConnectionService service, Account account) { + public BindProcessor(final XmppConnectionService service, final XmppConnection connection) { + super(service.getApplicationContext(), connection); this.service = service; - this.account = account; } @Override public void run() { - final XmppConnection connection = account.getXmppConnection(); + final var account = connection.getAccount(); final var features = connection.getFeatures(); service.cancelAvatarFetches(account); final boolean loggedInSuccessfully = From 14308bb2c956974a72a378953701b52bd85fe983 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 10 May 2025 17:37:03 +0200 Subject: [PATCH 4/5] clean caps cache when resources go unavailable --- .../conversations/parser/PresenceParser.java | 1 + .../services/XmppConnectionService.java | 4 ++++ .../siacs/conversations/xmpp/XmppConnection.java | 3 +++ .../conversations/xmpp/manager/DiscoManager.java | 16 ++++++++++++++++ 4 files changed, 24 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 57511058efe03628fae9e48d1ec9be6f6c037988..c56262ed1a475c8423e2522b2635bcdb3e7afbc0 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -419,6 +419,7 @@ public class PresenceParser extends AbstractParser if (contact.setLastseen(AbstractParser.parseTimestamp(packet, 0L, true))) { contact.flagInactive(); } + getManager(DiscoManager.class).clear(from); if (from.isBareJid()) { contact.clearPresences(); } else { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 687339108ded64da19dad7423f569918ba82a2d8..dec7cc36e73c2d67f93670067b91955f360441d6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -4209,6 +4209,10 @@ public class XmppConnectionService extends Service { conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid()); + final var connection = account.getXmppConnection(); + if (connection != null) { + connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid()); + } } else { synchronized (account.pendingConferenceLeaves) { account.pendingConferenceLeaves.add(conversation); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index f5d34ca26314a7f58bdd53b3060536602e475930..3aa8f6b197a8338d9b451b8ca030f3b09c41d55a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -216,6 +216,9 @@ public class XmppConnection implements Runnable { this.mXmppConnectionService = service; this.appSettings = mXmppConnectionService.getAppSettings(); this.presenceListener = new PresenceParser(service, this); + // TODO rename this to Iq request handler (it handles only IQ get and set; throw assert + // error in handler just to be safe) + // TODO requires roster and blocking not to be handled by this this.unregisteredIqListener = new IqParser(service, this); this.messageListener = new MessageParser(service, this); this.bindListener = new BindProcessor(service, this); 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 b5776c0aff718d40bc03bbf22eba5bf1f0b19786..bd52f3d28cfb9cc9d290f766fea56dd372e24b8f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -440,6 +440,22 @@ public class DiscoManager extends AbstractManager { } } + public void clear(final Jid address) { + synchronized (this.entityInformation) { + if (address.isFullJid()) { + this.entityInformation.remove(address); + } else { + final var iterator = this.entityInformation.entrySet().iterator(); + while (iterator.hasNext()) { + final var entry = iterator.next(); + if (entry.getKey().asBareJid().equals(address)) { + iterator.remove(); + } + } + } + } + } + public static final class CapsHashMismatchException extends IllegalStateException { public CapsHashMismatchException(final String message) { super(message); From 12df7f1a17c5cfa0ea1607dde6705b0773d6ffd6 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 11 May 2025 10:30:58 +0200 Subject: [PATCH 5/5] introduce ping and carbons managers --- .../services/XmppConnectionService.java | 1 + .../conversations/ui/EditAccountActivity.java | 6 +- .../eu/siacs/conversations/xmpp/Managers.java | 4 + .../conversations/xmpp/XmppConnection.java | 77 +++++++------------ .../xmpp/manager/CarbonsManager.java | 61 +++++++++++++++ .../xmpp/manager/DiscoManager.java | 5 ++ .../xmpp/manager/PingManager.java | 47 +++++++++++ .../android/xmpp/model/stanza/Iq.java | 10 ++- 8 files changed, 157 insertions(+), 54 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/CarbonsManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index dec7cc36e73c2d67f93670067b91955f360441d6..b090d622fd97e79bb347da656e16903a5af80105 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -3610,6 +3610,7 @@ public class XmppConnectionService extends Service { return; } } + // TODO use PingManager final Jid self = conversation.getMucOptions().getSelf().getFullJid(); final Iq ping = new Iq(Iq.Type.GET); ping.setTo(self); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 494e75dd257ed6e04d955b20044067c0c02b280c..c531fb7b8c87d6a7c94ff5994f94b65166c35d4d 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -78,6 +78,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Arrays; @@ -1226,7 +1227,8 @@ public class EditAccountActivity extends OmemoActivity this.binding.accountRegisterNew.setVisibility(View.GONE); } if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { - final Features features = this.mAccount.getXmppConnection().getFeatures(); + final var connection = this.mAccount.getXmppConnection(); + final Features features = connection.getFeatures(); this.binding.stats.setVisibility(View.VISIBLE); boolean showBatteryWarning = isOptimizingBattery(); boolean showDataSaverWarning = isAffectedByDataSaver(); @@ -1239,7 +1241,7 @@ public class EditAccountActivity extends OmemoActivity } else { this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable); } - if (features.carbons()) { + if (connection.getManager(CarbonsManager.class).isEnabled()) { this.binding.serverInfoCarbons.setText(R.string.server_info_available); } else { this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable); diff --git a/src/main/java/eu/siacs/conversations/xmpp/Managers.java b/src/main/java/eu/siacs/conversations/xmpp/Managers.java index 306d6df593460e58abcd3e924cc00b115be808af..84e0225e53773b73c08b43fa34e32cd88eebdffb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Managers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Managers.java @@ -4,7 +4,9 @@ import android.content.Context; import com.google.common.collect.ClassToInstanceMap; import com.google.common.collect.ImmutableClassToInstanceMap; import eu.siacs.conversations.xmpp.manager.AbstractManager; +import eu.siacs.conversations.xmpp.manager.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.PingManager; import eu.siacs.conversations.xmpp.manager.PresenceManager; public class Managers { @@ -16,7 +18,9 @@ public class Managers { public static ClassToInstanceMap get( final Context context, final XmppConnection connection) { return new ImmutableClassToInstanceMap.Builder() + .put(CarbonsManager.class, new CarbonsManager(context, connection)) .put(DiscoManager.class, new DiscoManager(context, connection)) + .put(PingManager.class, new PingManager(context, connection)) .put(PresenceManager.class, new PresenceManager(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 3aa8f6b197a8338d9b451b8ca030f3b09c41d55a..d9e79c35f96ac24b69a7acd35c22510a566ad048 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -72,7 +72,9 @@ 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.CarbonsManager; import eu.siacs.conversations.xmpp.manager.DiscoManager; +import eu.siacs.conversations.xmpp.manager.PingManager; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.AuthenticationFailure; import im.conversations.android.xmpp.model.AuthenticationRequest; @@ -895,7 +897,6 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": successfully enabled carbons (via Bind 2.0)"); - features.carbonsEnabled = true; } else if (currentLoginInfo.inlineBindFeatures != null && currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) { negotiatedCarbons = true; @@ -903,7 +904,6 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": successfully enabled carbons (via Bind 2.0/implicit)"); - features.carbonsEnabled = true; } else { negotiatedCarbons = false; } @@ -2175,7 +2175,7 @@ public class XmppConnection implements Runnable { private void sendPostBindInitialization( final boolean waitForDisco, final boolean carbonsEnabled) { - features.carbonsEnabled = carbonsEnabled; + getManager(CarbonsManager.class).setEnabledOnBind(carbonsEnabled); features.blockListRequested = false; getManager(DiscoManager.class).clear(); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); @@ -2365,35 +2365,15 @@ public class XmppConnection implements Runnable { advancedStreamFeaturesLoadedListeners) { listener.onAdvancedStreamFeaturesAvailable(account); } - if (getFeatures().carbons() && !features.carbonsEnabled) { - sendEnableCarbons(); + final var carbonsManager = getManager(CarbonsManager.class); + if (carbonsManager.hasFeature() && !carbonsManager.isEnabled()) { + carbonsManager.enable(); } if (getFeatures().commands()) { discoverCommands(); } } - private void sendEnableCarbons() { - final Iq iq = new Iq(Iq.Type.SET); - iq.addChild("enable", Namespace.CARBONS); - this.sendIqPacket( - iq, - (packet) -> { - if (packet.getType() == Iq.Type.RESULT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() + ": successfully enabled carbons"); - features.carbonsEnabled = true; - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not enable carbons " - + packet); - } - }); - } - private void processStreamError(final StreamError streamError) throws IOException { final var loginInfo = this.loginInfo; final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo); @@ -2525,6 +2505,10 @@ public class XmppConnection implements Runnable { return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3)); } + public void sendRequestStanza() { + this.sendPacket(new Request()); + } + public ListenableFuture sendIqPacket(final Iq request) { final SettableFuture settable = SettableFuture.create(); this.sendIqPacket( @@ -2654,12 +2638,7 @@ public class XmppConnection implements Runnable { } public void sendPing() { - if (!r()) { - final Iq iq = new Iq(Iq.Type.GET); - iq.setFrom(account.getJid()); - iq.addChild("ping", Namespace.PING); - this.sendIqPacket(iq, null); - } + this.getManager(PingManager.class).ping(); this.lastPingSent = SystemClock.elapsedRealtime(); } @@ -2873,17 +2852,16 @@ public class XmppConnection implements Runnable { public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) { if (trackOfflineMessageRetrieval) { - final Iq iqPing = new Iq(Iq.Type.GET); - iqPing.addChild("ping", Namespace.PING); - this.sendIqPacket( - iqPing, - (response) -> { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": got ping response after sending initial presence"); - XmppConnection.this.offlineMessagesRetrieved = true; - }); + getManager(PingManager.class) + .ping( + () -> { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": got ping response after sending initial" + + " presence"); + this.offlineMessagesRetrieved = true; + }); } else { this.offlineMessagesRetrieved = true; } @@ -2927,6 +2905,10 @@ public class XmppConnection implements Runnable { return this.account; } + public Features getStreamFeatures() { + return this.features; + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { @@ -3074,8 +3056,9 @@ public class XmppConnection implements Runnable { } public class Features { - XmppConnection connection; - private boolean carbonsEnabled = false; + private final XmppConnection connection; + + // TODO move these three into their respective managers or into XmppConnection private boolean encryptionEnabled = false; private boolean blockListRequested = false; @@ -3088,10 +3071,6 @@ public class XmppConnection implements Runnable { return infoQuery != null && infoQuery.getFeatureStrings().contains(feature); } - public boolean carbons() { - return hasDiscoFeature(account.getDomain(), Namespace.CARBONS); - } - public boolean commands() { return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/CarbonsManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/CarbonsManager.java new file mode 100644 index 0000000000000000000000000000000000000000..280275cbc6eee51346966b508b8825b414717dde --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/CarbonsManager.java @@ -0,0 +1,61 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +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.xml.Namespace; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.carbons.Enable; +import im.conversations.android.xmpp.model.stanza.Iq; + +public class CarbonsManager extends AbstractManager { + + private boolean enabled = false; + + public CarbonsManager(final Context context, final XmppConnection connection) { + super(context, connection); + } + + public void setEnabledOnBind(final boolean enabledOnBind) { + this.enabled = enabledOnBind; + } + + public void enable() { + final var request = new Iq(Iq.Type.SET); + request.addExtension(new Enable()); + final var future = this.connection.sendIqPacket(request); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(final Iq result) { + CarbonsManager.this.enabled = true; + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + + ": successfully enabled carbons"); + } + + @Override + public void onFailure(@NonNull final Throwable throwable) { + Log.d( + Config.LOGTAG, + getAccount().getJid().asBareJid() + ": could not enable carbons", + throwable); + } + }, + MoreExecutors.directExecutor()); + } + + public boolean isEnabled() { + return this.enabled; + } + + public boolean hasFeature() { + return getManager(DiscoManager.class).hasServerFeature(Namespace.CARBONS); + } +} 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 bd52f3d28cfb9cc9d290f766fea56dd372e24b8f..3c7c6215e5d949b9a1e246f540f7c1ee6ce09fef 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -422,6 +422,11 @@ public class DiscoManager extends AbstractManager { return builder.buildKeepingLast(); } + public boolean hasServerFeature(final String feature) { + final var infoQuery = this.get(getAccount().getDomain()); + return infoQuery != null && infoQuery.hasFeature(feature); + } + private void put(final Jid address, final InfoQuery infoQuery) { synchronized (this.entityInformation) { this.entityInformation.put(address, infoQuery); diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java new file mode 100644 index 0000000000000000000000000000000000000000..f162a04d6797d7adc9d296d5b54877de31d0dd69 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PingManager.java @@ -0,0 +1,47 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import androidx.annotation.NonNull; +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.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.ping.Ping; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.concurrent.TimeoutException; + +public class PingManager extends AbstractManager { + + public PingManager(final Context context, final XmppConnection connection) { + super(context, connection); + } + + public void ping() { + if (connection.getStreamFeatures().sm()) { + this.connection.sendRequestStanza(); + } else { + this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping())); + } + } + + public void ping(final Runnable runnable) { + final var pingFuture = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping())); + Futures.addCallback( + pingFuture, + new FutureCallback() { + @Override + public void onSuccess(Iq result) { + runnable.run(); + } + + @Override + public void onFailure(final @NonNull Throwable t) { + if (t instanceof TimeoutException) { + return; + } + runnable.run(); + } + }, + MoreExecutors.directExecutor()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java index 9f94400c32692b9a734c5d503e98d3151738d532..545808e69e51c554265b4c2e1e34c03fcd97e4da 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java @@ -1,12 +1,11 @@ package im.conversations.android.xmpp.model.stanza; import com.google.common.base.Strings; - import eu.siacs.conversations.xml.Element; - import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.error.Error; - +import java.util.Arrays; import java.util.Locale; @XmlElement @@ -23,6 +22,11 @@ public class Iq extends Stanza { this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); } + public Iq(final Type type, final Extension... extensions) { + this(type); + this.addExtensions(Arrays.asList(extensions)); + } + // TODO get rid of timeout public enum Type { SET,