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 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 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 {
final var attributes = currentTag.getAttributes();
final var namespace = attributes.get("xmlns");

View file

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

View file

@ -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<XmppConnection> 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) {

View file

@ -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<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.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<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
private final HashMap<String, Jid> commands = new HashMap<>();
private final SparseArray<AbstractAcknowledgeableStanza> mStanzaQueue = new SparseArray<>();
private final Hashtable<String, Pair<IqPacket, Consumer<IqPacket>>> 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<ListenableFuture<?>> 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<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();
}
@ -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<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() {
@ -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<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() {
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<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() {
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<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 {
protected final Context context;

View file

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