From 873644f52815cce7cc015d7c2f5f15aa5eaf73b2 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 18 Jan 2023 12:10:51 +0100 Subject: [PATCH] remove XmppConnection.Features helper class in favor of DiscoManager --- .../eu/siacs/conversations/xml/XmlReader.java | 11 + .../xmpp/stanzas/PresencePacket.java | 10 +- .../android/xmpp/ConnectionPool.java | 14 +- .../android/xmpp/Extensions.java | 1 + .../android/xmpp/XmppConnection.java | 530 +++--------------- .../android/xmpp/manager/DiscoManager.java | 18 +- .../model/capabilties/EntityCapabilities.java | 36 ++ .../android/xmpp/model/streams/Features.java | 26 + .../xmpp/model/streams/package-info.java | 5 + .../xmpp/processor/PresenceProcessor.java | 22 +- 10 files changed, 183 insertions(+), 490 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/streams/Features.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/streams/package-info.java diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index a142677db..10a23d095 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -4,6 +4,7 @@ import android.util.Log; import android.util.Xml; import eu.siacs.conversations.Config; import im.conversations.android.xmpp.Extensions; +import im.conversations.android.xmpp.model.Extension; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -91,6 +92,16 @@ public class XmlReader implements Closeable { return null; } + public T readElement(final Tag current, Class clazz) + throws IOException { + final Element element = readElement(current); + if (clazz.isInstance(element)) { + return clazz.cast(element); + } + throw new IOException( + String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName())); + } + public Element readElement(Tag currentTag) throws IOException { final var attributes = currentTag.getAttributes(); final var namespace = attributes.get("xmlns"); diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java index c321498d8..501170278 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java @@ -1,8 +1,10 @@ package eu.siacs.conversations.xmpp.stanzas; -public class PresencePacket extends AbstractAcknowledgeableStanza { +import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; - public PresencePacket() { - super("presence"); - } +public class PresencePacket extends AbstractAcknowledgeableStanza implements EntityCapabilities { + + public PresencePacket() { + super("presence"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/ConnectionPool.java b/src/main/java/im/conversations/android/xmpp/ConnectionPool.java index d4af0f8f5..b60c9b63f 100644 --- a/src/main/java/im/conversations/android/xmpp/ConnectionPool.java +++ b/src/main/java/im/conversations/android/xmpp/ConnectionPool.java @@ -36,7 +36,7 @@ public class ConnectionPool { private final Context context; private final Executor reconfigurationExecutor = Executors.newSingleThreadExecutor(); - private final ScheduledExecutorService reconnectExecutor = + public static final ScheduledExecutorService CONNECTION_SCHEDULER = Executors.newSingleThreadScheduledExecutor(); private final List connections = new ArrayList<>(); @@ -106,7 +106,7 @@ public class ConnectionPool { ConversationsDatabase.getInstance(context) .accountDao() .setShowErrorNotification(account.id, true); - if (connection.getFeatures().csi()) { + if (connection.supportsClientStateIndication()) { // TODO send correct CSI state (connection.sendActive or connection.sendInactive) } scheduleWakeUpCall(Config.PING_MAX_INTERVAL); @@ -163,7 +163,7 @@ public class ConnectionPool { } public void scheduleWakeUpCall(final int seconds) { - reconnectExecutor.schedule( + CONNECTION_SCHEDULER.schedule( () -> { manageConnectionStates(); }, @@ -272,9 +272,6 @@ public class ConnectionPool { } else if (connection.getStatus() == ConnectionState.CONNECTING) { long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - connection.getLastConnect()) / 1000; - long secondsSinceLastDisco = - (SystemClock.elapsedRealtime() - connection.getLastDiscoStarted()) / 1000; - long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; if (timeout < 0) { Log.d( @@ -286,11 +283,6 @@ public class ConnectionPool { + ")"); connection.resetAttemptCount(false); reconnectAccount(connection); - } else if (discoTimeout < 0) { - connection.sendDiscoTimeout(); - scheduleWakeUpCall(Ints.saturatedCast(discoTimeout)); - } else { - scheduleWakeUpCall(Ints.saturatedCast(Math.min(timeout, discoTimeout))); } } else { if (connection.getTimeToNextAttempt() <= 0) { diff --git a/src/main/java/im/conversations/android/xmpp/Extensions.java b/src/main/java/im/conversations/android/xmpp/Extensions.java index eff8eb155..689186313 100644 --- a/src/main/java/im/conversations/android/xmpp/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/Extensions.java @@ -35,6 +35,7 @@ public final class Extensions { im.conversations.android.xmpp.model.disco.items.ItemsQuery.class, im.conversations.android.xmpp.model.roster.Query.class, im.conversations.android.xmpp.model.roster.Item.class, + im.conversations.android.xmpp.model.streams.Features.class, im.conversations.android.xmpp.model.Hash.class); private static final BiMap> EXTENSION_CLASS_MAP; diff --git a/src/main/java/im/conversations/android/xmpp/XmppConnection.java b/src/main/java/im/conversations/android/xmpp/XmppConnection.java index 75c047f59..52add3b6e 100644 --- a/src/main/java/im/conversations/android/xmpp/XmppConnection.java +++ b/src/main/java/im/conversations/android/xmpp/XmppConnection.java @@ -17,13 +17,14 @@ import androidx.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ClassToInstanceMap; +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 com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MemorizingTrustManager; @@ -41,7 +42,6 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.XmlReader; -import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; @@ -64,6 +64,8 @@ import im.conversations.android.database.model.Account; import im.conversations.android.database.model.Connection; import im.conversations.android.database.model.Credential; import im.conversations.android.xmpp.manager.AbstractManager; +import im.conversations.android.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.streams.Features; import im.conversations.android.xmpp.processor.BindProcessor; import im.conversations.android.xmpp.processor.IqProcessor; import im.conversations.android.xmpp.processor.JingleProcessor; @@ -90,13 +92,10 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; -import java.util.Map.Entry; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -121,8 +120,6 @@ public class XmppConnection implements Runnable { private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; protected final Account account; - private final Features features = new Features(this); - private final HashMap disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); private final SparseArray mStanzaQueue = new SparseArray<>(); private final Hashtable>> packetCallbacks = @@ -131,11 +128,15 @@ public class XmppConnection implements Runnable { private Socket socket; private XmlReader tagReader; private TagWriter tagWriter = new TagWriter(); + + private boolean encryptionEnabled = false; + + private boolean carbonsEnabled = false; private boolean shouldAuthenticate = true; private boolean inSmacksSession = false; private boolean quickStartInProgress = false; private boolean isBound = false; - private Element streamFeatures; + private Features streamFeatures; private String streamId = null; private Jid connectionAddress; private ConnectionState connectionState = ConnectionState.OFFLINE; @@ -147,10 +148,7 @@ public class XmppConnection implements Runnable { private long lastPingSent = 0; private long lastConnect = 0; private long lastSessionStarted = 0; - private long lastDiscoStarted = 0; private boolean isMamPreferenceAlways = false; - private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0); - private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true); private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false); private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); private int attempt = 0; @@ -261,7 +259,6 @@ public class XmppConnection implements Runnable { public void prepareNewConnection() { this.lastConnect = SystemClock.elapsedRealtime(); this.lastPingSent = SystemClock.elapsedRealtime(); - this.lastDiscoStarted = Long.MAX_VALUE; this.mWaitingForSmCatchup.set(false); this.changeStatus(ConnectionState.CONNECTING); } @@ -280,7 +277,7 @@ public class XmppConnection implements Runnable { .accountDao() .getConnectionSettings(account.id); Log.d(Config.LOGTAG, account.address + ": connecting"); - features.encryptionEnabled = false; + this.encryptionEnabled = false; this.inSmacksSession = false; this.quickStartInProgress = false; this.isBound = false; @@ -323,7 +320,7 @@ public class XmppConnection implements Runnable { if (directTls) { localSocket = upgradeSocketToTls(localSocket); - features.encryptionEnabled = true; + this.encryptionEnabled = true; } try { @@ -377,7 +374,7 @@ public class XmppConnection implements Runnable { } try { // if tls is true, encryption is implied and must not be started - features.encryptionEnabled = result.isDirectTls(); + this.encryptionEnabled = result.isDirectTls(); verifiedHostname = result.isAuthenticated() ? result.getHostname().toString() : null; Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname); @@ -395,7 +392,7 @@ public class XmppConnection implements Runnable { + ":" + result.getPort() + " tls: " - + features.encryptionEnabled); + + this.encryptionEnabled); } else { addr = new InetSocketAddress( @@ -409,13 +406,13 @@ public class XmppConnection implements Runnable { + ":" + result.getPort() + " tls: " - + features.encryptionEnabled); + + this.encryptionEnabled); } localSocket = new Socket(); localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - if (features.encryptionEnabled) { + if (this.encryptionEnabled) { localSocket = upgradeSocketToTls(localSocket); } @@ -776,20 +773,18 @@ public class XmppConnection implements Runnable { final Element streamManagementEnabled = bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); - final boolean waitForDisco; if (streamManagementEnabled != null) { resetOutboundStanzaQueue(); processEnabled(streamManagementEnabled); - waitForDisco = true; } else { // if we did not enable stream management in bind do it now - waitForDisco = enableStreamManagement(); + enableStreamManagement(); } if (carbonsEnabled != null) { Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons"); - features.carbonsEnabled = true; + this.carbonsEnabled = true; } - sendPostBindInitialization(waitForDisco, carbonsEnabled != null); + sendPostBindInitialization(carbonsEnabled != null); processNopStreamFeatures = true; } else { processNopStreamFeatures = false; @@ -874,7 +869,7 @@ public class XmppConnection implements Runnable { private void processNopStreamFeatures() throws IOException { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("features", Namespace.STREAMS)) { - this.streamFeatures = tagReader.readElement(tag); + this.streamFeatures = tagReader.readElement(tag, Features.class); Log.d( Config.LOGTAG, account.address @@ -1103,7 +1098,7 @@ public class XmppConnection implements Runnable { } if (inSmacksSession) { ++stanzasReceived; - } else if (features.sm()) { + } else if (this.streamFeatures.streamManagement()) { Log.d( Config.LOGTAG, account.address @@ -1229,7 +1224,7 @@ public class XmppConnection implements Runnable { if (quickStart) { this.quickStartInProgress = true; } - features.encryptionEnabled = true; + this.encryptionEnabled = true; final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { SSLSockets.log(account.address, sslSocket); @@ -1280,7 +1275,7 @@ public class XmppConnection implements Runnable { ConversationsDatabase.getInstance(context) .accountDao() .pendingRegistration(account.id); - this.streamFeatures = tagReader.readElement(currentTag); + this.streamFeatures = tagReader.readElement(currentTag, Features.class); final boolean isSecure = isSecure(); final boolean needsBinding = !isBound && !pendingRegistration; if (this.quickStartInProgress) { @@ -1312,8 +1307,7 @@ public class XmppConnection implements Runnable { .setQuickStartAvailable(account.id, false); throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); } - if (this.streamFeatures.hasChild("starttls", Namespace.TLS) - && !features.encryptionEnabled) { + if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !this.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && pendingRegistration) { @@ -1338,9 +1332,7 @@ public class XmppConnection implements Runnable { && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL); - } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) - && streamId != null - && !inSmacksSession) { + } else if (this.streamFeatures.streamManagement() && streamId != null && !inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { Log.d( Config.LOGTAG, @@ -1382,7 +1374,7 @@ public class XmppConnection implements Runnable { } private boolean isSecure() { - return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + return this.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); } private void authenticate(final SaslMechanism.Version version) throws IOException { @@ -1555,7 +1547,7 @@ public class XmppConnection implements Runnable { private void register() { final Credential credential = CredentialStore.getInstance(context).get(account); final String preAuth = credential.preAuthRegistrationToken; - if (Strings.isNullOrEmpty(preAuth) || !features.invite()) { + if (Strings.isNullOrEmpty(preAuth) || !streamFeatures.invite()) { sendRegistryRequest(); return; } @@ -1712,9 +1704,6 @@ public class XmppConnection implements Runnable { this.stanzasSent = 0; mStanzaQueue.clear(); this.redirectionUrl = null; - synchronized (this.disco) { - disco.clear(); - } synchronized (this.commands) { this.commands.clear(); } @@ -1765,8 +1754,8 @@ public class XmppConnection implements Runnable { .hasChild("optional")) { sendStartSession(); } else { - final boolean waitForDisco = enableStreamManagement(); - sendPostBindInitialization(waitForDisco, false); + enableStreamManagement(); + sendPostBindInitialization(false); } return; } catch (final IllegalArgumentException e) { @@ -1856,13 +1845,6 @@ public class XmppConnection implements Runnable { + " left"); } - public void sendDiscoTimeout() { - if (mWaitForDisco.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.address + ": finalizing bind after disco timeout"); - finalizeBind(); - } - } - private void sendStartSession() { Log.d(Config.LOGTAG, account.address + ": sending legacy session to outdated server"); final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); @@ -1871,8 +1853,8 @@ public class XmppConnection implements Runnable { startSession, (packet) -> { if (packet.getType() == IqPacket.TYPE.RESULT) { - final boolean waitForDisco = enableStreamManagement(); - sendPostBindInitialization(waitForDisco, false); + enableStreamManagement(); + sendPostBindInitialization(false); } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { throw new StateChangingError(ConnectionState.SESSION_FAILURE); } @@ -1880,9 +1862,9 @@ public class XmppConnection implements Runnable { true); } + // TODO the return value is not used any more private boolean enableStreamManagement() { - final boolean streamManagement = - this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); + final boolean streamManagement = this.streamFeatures.streamManagement(); if (streamManagement) { synchronized (this.mStanzaQueue) { final EnablePacket enable = new EnablePacket(); @@ -1896,51 +1878,45 @@ public class XmppConnection implements Runnable { } } - private void sendPostBindInitialization( - final boolean waitForDisco, final boolean carbonsEnabled) { - features.carbonsEnabled = carbonsEnabled; - features.blockListRequested = false; - synchronized (this.disco) { - this.disco.clear(); - } + private void sendPostBindInitialization(final boolean carbonsEnabled) { + this.carbonsEnabled = carbonsEnabled; Log.d(Config.LOGTAG, account.address + ": starting service discovery"); - mPendingServiceDiscoveries.set(0); - mWaitForDisco.set(waitForDisco); - lastDiscoStarted = SystemClock.elapsedRealtime(); - // TODO bring back disco timeout - // context.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account); - final Element caps = streamFeatures.findChild("c"); - final String hash = caps == null ? null : caps.getAttribute("hash"); - final String ver = caps == null ? null : caps.getAttribute("ver"); - ServiceDiscoveryResult discoveryResult = null; - if (hash != null && ver != null) { - // Bring back disco result caching - discoveryResult = - null; // context.getCachedServiceDiscoveryResult(new Pair<>(hash, ver)); - } - // TODO from an older git commit "should make initial connect faster because code is not - // waiting for omemo code to run" - do we need to keep this? - final boolean requestDiscoItemsFirst = - !ConversationsDatabase.getInstance(context).accountDao().isInitialLogin(account.id); + final ArrayList> discoFutures = new ArrayList<>(); + final var discoManager = getManager(DiscoManager.class); - if (requestDiscoItemsFirst) { - sendServiceDiscoveryItems(account.address.getDomain()); - } - if (discoveryResult == null) { - sendServiceDiscoveryInfo(account.address.getDomain()); + final var nodeHash = this.streamFeatures.getCapabilities(); + if (nodeHash != null) { + discoFutures.add( + discoManager.info(account.address.getDomain(), nodeHash.node, nodeHash.hash)); } else { - Log.d(Config.LOGTAG, account.address + ": server caps came from cache"); - disco.put(account.address.getDomain(), discoveryResult); - } - discoverMamPreferences(); - sendServiceDiscoveryInfo(account.address); - if (!requestDiscoItemsFirst) { - sendServiceDiscoveryItems(account.address.getDomain()); + discoFutures.add(discoManager.info(account.address.getDomain())); } + discoFutures.add(discoManager.info(account.address)); + discoFutures.add(discoManager.itemsWithInfo(account.address.getDomain())); - if (!mWaitForDisco.get()) { - finalizeBind(); - } + final var discoFuture = + Futures.withTimeout( + Futures.allAsList(discoFutures), + Config.CONNECT_DISCO_TIMEOUT, + TimeUnit.SECONDS, + ConnectionPool.CONNECTION_SCHEDULER); + + Futures.addCallback( + discoFuture, + new FutureCallback<>() { + @Override + public void onSuccess(List result) { + // TODO enable advanced stream features like carbons + finalizeBind(); + } + + @Override + public void onFailure(@NonNull Throwable t) { + // TODO reset stream ID so we get a proper connect next time + finalizeBind(); + } + }, + MoreExecutors.directExecutor()); this.lastSessionStarted = SystemClock.elapsedRealtime(); } @@ -1949,64 +1925,6 @@ public class XmppConnection implements Runnable { return this.connectionState; } - private void sendServiceDiscoveryInfo(final Jid jid) { - mPendingServiceDiscoveries.incrementAndGet(); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setTo(jid); - iq.query("http://jabber.org/protocol/disco#info"); - this.sendIqPacket( - iq, - (packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - boolean advancedStreamFeaturesLoaded; - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); - if (jid.equals(account.address.getDomain())) { - // context.databaseBackend.insertDiscoveryResult(result); - } - disco.put(jid, result); - advancedStreamFeaturesLoaded = - disco.containsKey(account.address.getDomain()) - && disco.containsKey(account.address); - } - if (advancedStreamFeaturesLoaded - && (jid.equals(account.address.getDomain()) - || jid.equals(account.address))) { - enableAdvancedStreamFeatures(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d( - Config.LOGTAG, - account.address - + ": could not query disco info for " - + jid.toString()); - final boolean serverOrAccount = - jid.equals(account.address.getDomain()) - || jid.equals(account.address); - final boolean advancedStreamFeaturesLoaded; - if (serverOrAccount) { - synchronized (XmppConnection.this.disco) { - disco.put(jid, ServiceDiscoveryResult.empty()); - advancedStreamFeaturesLoaded = - disco.containsKey(account.address.getDomain()) - && disco.containsKey(account.address); - } - } else { - advancedStreamFeaturesLoaded = false; - } - if (advancedStreamFeaturesLoaded) { - enableAdvancedStreamFeatures(); - } - } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); - } - private void discoverMamPreferences() { IqPacket request = new IqPacket(IqPacket.TYPE.GET); request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace); @@ -2067,56 +1985,15 @@ public class XmppConnection implements Runnable { } private void enableAdvancedStreamFeatures() { - if (getFeatures().blocking() && !features.blockListRequested) { - Log.d(Config.LOGTAG, account.address + ": Requesting block list"); - // TODO actually request block list - /*this.sendIqPacket( - getIqGenerator().generateGetBlockList(), context.getIqParser());*/ - } - if (getFeatures().carbons() && !features.carbonsEnabled) { + if (getManager(DiscoManager.class) + .isFeature(connectionAddress.getDomain(), Namespace.CARBONS) + && !this.carbonsEnabled) { sendEnableCarbons(); } - if (getFeatures().commands()) { + // TODO discover commands + /*if (getFeatures().commands()) { discoverCommands(); - } - } - - private void sendServiceDiscoveryItems(final Jid server) { - mPendingServiceDiscoveries.incrementAndGet(); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); - iq.setTo(server.getDomain()); - iq.query("http://jabber.org/protocol/disco#items"); - this.sendIqPacket( - iq, - (packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final HashSet items = new HashSet<>(); - final List elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = - InvalidJid.getNullForInvalid( - element.getAttributeAsJid("jid")); - if (jid != null && !jid.equals(account.address.getDomain())) { - items.add(jid); - } - } - } - for (Jid jid : items) { - sendServiceDiscoveryInfo(jid); - } - } else { - Log.d( - Config.LOGTAG, - account.address + ": could not query disco items of " + server); - } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); + }*/ } private void sendEnableCarbons() { @@ -2127,7 +2004,7 @@ public class XmppConnection implements Runnable { (packet) -> { if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons"); - features.carbonsEnabled = true; + this.carbonsEnabled = true; } else { Log.d( Config.LOGTAG, @@ -2412,28 +2289,8 @@ public class XmppConnection implements Runnable { this.streamId = null; } - private List> findDiscoItemsByFeature(final String feature) { - synchronized (this.disco) { - final List> items = new ArrayList<>(); - for (final Entry cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - items.add(cursor); - } - } - return items; - } - } - - public Jid findDiscoItemByFeature(final String feature) { - final List> items = findDiscoItemsByFeature(feature); - if (items.size() >= 1) { - return items.get(0).getKey(); - } - return null; - } - public boolean r() { - if (getFeatures().sm()) { + if (this.inSmacksSession) { this.tagWriter.writeStanzaAsync(new RequestPacket()); return true; } else { @@ -2441,33 +2298,6 @@ public class XmppConnection implements Runnable { } } - public List getMucServersWithholdAccount() { - final List servers = getMucServers(); - servers.remove(account.address.getDomain().toEscapedString()); - return servers; - } - - public List getMucServers() { - List servers = new ArrayList<>(); - synchronized (this.disco) { - for (final Entry cursor : disco.entrySet()) { - final ServiceDiscoveryResult value = cursor.getValue(); - if (value.getFeatures().contains("http://jabber.org/protocol/muc") - && value.hasIdentity("conference", "text") - && !value.getFeatures().contains("jabber:iq:gateway") - && !value.hasIdentity("conference", "irc")) { - servers.add(cursor.getKey().toString()); - } - } - } - return servers; - } - - public String getMucServer() { - List servers = getMucServers(); - return servers.size() > 0 ? servers.get(0) : null; - } - public int getTimeToNextAttempt() { final int additionalTime = recentErrorConnectionState == ConnectionState.POLICY_VIOLATION ? 3 : 0; @@ -2481,10 +2311,6 @@ public class XmppConnection implements Runnable { return this.attempt; } - public Features getFeatures() { - return this.features; - } - public long getLastSessionEstablished() { final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; return System.currentTimeMillis() - diff; @@ -2498,10 +2324,6 @@ public class XmppConnection implements Runnable { return this.lastPingSent; } - public long getLastDiscoStarted() { - return this.lastDiscoStarted; - } - public long getLastPacketReceived() { return this.lastPacketReceived; } @@ -2542,6 +2364,10 @@ public class XmppConnection implements Runnable { return from != null && from.asBareJid().equals(connectionAddress.asBareJid()); } + public boolean supportsClientStateIndication() { + return this.streamFeatures != null && this.streamFeatures.clientStateIndication(); + } + private static class MyKeyManager implements X509KeyManager { private final Context context; @@ -2610,204 +2436,6 @@ public class XmppConnection implements Runnable { } } - public class Features { - XmppConnection connection; - private boolean carbonsEnabled = false; - private boolean encryptionEnabled = false; - private boolean blockListRequested = false; - - public Features(final XmppConnection connection) { - this.connection = connection; - } - - private boolean hasDiscoFeature(final Jid server, final String feature) { - synchronized (XmppConnection.this.disco) { - final ServiceDiscoveryResult sdr = connection.disco.get(server); - return sdr != null && sdr.getFeatures().contains(feature); - } - } - - public boolean carbons() { - return hasDiscoFeature(account.address.getDomain(), Namespace.CARBONS); - } - - public boolean commands() { - return hasDiscoFeature(account.address.getDomain(), Namespace.COMMANDS); - } - - public boolean easyOnboardingInvites() { - synchronized (commands) { - return commands.containsKey(Namespace.EASY_ONBOARDING_INVITE); - } - } - - public boolean bookmarksConversion() { - return hasDiscoFeature(account.address, Namespace.BOOKMARKS_CONVERSION) - && pepPublishOptions(); - } - - public boolean avatarConversion() { - return hasDiscoFeature(account.address, Namespace.AVATAR_CONVERSION) - && pepPublishOptions(); - } - - public boolean blocking() { - return hasDiscoFeature(account.address.getDomain(), Namespace.BLOCKING); - } - - public boolean spamReporting() { - return hasDiscoFeature(account.address.getDomain(), "urn:xmpp:reporting:reason:spam:0"); - } - - public boolean flexibleOfflineMessageRetrieval() { - return hasDiscoFeature( - account.address.getDomain(), Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL); - } - - public boolean register() { - return hasDiscoFeature(account.address.getDomain(), Namespace.REGISTER); - } - - public boolean invite() { - return connection.streamFeatures != null - && connection.streamFeatures.hasChild("register", Namespace.INVITE); - } - - public boolean sm() { - return streamId != null - || (connection.streamFeatures != null - && connection.streamFeatures.hasChild( - "sm", Namespace.STREAM_MANAGEMENT)); - } - - public boolean csi() { - return connection.streamFeatures != null - && connection.streamFeatures.hasChild("csi", Namespace.CSI); - } - - public boolean pep() { - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult info = disco.get(account.address); - return info != null && info.hasIdentity("pubsub", "pep"); - } - } - - public boolean pepPersistent() { - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult info = disco.get(account.address); - return info != null - && info.getFeatures() - .contains("http://jabber.org/protocol/pubsub#persistent-items"); - } - } - - public boolean pepPublishOptions() { - return hasDiscoFeature(account.address, Namespace.PUBSUB_PUBLISH_OPTIONS); - } - - public boolean pepOmemoWhitelisted() { - return hasDiscoFeature(account.address, AxolotlService.PEP_OMEMO_WHITELISTED); - } - - public boolean mam() { - return MessageArchiveService.Version.has(getAccountFeatures()); - } - - public List getAccountFeatures() { - ServiceDiscoveryResult result = connection.disco.get(account.address); - return result == null ? Collections.emptyList() : result.getFeatures(); - } - - public boolean push() { - return hasDiscoFeature(account.address, Namespace.PUSH) - || hasDiscoFeature(account.address.getDomain(), Namespace.PUSH); - } - - public boolean rosterVersioning() { - return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); - } - - public void setBlockListRequested(boolean value) { - this.blockListRequested = value; - } - - public boolean httpUpload(long filesize) { - if (Config.DISABLE_HTTP_UPLOAD) { - return false; - } else { - for (String namespace : - new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { - List> items = - findDiscoItemsByFeature(namespace); - if (items.size() > 0) { - try { - long maxsize = - Long.parseLong( - items.get(0) - .getValue() - .getExtendedDiscoInformation( - namespace, "max-file-size")); - if (filesize <= maxsize) { - return true; - } else { - Log.d( - Config.LOGTAG, - account.address - + ": http upload is not available for files with" - + " size " - + filesize - + " (max is " - + maxsize - + ")"); - return false; - } - } catch (Exception e) { - return true; - } - } - } - return false; - } - } - - public boolean useLegacyHttpUpload() { - return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null - && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null; - } - - public long getMaxHttpUploadSize() { - for (String namespace : - new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { - List> items = findDiscoItemsByFeature(namespace); - if (items.size() > 0) { - try { - return Long.parseLong( - items.get(0) - .getValue() - .getExtendedDiscoInformation(namespace, "max-file-size")); - } catch (Exception e) { - // ignored - } - } - } - return -1; - } - - public boolean stanzaIds() { - return hasDiscoFeature(account.address, Namespace.STANZA_IDS); - } - - public boolean bookmarks2() { - return Config - .USE_BOOKMARKS2 /* || hasDiscoFeature(account.address, Namespace.BOOKMARKS2_COMPAT)*/; - } - - public boolean externalServiceDiscovery() { - return hasDiscoFeature( - account.address.getDomain(), Namespace.EXTERNAL_SERVICE_DISCOVERY); - } - } - public abstract static class Delegate { protected final Context context; diff --git a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java index d2e0d43be..43b4ab148 100644 --- a/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java +++ b/src/main/java/im/conversations/android/xmpp/manager/DiscoManager.java @@ -91,10 +91,18 @@ public class DiscoManager extends AbstractManager { public ListenableFuture> itemsWithInfo(final Jid entity) { final var itemsFutures = items(entity); - return Futures.transformAsync(itemsFutures, items -> { - // TODO filter out items with empty jid - Collection> infoFutures = Collections2.transform(items, i -> info(i.getJid(), i.getNode())); - return Futures.allAsList(infoFutures); - }, MoreExecutors.directExecutor()); + return Futures.transformAsync( + itemsFutures, + items -> { + // TODO filter out items with empty jid + Collection> infoFutures = + Collections2.transform(items, i -> info(i.getJid(), i.getNode())); + return Futures.allAsList(infoFutures); + }, + MoreExecutors.directExecutor()); + } + + public boolean isFeature(final Jid entity, final String feature) { + return true; } } diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java new file mode 100644 index 000000000..b55f64bc7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java @@ -0,0 +1,36 @@ +package im.conversations.android.xmpp.model.capabilties; + +import im.conversations.android.xmpp.model.Extension; + +public interface EntityCapabilities { + + E getExtension(final Class clazz); + + default NodeHash getCapabilities() { + final String node; + final im.conversations.android.xmpp.EntityCapabilities.Hash hash; + final var capabilities = this.getExtension(Capabilities.class); + final var legacyCapabilities = this.getExtension(LegacyCapabilities.class); + if (capabilities != null) { + node = null; + hash = capabilities.getHash(); + } else if (legacyCapabilities != null) { + node = legacyCapabilities.getNode(); + hash = legacyCapabilities.getHash(); + } else { + return null; + } + return new NodeHash(node, hash); + } + + class NodeHash { + public final String node; + public final im.conversations.android.xmpp.EntityCapabilities.Hash hash; + + private NodeHash( + String node, final im.conversations.android.xmpp.EntityCapabilities.Hash hash) { + this.node = node; + this.hash = hash; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/Features.java b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java new file mode 100644 index 000000000..08cc97e87 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java @@ -0,0 +1,26 @@ +package im.conversations.android.xmpp.model.streams; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; + +@XmlElement +public class Features extends Extension implements EntityCapabilities { + public Features() { + super(Features.class); + } + + public boolean streamManagement() { + // TODO use hasExtension + return this.hasChild("sm", Namespace.STREAM_MANAGEMENT); + } + + public boolean invite() { + return this.hasChild("register", Namespace.INVITE); + } + + public boolean clientStateIndication() { + return this.hasChild("csi", Namespace.CSI); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java b/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java new file mode 100644 index 000000000..02dd19407 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STREAMS) +package im.conversations.android.xmpp.model.streams; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java index 553ac2690..7324f6f1b 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/PresenceProcessor.java @@ -2,11 +2,8 @@ package im.conversations.android.xmpp.processor; import android.content.Context; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import im.conversations.android.xmpp.EntityCapabilities; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.manager.DiscoManager; -import im.conversations.android.xmpp.model.capabilties.Capabilities; -import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities; import java.util.function.Consumer; public class PresenceProcessor extends XmppConnection.Delegate implements Consumer { @@ -23,22 +20,9 @@ public class PresenceProcessor extends XmppConnection.Delegate implements Consum private void fetchCapabilities(final PresencePacket presencePacket) { final var entity = presencePacket.getFrom(); - final String node; - final EntityCapabilities.Hash hash; - final var capabilities = presencePacket.getExtension(Capabilities.class); - final var legacyCapabilities = presencePacket.getExtension(LegacyCapabilities.class); - if (capabilities != null) { - node = null; - hash = capabilities.getHash(); - } else if (legacyCapabilities != null) { - node = legacyCapabilities.getNode(); - hash = legacyCapabilities.getHash(); - } else { - node = null; - hash = null; - } - if (hash != null) { - getManager(DiscoManager.class).info(entity, node, hash); + final var nodeHash = presencePacket.getCapabilities(); + if (nodeHash != null) { + getManager(DiscoManager.class).info(entity, nodeHash.node, nodeHash.hash); } } }