remove XmppConnection.Features helper class in favor of DiscoManager

This commit is contained in:
Daniel Gultsch 2023-01-18 12:10:51 +01:00
parent 199a1cdc64
commit 873644f528
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
10 changed files with 183 additions and 490 deletions

View file

@ -4,6 +4,7 @@ import android.util.Log;
import android.util.Xml; import android.util.Xml;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import im.conversations.android.xmpp.Extensions; import im.conversations.android.xmpp.Extensions;
import im.conversations.android.xmpp.model.Extension;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -91,6 +92,16 @@ public class XmlReader implements Closeable {
return null; return null;
} }
public <T extends Extension> T readElement(final Tag current, Class<T> 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 { public Element readElement(Tag currentTag) throws IOException {
final var attributes = currentTag.getAttributes(); final var attributes = currentTag.getAttributes();
final var namespace = attributes.get("xmlns"); final var namespace = attributes.get("xmlns");

View file

@ -1,8 +1,10 @@
package eu.siacs.conversations.xmpp.stanzas; package eu.siacs.conversations.xmpp.stanzas;
public class PresencePacket extends AbstractAcknowledgeableStanza { import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
public PresencePacket() { public class PresencePacket extends AbstractAcknowledgeableStanza implements EntityCapabilities {
super("presence");
} public PresencePacket() {
super("presence");
}
} }

View file

@ -36,7 +36,7 @@ public class ConnectionPool {
private final Context context; private final Context context;
private final Executor reconfigurationExecutor = Executors.newSingleThreadExecutor(); private final Executor reconfigurationExecutor = Executors.newSingleThreadExecutor();
private final ScheduledExecutorService reconnectExecutor = public static final ScheduledExecutorService CONNECTION_SCHEDULER =
Executors.newSingleThreadScheduledExecutor(); Executors.newSingleThreadScheduledExecutor();
private final List<XmppConnection> connections = new ArrayList<>(); private final List<XmppConnection> connections = new ArrayList<>();
@ -106,7 +106,7 @@ public class ConnectionPool {
ConversationsDatabase.getInstance(context) ConversationsDatabase.getInstance(context)
.accountDao() .accountDao()
.setShowErrorNotification(account.id, true); .setShowErrorNotification(account.id, true);
if (connection.getFeatures().csi()) { if (connection.supportsClientStateIndication()) {
// TODO send correct CSI state (connection.sendActive or connection.sendInactive) // TODO send correct CSI state (connection.sendActive or connection.sendInactive)
} }
scheduleWakeUpCall(Config.PING_MAX_INTERVAL); scheduleWakeUpCall(Config.PING_MAX_INTERVAL);
@ -163,7 +163,7 @@ public class ConnectionPool {
} }
public void scheduleWakeUpCall(final int seconds) { public void scheduleWakeUpCall(final int seconds) {
reconnectExecutor.schedule( CONNECTION_SCHEDULER.schedule(
() -> { () -> {
manageConnectionStates(); manageConnectionStates();
}, },
@ -272,9 +272,6 @@ public class ConnectionPool {
} else if (connection.getStatus() == ConnectionState.CONNECTING) { } else if (connection.getStatus() == ConnectionState.CONNECTING) {
long secondsSinceLastConnect = long secondsSinceLastConnect =
(SystemClock.elapsedRealtime() - connection.getLastConnect()) / 1000; (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; long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
if (timeout < 0) { if (timeout < 0) {
Log.d( Log.d(
@ -286,11 +283,6 @@ public class ConnectionPool {
+ ")"); + ")");
connection.resetAttemptCount(false); connection.resetAttemptCount(false);
reconnectAccount(connection); reconnectAccount(connection);
} else if (discoTimeout < 0) {
connection.sendDiscoTimeout();
scheduleWakeUpCall(Ints.saturatedCast(discoTimeout));
} else {
scheduleWakeUpCall(Ints.saturatedCast(Math.min(timeout, discoTimeout)));
} }
} else { } else {
if (connection.getTimeToNextAttempt() <= 0) { if (connection.getTimeToNextAttempt() <= 0) {

View file

@ -35,6 +35,7 @@ public final class Extensions {
im.conversations.android.xmpp.model.disco.items.ItemsQuery.class, im.conversations.android.xmpp.model.disco.items.ItemsQuery.class,
im.conversations.android.xmpp.model.roster.Query.class, im.conversations.android.xmpp.model.roster.Query.class,
im.conversations.android.xmpp.model.roster.Item.class, im.conversations.android.xmpp.model.roster.Item.class,
im.conversations.android.xmpp.model.streams.Features.class,
im.conversations.android.xmpp.model.Hash.class); im.conversations.android.xmpp.model.Hash.class);
private static final BiMap<Id, Class<? extends Extension>> EXTENSION_CLASS_MAP; private static final BiMap<Id, Class<? extends Extension>> EXTENSION_CLASS_MAP;

View file

@ -17,13 +17,14 @@ import androidx.annotation.Nullable;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ClassToInstanceMap; 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.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.XmppDomainVerifier; 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.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MemorizingTrustManager; 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.Tag;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.InvalidJid;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.bind.Bind2;
import eu.siacs.conversations.xmpp.forms.Data; 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.Connection;
import im.conversations.android.database.model.Credential; import im.conversations.android.database.model.Credential;
import im.conversations.android.xmpp.manager.AbstractManager; 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.BindProcessor;
import im.conversations.android.xmpp.processor.IqProcessor; import im.conversations.android.xmpp.processor.IqProcessor;
import im.conversations.android.xmpp.processor.JingleProcessor; import im.conversations.android.xmpp.processor.JingleProcessor;
@ -90,13 +92,10 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; 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_MESSAGE = 1;
private static final int PACKET_PRESENCE = 2; private static final int PACKET_PRESENCE = 2;
protected final Account account; protected final Account account;
private final Features features = new Features(this);
private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
private final HashMap<String, Jid> commands = new HashMap<>(); private final HashMap<String, Jid> commands = new HashMap<>();
private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>(); private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
private final Hashtable<String, Pair<IqPacket, Consumer<IqPacket>>> packetCallbacks = private final Hashtable<String, Pair<IqPacket, Consumer<IqPacket>>> packetCallbacks =
@ -131,11 +128,15 @@ public class XmppConnection implements Runnable {
private Socket socket; private Socket socket;
private XmlReader tagReader; private XmlReader tagReader;
private TagWriter tagWriter = new TagWriter(); private TagWriter tagWriter = new TagWriter();
private boolean encryptionEnabled = false;
private boolean carbonsEnabled = false;
private boolean shouldAuthenticate = true; private boolean shouldAuthenticate = true;
private boolean inSmacksSession = false; private boolean inSmacksSession = false;
private boolean quickStartInProgress = false; private boolean quickStartInProgress = false;
private boolean isBound = false; private boolean isBound = false;
private Element streamFeatures; private Features streamFeatures;
private String streamId = null; private String streamId = null;
private Jid connectionAddress; private Jid connectionAddress;
private ConnectionState connectionState = ConnectionState.OFFLINE; private ConnectionState connectionState = ConnectionState.OFFLINE;
@ -147,10 +148,7 @@ public class XmppConnection implements Runnable {
private long lastPingSent = 0; private long lastPingSent = 0;
private long lastConnect = 0; private long lastConnect = 0;
private long lastSessionStarted = 0; private long lastSessionStarted = 0;
private long lastDiscoStarted = 0;
private boolean isMamPreferenceAlways = false; 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 AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
private int attempt = 0; private int attempt = 0;
@ -261,7 +259,6 @@ public class XmppConnection implements Runnable {
public void prepareNewConnection() { public void prepareNewConnection() {
this.lastConnect = SystemClock.elapsedRealtime(); this.lastConnect = SystemClock.elapsedRealtime();
this.lastPingSent = SystemClock.elapsedRealtime(); this.lastPingSent = SystemClock.elapsedRealtime();
this.lastDiscoStarted = Long.MAX_VALUE;
this.mWaitingForSmCatchup.set(false); this.mWaitingForSmCatchup.set(false);
this.changeStatus(ConnectionState.CONNECTING); this.changeStatus(ConnectionState.CONNECTING);
} }
@ -280,7 +277,7 @@ public class XmppConnection implements Runnable {
.accountDao() .accountDao()
.getConnectionSettings(account.id); .getConnectionSettings(account.id);
Log.d(Config.LOGTAG, account.address + ": connecting"); Log.d(Config.LOGTAG, account.address + ": connecting");
features.encryptionEnabled = false; this.encryptionEnabled = false;
this.inSmacksSession = false; this.inSmacksSession = false;
this.quickStartInProgress = false; this.quickStartInProgress = false;
this.isBound = false; this.isBound = false;
@ -323,7 +320,7 @@ public class XmppConnection implements Runnable {
if (directTls) { if (directTls) {
localSocket = upgradeSocketToTls(localSocket); localSocket = upgradeSocketToTls(localSocket);
features.encryptionEnabled = true; this.encryptionEnabled = true;
} }
try { try {
@ -377,7 +374,7 @@ public class XmppConnection implements Runnable {
} }
try { try {
// if tls is true, encryption is implied and must not be started // if tls is true, encryption is implied and must not be started
features.encryptionEnabled = result.isDirectTls(); this.encryptionEnabled = result.isDirectTls();
verifiedHostname = verifiedHostname =
result.isAuthenticated() ? result.getHostname().toString() : null; result.isAuthenticated() ? result.getHostname().toString() : null;
Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname); Log.d(Config.LOGTAG, "verified hostname " + verifiedHostname);
@ -395,7 +392,7 @@ public class XmppConnection implements Runnable {
+ ":" + ":"
+ result.getPort() + result.getPort()
+ " tls: " + " tls: "
+ features.encryptionEnabled); + this.encryptionEnabled);
} else { } else {
addr = addr =
new InetSocketAddress( new InetSocketAddress(
@ -409,13 +406,13 @@ public class XmppConnection implements Runnable {
+ ":" + ":"
+ result.getPort() + result.getPort()
+ " tls: " + " tls: "
+ features.encryptionEnabled); + this.encryptionEnabled);
} }
localSocket = new Socket(); localSocket = new Socket();
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
if (features.encryptionEnabled) { if (this.encryptionEnabled) {
localSocket = upgradeSocketToTls(localSocket); localSocket = upgradeSocketToTls(localSocket);
} }
@ -776,20 +773,18 @@ public class XmppConnection implements Runnable {
final Element streamManagementEnabled = final Element streamManagementEnabled =
bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); bound.findChild("enabled", Namespace.STREAM_MANAGEMENT);
final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS);
final boolean waitForDisco;
if (streamManagementEnabled != null) { if (streamManagementEnabled != null) {
resetOutboundStanzaQueue(); resetOutboundStanzaQueue();
processEnabled(streamManagementEnabled); processEnabled(streamManagementEnabled);
waitForDisco = true;
} else { } else {
// if we did not enable stream management in bind do it now // if we did not enable stream management in bind do it now
waitForDisco = enableStreamManagement(); enableStreamManagement();
} }
if (carbonsEnabled != null) { if (carbonsEnabled != null) {
Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons"); Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons");
features.carbonsEnabled = true; this.carbonsEnabled = true;
} }
sendPostBindInitialization(waitForDisco, carbonsEnabled != null); sendPostBindInitialization(carbonsEnabled != null);
processNopStreamFeatures = true; processNopStreamFeatures = true;
} else { } else {
processNopStreamFeatures = false; processNopStreamFeatures = false;
@ -874,7 +869,7 @@ public class XmppConnection implements Runnable {
private void processNopStreamFeatures() throws IOException { private void processNopStreamFeatures() throws IOException {
final Tag tag = tagReader.readTag(); final Tag tag = tagReader.readTag();
if (tag != null && tag.isStart("features", Namespace.STREAMS)) { if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
this.streamFeatures = tagReader.readElement(tag); this.streamFeatures = tagReader.readElement(tag, Features.class);
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
account.address account.address
@ -1103,7 +1098,7 @@ public class XmppConnection implements Runnable {
} }
if (inSmacksSession) { if (inSmacksSession) {
++stanzasReceived; ++stanzasReceived;
} else if (features.sm()) { } else if (this.streamFeatures.streamManagement()) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
account.address account.address
@ -1229,7 +1224,7 @@ public class XmppConnection implements Runnable {
if (quickStart) { if (quickStart) {
this.quickStartInProgress = true; this.quickStartInProgress = true;
} }
features.encryptionEnabled = true; this.encryptionEnabled = true;
final Tag tag = tagReader.readTag(); final Tag tag = tagReader.readTag();
if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
SSLSockets.log(account.address, sslSocket); SSLSockets.log(account.address, sslSocket);
@ -1280,7 +1275,7 @@ public class XmppConnection implements Runnable {
ConversationsDatabase.getInstance(context) ConversationsDatabase.getInstance(context)
.accountDao() .accountDao()
.pendingRegistration(account.id); .pendingRegistration(account.id);
this.streamFeatures = tagReader.readElement(currentTag); this.streamFeatures = tagReader.readElement(currentTag, Features.class);
final boolean isSecure = isSecure(); final boolean isSecure = isSecure();
final boolean needsBinding = !isBound && !pendingRegistration; final boolean needsBinding = !isBound && !pendingRegistration;
if (this.quickStartInProgress) { if (this.quickStartInProgress) {
@ -1312,8 +1307,7 @@ public class XmppConnection implements Runnable {
.setQuickStartAvailable(account.id, false); .setQuickStartAvailable(account.id, false);
throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER); throw new StateChangingException(ConnectionState.INCOMPATIBLE_SERVER);
} }
if (this.streamFeatures.hasChild("starttls", Namespace.TLS) if (this.streamFeatures.hasChild("starttls", Namespace.TLS) && !this.encryptionEnabled) {
&& !features.encryptionEnabled) {
sendStartTLS(); sendStartTLS();
} else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
&& pendingRegistration) { && pendingRegistration) {
@ -1338,9 +1332,7 @@ public class XmppConnection implements Runnable {
&& shouldAuthenticate && shouldAuthenticate
&& isSecure) { && isSecure) {
authenticate(SaslMechanism.Version.SASL); authenticate(SaslMechanism.Version.SASL);
} else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) } else if (this.streamFeatures.streamManagement() && streamId != null && !inSmacksSession) {
&& streamId != null
&& !inSmacksSession) {
if (Config.EXTENDED_SM_LOGGING) { if (Config.EXTENDED_SM_LOGGING) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -1382,7 +1374,7 @@ public class XmppConnection implements Runnable {
} }
private boolean isSecure() { 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 { private void authenticate(final SaslMechanism.Version version) throws IOException {
@ -1555,7 +1547,7 @@ public class XmppConnection implements Runnable {
private void register() { private void register() {
final Credential credential = CredentialStore.getInstance(context).get(account); final Credential credential = CredentialStore.getInstance(context).get(account);
final String preAuth = credential.preAuthRegistrationToken; final String preAuth = credential.preAuthRegistrationToken;
if (Strings.isNullOrEmpty(preAuth) || !features.invite()) { if (Strings.isNullOrEmpty(preAuth) || !streamFeatures.invite()) {
sendRegistryRequest(); sendRegistryRequest();
return; return;
} }
@ -1712,9 +1704,6 @@ public class XmppConnection implements Runnable {
this.stanzasSent = 0; this.stanzasSent = 0;
mStanzaQueue.clear(); mStanzaQueue.clear();
this.redirectionUrl = null; this.redirectionUrl = null;
synchronized (this.disco) {
disco.clear();
}
synchronized (this.commands) { synchronized (this.commands) {
this.commands.clear(); this.commands.clear();
} }
@ -1765,8 +1754,8 @@ public class XmppConnection implements Runnable {
.hasChild("optional")) { .hasChild("optional")) {
sendStartSession(); sendStartSession();
} else { } else {
final boolean waitForDisco = enableStreamManagement(); enableStreamManagement();
sendPostBindInitialization(waitForDisco, false); sendPostBindInitialization(false);
} }
return; return;
} catch (final IllegalArgumentException e) { } catch (final IllegalArgumentException e) {
@ -1856,13 +1845,6 @@ public class XmppConnection implements Runnable {
+ " left"); + " left");
} }
public void sendDiscoTimeout() {
if (mWaitForDisco.compareAndSet(true, false)) {
Log.d(Config.LOGTAG, account.address + ": finalizing bind after disco timeout");
finalizeBind();
}
}
private void sendStartSession() { private void sendStartSession() {
Log.d(Config.LOGTAG, account.address + ": sending legacy session to outdated server"); Log.d(Config.LOGTAG, account.address + ": sending legacy session to outdated server");
final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
@ -1871,8 +1853,8 @@ public class XmppConnection implements Runnable {
startSession, startSession,
(packet) -> { (packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) { if (packet.getType() == IqPacket.TYPE.RESULT) {
final boolean waitForDisco = enableStreamManagement(); enableStreamManagement();
sendPostBindInitialization(waitForDisco, false); sendPostBindInitialization(false);
} else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) {
throw new StateChangingError(ConnectionState.SESSION_FAILURE); throw new StateChangingError(ConnectionState.SESSION_FAILURE);
} }
@ -1880,9 +1862,9 @@ public class XmppConnection implements Runnable {
true); true);
} }
// TODO the return value is not used any more
private boolean enableStreamManagement() { private boolean enableStreamManagement() {
final boolean streamManagement = final boolean streamManagement = this.streamFeatures.streamManagement();
this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT);
if (streamManagement) { if (streamManagement) {
synchronized (this.mStanzaQueue) { synchronized (this.mStanzaQueue) {
final EnablePacket enable = new EnablePacket(); final EnablePacket enable = new EnablePacket();
@ -1896,51 +1878,45 @@ public class XmppConnection implements Runnable {
} }
} }
private void sendPostBindInitialization( private void sendPostBindInitialization(final boolean carbonsEnabled) {
final boolean waitForDisco, final boolean carbonsEnabled) { this.carbonsEnabled = carbonsEnabled;
features.carbonsEnabled = carbonsEnabled;
features.blockListRequested = false;
synchronized (this.disco) {
this.disco.clear();
}
Log.d(Config.LOGTAG, account.address + ": starting service discovery"); Log.d(Config.LOGTAG, account.address + ": starting service discovery");
mPendingServiceDiscoveries.set(0); final ArrayList<ListenableFuture<?>> discoFutures = new ArrayList<>();
mWaitForDisco.set(waitForDisco); final var discoManager = getManager(DiscoManager.class);
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);
if (requestDiscoItemsFirst) { final var nodeHash = this.streamFeatures.getCapabilities();
sendServiceDiscoveryItems(account.address.getDomain()); if (nodeHash != null) {
} discoFutures.add(
if (discoveryResult == null) { discoManager.info(account.address.getDomain(), nodeHash.node, nodeHash.hash));
sendServiceDiscoveryInfo(account.address.getDomain());
} else { } else {
Log.d(Config.LOGTAG, account.address + ": server caps came from cache"); discoFutures.add(discoManager.info(account.address.getDomain()));
disco.put(account.address.getDomain(), discoveryResult);
}
discoverMamPreferences();
sendServiceDiscoveryInfo(account.address);
if (!requestDiscoItemsFirst) {
sendServiceDiscoveryItems(account.address.getDomain());
} }
discoFutures.add(discoManager.info(account.address));
discoFutures.add(discoManager.itemsWithInfo(account.address.getDomain()));
if (!mWaitForDisco.get()) { final var discoFuture =
finalizeBind(); Futures.withTimeout(
} Futures.allAsList(discoFutures),
Config.CONNECT_DISCO_TIMEOUT,
TimeUnit.SECONDS,
ConnectionPool.CONNECTION_SCHEDULER);
Futures.addCallback(
discoFuture,
new FutureCallback<>() {
@Override
public void onSuccess(List<Object> 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(); this.lastSessionStarted = SystemClock.elapsedRealtime();
} }
@ -1949,64 +1925,6 @@ public class XmppConnection implements Runnable {
return this.connectionState; 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() { private void discoverMamPreferences() {
IqPacket request = new IqPacket(IqPacket.TYPE.GET); IqPacket request = new IqPacket(IqPacket.TYPE.GET);
request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace); request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace);
@ -2067,56 +1985,15 @@ public class XmppConnection implements Runnable {
} }
private void enableAdvancedStreamFeatures() { private void enableAdvancedStreamFeatures() {
if (getFeatures().blocking() && !features.blockListRequested) { if (getManager(DiscoManager.class)
Log.d(Config.LOGTAG, account.address + ": Requesting block list"); .isFeature(connectionAddress.getDomain(), Namespace.CARBONS)
// TODO actually request block list && !this.carbonsEnabled) {
/*this.sendIqPacket(
getIqGenerator().generateGetBlockList(), context.getIqParser());*/
}
if (getFeatures().carbons() && !features.carbonsEnabled) {
sendEnableCarbons(); sendEnableCarbons();
} }
if (getFeatures().commands()) { // TODO discover commands
/*if (getFeatures().commands()) {
discoverCommands(); 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<Jid> items = new HashSet<>();
final List<Element> 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() { private void sendEnableCarbons() {
@ -2127,7 +2004,7 @@ public class XmppConnection implements Runnable {
(packet) -> { (packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) { if (packet.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons"); Log.d(Config.LOGTAG, account.address + ": successfully enabled carbons");
features.carbonsEnabled = true; this.carbonsEnabled = true;
} else { } else {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -2412,28 +2289,8 @@ public class XmppConnection implements Runnable {
this.streamId = null; this.streamId = null;
} }
private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
synchronized (this.disco) {
final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
if (cursor.getValue().getFeatures().contains(feature)) {
items.add(cursor);
}
}
return items;
}
}
public Jid findDiscoItemByFeature(final String feature) {
final List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(feature);
if (items.size() >= 1) {
return items.get(0).getKey();
}
return null;
}
public boolean r() { public boolean r() {
if (getFeatures().sm()) { if (this.inSmacksSession) {
this.tagWriter.writeStanzaAsync(new RequestPacket()); this.tagWriter.writeStanzaAsync(new RequestPacket());
return true; return true;
} else { } else {
@ -2441,33 +2298,6 @@ public class XmppConnection implements Runnable {
} }
} }
public List<String> getMucServersWithholdAccount() {
final List<String> servers = getMucServers();
servers.remove(account.address.getDomain().toEscapedString());
return servers;
}
public List<String> getMucServers() {
List<String> servers = new ArrayList<>();
synchronized (this.disco) {
for (final Entry<Jid, ServiceDiscoveryResult> 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<String> servers = getMucServers();
return servers.size() > 0 ? servers.get(0) : null;
}
public int getTimeToNextAttempt() { public int getTimeToNextAttempt() {
final int additionalTime = final int additionalTime =
recentErrorConnectionState == ConnectionState.POLICY_VIOLATION ? 3 : 0; recentErrorConnectionState == ConnectionState.POLICY_VIOLATION ? 3 : 0;
@ -2481,10 +2311,6 @@ public class XmppConnection implements Runnable {
return this.attempt; return this.attempt;
} }
public Features getFeatures() {
return this.features;
}
public long getLastSessionEstablished() { public long getLastSessionEstablished() {
final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted; final long diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
return System.currentTimeMillis() - diff; return System.currentTimeMillis() - diff;
@ -2498,10 +2324,6 @@ public class XmppConnection implements Runnable {
return this.lastPingSent; return this.lastPingSent;
} }
public long getLastDiscoStarted() {
return this.lastDiscoStarted;
}
public long getLastPacketReceived() { public long getLastPacketReceived() {
return this.lastPacketReceived; return this.lastPacketReceived;
} }
@ -2542,6 +2364,10 @@ public class XmppConnection implements Runnable {
return from != null && from.asBareJid().equals(connectionAddress.asBareJid()); 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 static class MyKeyManager implements X509KeyManager {
private final Context context; 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<String> 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<Entry<Jid, ServiceDiscoveryResult>> 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<Entry<Jid, ServiceDiscoveryResult>> 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 { public abstract static class Delegate {
protected final Context context; protected final Context context;

View file

@ -91,10 +91,18 @@ public class DiscoManager extends AbstractManager {
public ListenableFuture<List<InfoQuery>> itemsWithInfo(final Jid entity) { public ListenableFuture<List<InfoQuery>> itemsWithInfo(final Jid entity) {
final var itemsFutures = items(entity); final var itemsFutures = items(entity);
return Futures.transformAsync(itemsFutures, items -> { return Futures.transformAsync(
// TODO filter out items with empty jid itemsFutures,
Collection<ListenableFuture<InfoQuery>> infoFutures = Collections2.transform(items, i -> info(i.getJid(), i.getNode())); items -> {
return Futures.allAsList(infoFutures); // TODO filter out items with empty jid
}, MoreExecutors.directExecutor()); Collection<ListenableFuture<InfoQuery>> 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;
} }
} }

View file

@ -0,0 +1,36 @@
package im.conversations.android.xmpp.model.capabilties;
import im.conversations.android.xmpp.model.Extension;
public interface EntityCapabilities {
<E extends Extension> E getExtension(final Class<E> 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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -2,11 +2,8 @@ package im.conversations.android.xmpp.processor;
import android.content.Context; import android.content.Context;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import im.conversations.android.xmpp.EntityCapabilities;
import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.XmppConnection;
import im.conversations.android.xmpp.manager.DiscoManager; 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; import java.util.function.Consumer;
public class PresenceProcessor extends XmppConnection.Delegate implements Consumer<PresencePacket> { public class PresenceProcessor extends XmppConnection.Delegate implements Consumer<PresencePacket> {
@ -23,22 +20,9 @@ public class PresenceProcessor extends XmppConnection.Delegate implements Consum
private void fetchCapabilities(final PresencePacket presencePacket) { private void fetchCapabilities(final PresencePacket presencePacket) {
final var entity = presencePacket.getFrom(); final var entity = presencePacket.getFrom();
final String node; final var nodeHash = presencePacket.getCapabilities();
final EntityCapabilities.Hash hash; if (nodeHash != null) {
final var capabilities = presencePacket.getExtension(Capabilities.class); getManager(DiscoManager.class).info(entity, nodeHash.node, nodeHash.hash);
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);
} }
} }
} }