Detailed changes
@@ -135,6 +135,18 @@ public class AppSettings {
return getBooleanPreference(ALIGN_START, R.bool.align_start);
}
+ public boolean isConfirmMessages() {
+ return getBooleanPreference(CONFIRM_MESSAGES, R.bool.confirm_messages);
+ }
+
+ public boolean isAllowMessageCorrection() {
+ return getBooleanPreference(ALLOW_MESSAGE_CORRECTION, R.bool.allow_message_correction);
+ }
+
+ public boolean isBroadcastLastActivity() {
+ return getBooleanPreference(BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
+ }
+
public boolean isUseTor() {
return QuickConversationsService.isConversations()
&& getBooleanPreference(USE_TOR, R.bool.use_tor);
@@ -119,6 +119,8 @@ public final class Config {
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean USE_JINGLE_MESSAGE_INIT = true;
+ public static final boolean ENABLE_CAPS_CACHE = true;
+
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean EXTENDED_SM_LOGGING = true; // log stanza counts
public static final boolean BACKGROUND_STANZA_LOGGING =
@@ -3,31 +3,42 @@ package eu.siacs.conversations.entities;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.utils.LanguageUtils;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
public class Room implements AvatarService.Avatarable, Comparable<Room> {
- public String address;
- public String name;
- public String description;
- public String language;
- public int nusers;
+ public final String address;
+ public final String name;
+ public final String description;
+ public final String language;
+ public final int numberOfUsers;
- public Room(String address, String name, String description, String language, int nusers) {
+ public Room(
+ final String address,
+ final String name,
+ final String description,
+ final String language,
+ final Integer numberOfUsers) {
this.address = address;
this.name = name;
this.description = description;
this.language = language;
- this.nusers = nusers;
+ this.numberOfUsers = numberOfUsers == null ? 0 : numberOfUsers;
}
- public Room() {}
-
public String getName() {
- return name;
+ if (Strings.isNullOrEmpty(name)) {
+ final var jid = Jid.ofOrInvalid(address);
+ return jid.getLocal();
+ } else {
+ return name;
+ }
}
public String getDescription() {
@@ -81,9 +92,28 @@ public class Room implements AvatarService.Avatarable, Comparable<Room> {
@Override
public int compareTo(Room o) {
return ComparisonChain.start()
- .compare(o.nusers, nusers)
+ .compare(o.numberOfUsers, numberOfUsers)
.compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name))
.compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address))
.result();
}
+
+ public static Room of(final Jid address, InfoQuery query) {
+ final var identity = Iterables.getFirst(query.getIdentities(), null);
+ final var ri =
+ query.getServiceDiscoveryExtension("http://jabber.org/protocol/muc#roominfo");
+ final String name = identity == null ? null : identity.getIdentityName();
+ String roomName = ri == null ? null : ri.getValue("muc#roomconfig_roomname");
+ String description = ri == null ? null : ri.getValue("muc#roominfo_description");
+ String language = ri == null ? null : ri.getValue("muc#roominfo_lang");
+ String occupants = ri == null ? null : ri.getValue("muc#roominfo_occupants");
+ final Integer numberOfUsers = Ints.tryParse(Strings.nullToEmpty(occupants));
+
+ return new Room(
+ address.toString(),
+ Strings.isNullOrEmpty(roomName) ? name : roomName,
+ description,
+ language,
+ numberOfUsers);
+ }
}
@@ -1,61 +1,15 @@
package eu.siacs.conversations.generator;
-import android.util.Base64;
import eu.siacs.conversations.BuildConfig;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.XmppConnection;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
public abstract class AbstractGenerator {
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
- private final String[] STATIC_FEATURES = {
- Namespace.JINGLE,
- Namespace.JINGLE_APPS_FILE_TRANSFER,
- Namespace.JINGLE_TRANSPORTS_S5B,
- Namespace.JINGLE_TRANSPORTS_IBB,
- Namespace.JINGLE_ENCRYPTED_TRANSPORT,
- Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
- "http://jabber.org/protocol/muc",
- "jabber:x:conference",
- Namespace.OOB,
- "http://jabber.org/protocol/caps",
- "http://jabber.org/protocol/disco#info",
- "urn:xmpp:avatar:metadata+notify",
- Namespace.NICK + "+notify",
- "urn:xmpp:ping",
- "jabber:iq:version",
- "http://jabber.org/protocol/chatstates",
- Namespace.REACTIONS
- };
- private final String[] MESSAGE_CONFIRMATION_FEATURES = {
- "urn:xmpp:chat-markers:0", "urn:xmpp:receipts"
- };
- private final String[] MESSAGE_CORRECTION_FEATURES = {"urn:xmpp:message-correct:0"};
- private final String[] PRIVACY_SENSITIVE = {
- "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
- };
- private final String[] VOIP_NAMESPACES = {
- Namespace.JINGLE_TRANSPORT_ICE_UDP,
- Namespace.JINGLE_FEATURE_AUDIO,
- Namespace.JINGLE_FEATURE_VIDEO,
- Namespace.JINGLE_APPS_RTP,
- Namespace.JINGLE_APPS_DTLS,
- Namespace.JINGLE_MESSAGE
- };
+
protected XmppConnectionService mXmppConnectionService;
AbstractGenerator(XmppConnectionService service) {
@@ -70,72 +24,4 @@ public abstract class AbstractGenerator {
String getIdentityVersion() {
return BuildConfig.VERSION_NAME;
}
-
- String getIdentityName() {
- return BuildConfig.APP_NAME;
- }
-
- String getIdentityType() {
- if ("chromium".equals(android.os.Build.BRAND)) {
- return "pc";
- } else {
- return mXmppConnectionService.getString(R.string.default_resource).toLowerCase();
- }
- }
-
- String getCapHash(final Account account) {
- StringBuilder s = new StringBuilder();
- s.append("client/")
- .append(getIdentityType())
- .append("//")
- .append(getIdentityName())
- .append('<');
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- return null;
- }
-
- for (String feature : getFeatures(account)) {
- s.append(feature).append('<');
- }
- final byte[] sha1 = md.digest(s.toString().getBytes());
- return Base64.encodeToString(sha1, Base64.NO_WRAP);
- }
-
- public List<String> getFeatures(final Account account) {
- final XmppConnection connection = account.getXmppConnection();
- final ArrayList<String> features = new ArrayList<>(Arrays.asList(STATIC_FEATURES));
- features.add("http://jabber.org/protocol/xhtml-im");
- features.add("urn:xmpp:bob");
- if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
- features.add(Namespace.MDS_DISPLAYED + "+notify");
- }
- if (mXmppConnectionService.confirmMessages()) {
- features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
- }
- if (mXmppConnectionService.allowMessageCorrection()) {
- features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES));
- }
- if (Config.supportOmemo()) {
- features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
- }
- if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
- features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
- features.addAll(Arrays.asList(VOIP_NAMESPACES));
- features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
- }
- if (mXmppConnectionService.broadcastLastActivity()) {
- features.add(Namespace.IDLE);
- }
- if (connection != null && connection.getFeatures().bookmarks2()) {
- features.add(Namespace.BOOKMARKS2 + "+notify");
- } else {
- features.add(Namespace.BOOKMARKS + "+notify");
- }
-
- Collections.sort(features);
- return features;
- }
}
@@ -19,7 +19,6 @@ import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
@@ -70,46 +69,6 @@ public class IqGenerator extends AbstractGenerator {
super(service);
}
- public Iq discoResponse(final Account account, final Iq request) {
- final var packet = new Iq(Iq.Type.RESULT);
- packet.setId(request.getId());
- packet.setTo(request.getFrom());
- final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info");
- query.setAttribute("node", request.query().getAttribute("node"));
- final Element identity = query.addChild("identity");
- identity.setAttribute("category", "client");
- identity.setAttribute("type", getIdentityType());
- identity.setAttribute("name", getIdentityName());
- for (final String feature : getFeatures(account)) {
- query.addChild("feature").setAttribute("var", feature);
- }
- return packet;
- }
-
- public Iq versionResponse(final Iq request) {
- final var packet = request.generateResponse(Iq.Type.RESULT);
- Element query = packet.query("jabber:iq:version");
- query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name));
- query.addChild("version").setContent(getIdentityVersion());
- final StringBuilder os = new StringBuilder();
- if ("chromium".equals(android.os.Build.BRAND)) {
- os.append("Chrome OS");
- } else {
- os.append("Android");
- }
- os.append(" ");
- os.append(android.os.Build.VERSION.RELEASE);
- if (QuickConversationsService.isPlayStoreFlavor()) {
- os.append(" (");
- os.append(android.os.Build.BOARD);
- os.append(", ");
- os.append(android.os.Build.FINGERPRINT);
- os.append(")");
- query.addChild("os").setContent(os.toString());
- }
- return packet;
- }
-
public Iq entityTimeResponse(final Iq request) {
final Iq packet = request.generateResponse(Iq.Type.RESULT);
Element time = packet.addChild("time", "urn:xmpp:time");
@@ -665,27 +624,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public Iq queryDiscoItems(final Jid jid) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(jid);
- packet.query(Namespace.DISCO_ITEMS);
- return packet;
- }
-
- public Iq queryDiscoItems(Jid jid, String node) {
- final var packet = queryDiscoItems(jid);
- final var query = packet.query(Namespace.DISCO_ITEMS);
- query.setAttribute("node", node);
- return packet;
- }
-
- public Iq queryDiscoInfo(final Jid jid) {
- final Iq packet = new Iq(Iq.Type.GET);
- packet.setTo(jid);
- packet.addChild("query", Namespace.DISCO_INFO);
- return packet;
- }
-
public Iq bobResponse(Iq request) {
try {
final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid");
@@ -5,8 +5,8 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Presence;
public class PresenceGenerator extends AbstractGenerator {
@@ -63,29 +63,17 @@ public class PresenceGenerator extends AbstractGenerator {
return selfPresence(account, status, true, null);
}
- public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Availability status, final boolean personal, final String nickname) {
- final im.conversations.android.xmpp.model.stanza.Presence packet =
- new im.conversations.android.xmpp.model.stanza.Presence();
- if (personal) {
- final String sig = account.getPgpSignature();
- final String message = account.getPresenceStatusMessage();
- packet.setAvailability(status);
- packet.setStatus(message);
- if (sig != null && mXmppConnectionService.getPgpEngine() != null) {
- packet.addChild("x", "jabber:x:signed").setContent(sig);
- }
+ public im.conversations.android.xmpp.model.stanza.Presence selfPresence(
+ final Account account, final Presence.Availability status, final boolean personal, final String nickname) {
+ final var connection = account.getXmppConnection();
+ if (connection == null) {
+ return new Presence();
}
+ final var packet = connection.getManager(PresenceManager.class).getPresence(status, personal);
if (nickname != null) {
- Element nick = packet.addChild("nick", "http://jabber.org/protocol/nick");
+ final var nick = packet.addChild("nick", "http://jabber.org/protocol/nick");
nick.setContent(nickname);
}
- final String capHash = getCapHash(account);
- if (capHash != null) {
- Element cap = packet.addChild("c", "http://jabber.org/protocol/caps");
- cap.setAttribute("hash", "sha-1");
- cap.setAttribute("node", "https://cheogram.com");
- cap.setAttribute("ver", capHash);
- }
return packet;
}
@@ -16,16 +16,16 @@ import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.stanza.Stanza;
-public abstract class AbstractParser {
+public abstract class AbstractParser extends XmppConnection.Delegate {
protected final XmppConnectionService mXmppConnectionService;
- protected final Account account;
- protected AbstractParser(final XmppConnectionService service, final Account account) {
+ protected AbstractParser(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
this.mXmppConnectionService = service;
- this.account = account;
}
public static Long parseTimestamp(Element element, Long d) {
@@ -1,6 +1,5 @@
package eu.siacs.conversations.parser;
-import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
@@ -10,14 +9,16 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Room;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
-import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.version.Version;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@@ -53,8 +54,8 @@ import org.whispersystems.libsignal.state.PreKeyBundle;
public class IqParser extends AbstractParser implements Consumer<Iq> {
- public IqParser(final XmppConnectionService service, final Account account) {
- super(service, account);
+ public IqParser(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
}
public static List<Jid> items(final Iq packet) {
@@ -74,38 +75,6 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
return items;
}
- public static Room parseRoom(Iq packet) {
- final Element query = packet.findChild("query", Namespace.DISCO_INFO);
- if (query == null) {
- return null;
- }
- final Element x = query.findChild("x");
- if (x == null) {
- return null;
- }
- final Element identity = query.findChild("identity");
- Data data = Data.parse(x);
- String address = packet.getFrom().toString();
- String name = identity == null ? null : identity.getAttribute("name");
- String roomName = data.getValue("muc#roomconfig_roomname");
- String description = data.getValue("muc#roominfo_description");
- String language = data.getValue("muc#roominfo_lang");
- String occupants = data.getValue("muc#roominfo_occupants");
- int nusers;
- try {
- nusers = occupants == null ? 0 : Integer.parseInt(occupants);
- } catch (NumberFormatException e) {
- nusers = 0;
- }
-
- return new Room(
- address,
- TextUtils.isEmpty(roomName) ? name : roomName,
- description,
- language,
- nusers);
- }
-
private void rosterItems(final Account account, final Element query) {
final String version = query.getAttribute("ver");
if (version != null) {
@@ -427,6 +396,7 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
@Override
public void accept(final Iq packet) {
+ final var account = getAccount();
final boolean isGet = packet.getType() == Iq.Type.GET;
if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
return;
@@ -454,7 +424,7 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
// Otherwise, just update the existing blocklist.
if (packet.getType() == Iq.Type.RESULT) {
account.clearBlocklist();
- account.getXmppConnection().getFeatures().setBlockListRequested(true);
+ connection.getFeatures().setBlockListRequested(true);
}
if (items != null) {
final Collection<Jid> jids = new ArrayList<>(items.size());
@@ -514,13 +484,10 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")
|| packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet);
- } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
- final Iq response =
- mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
- mXmppConnectionService.sendIqPacket(account, response, null);
- } else if (packet.hasChild("query", "jabber:iq:version") && isGet) {
- final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
- mXmppConnectionService.sendIqPacket(account, response, null);
+ } else if (packet.hasExtension(InfoQuery.class) && isGet) {
+ this.getManager(DiscoManager.class).handleInfoQuery(packet);
+ } else if (packet.hasExtension(Version.class) && isGet) {
+ this.getManager(DiscoManager.class).handleVersionRequest(packet);
} else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
final Iq response = packet.generateResponse(Iq.Type.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
@@ -564,7 +531,7 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas");
- account.getXmppConnection().sendIqPacket(response, null);
+ connection.sendIqPacket(response, null);
}
}
}
@@ -61,6 +61,7 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.LocalizedContent;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
@@ -108,8 +109,8 @@ public class MessageParser extends AbstractParser
private static final List<String> JINGLE_MESSAGE_ELEMENT_NAMES =
Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish");
- public MessageParser(final XmppConnectionService service, final Account account) {
- super(service, account);
+ public MessageParser(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
}
private static String extractStanzaId(
@@ -518,6 +519,7 @@ public class MessageParser extends AbstractParser
@Override
public void accept(final im.conversations.android.xmpp.model.stanza.Message original) {
+ final var account = connection.getAccount();
if (handleErrorMessage(account, original)) {
return;
}
@@ -539,8 +541,7 @@ public class MessageParser extends AbstractParser
queryId == null
? null
: mXmppConnectionService.getMessageArchiveService().findQuery(queryId);
- final boolean offlineMessagesRetrieved =
- account.getXmppConnection().isOfflineMessagesRetrieved();
+ final boolean offlineMessagesRetrieved = connection.isOfflineMessagesRetrieved();
if (query != null && query.validFrom(original.getFrom())) {
final var f = getForwardedMessagePacket(original, "result", query.version.namespace);
if (f == null) {
@@ -756,7 +757,12 @@ public class MessageParser extends AbstractParser
}
}
- final boolean conversationIsProbablyMuc = isTypeGroupChat || mucUserElement != null || account.getXmppConnection().getMucServersWithholdAccount().contains(counterpart.getDomain().toString());
+ final boolean conversationIsProbablyMuc =
+ isTypeGroupChat
+ || mucUserElement != null
+ || connection
+ .getMucServersWithholdAccount()
+ .contains(counterpart.getDomain().toString());
final Element webxdc = packet.findChild("x", "urn:xmpp:webxdc:0");
final Element thread = packet.findChild("thread");
if (webxdc != null && thread != null) {
@@ -1638,6 +1644,7 @@ public class MessageParser extends AbstractParser
final im.conversations.android.xmpp.model.stanza.Message packet,
final MessageArchiveService.Query query,
final Jid from) {
+ final var account = this.connection.getAccount();
final var id = received.getId();
if (packet.fromAccount(account)) {
if (query != null && id != null && packet.getTo() != null) {
@@ -1668,9 +1675,10 @@ public class MessageParser extends AbstractParser
final Jid counterpart,
final MessageArchiveService.Query query,
final boolean isTypeGroupChat,
- Conversation conversation,
- Element mucUserElement,
- Jid from) {
+ final Conversation conversation,
+ final Element mucUserElement,
+ final Jid from) {
+ final var account = getAccount();
final var id = displayed.getId();
// TODO we donβt even use 'sender' any more. Remove this!
final Jid sender = Jid.Invalid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
@@ -1759,6 +1767,7 @@ public class MessageParser extends AbstractParser
final Jid mucTrueCounterPart,
final int status,
final im.conversations.android.xmpp.model.stanza.Message packet) {
+ final var account = getAccount();
final String reactingTo = reactions.getId();
if (conversation != null && reactingTo != null) {
if (isTypeGroupChat && conversation.getMode() == Conversational.MODE_MULTI) {
@@ -22,6 +22,7 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import im.conversations.android.xmpp.Entity;
@@ -35,12 +36,13 @@ import org.openintents.openpgp.util.OpenPgpUtils;
public class PresenceParser extends AbstractParser
implements Consumer<im.conversations.android.xmpp.model.stanza.Presence> {
- public PresenceParser(final XmppConnectionService service, final Account account) {
- super(service, account);
+ public PresenceParser(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
}
public void parseConferencePresence(
- final im.conversations.android.xmpp.model.stanza.Presence packet, Account account) {
+ final im.conversations.android.xmpp.model.stanza.Presence packet) {
+ final var account = getAccount();
final Conversation conversation =
packet.getFrom() == null
? null
@@ -331,8 +333,8 @@ public class PresenceParser extends AbstractParser
}
private void parseContactPresence(
- final im.conversations.android.xmpp.model.stanza.Presence packet,
- final Account account) {
+ final im.conversations.android.xmpp.model.stanza.Presence packet) {
+ final var account = getAccount();
final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
final Jid from = packet.getFrom();
if (from == null || from.equals(account.getJid())) {
@@ -378,8 +380,7 @@ public class PresenceParser extends AbstractParser
final var connection = account.getXmppConnection();
if (nodeHash != null && connection != null) {
final var discoFuture =
- connection
- .getManager(DiscoManager.class)
+ this.getManager(DiscoManager.class)
.infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash);
logDiscoFailure(from, discoFuture);
@@ -424,6 +425,7 @@ public class PresenceParser extends AbstractParser
if (contact.setLastseen(AbstractParser.parseTimestamp(packet, 0L, true))) {
contact.flagInactive();
}
+ getManager(DiscoManager.class).clear(from);
if (from.isBareJid()) {
contact.clearPresences();
} else {
@@ -492,14 +494,14 @@ public class PresenceParser extends AbstractParser
@Override
public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) {
if (packet.hasChild("x", Namespace.MUC_USER)) {
- this.parseConferencePresence(packet, account);
+ this.parseConferencePresence(packet);
} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
- this.parseConferencePresence(packet, account);
+ this.parseConferencePresence(packet);
} else if ("error".equals(packet.getAttribute("type"))
- && mXmppConnectionService.isMuc(account, packet.getFrom())) {
- this.parseConferencePresence(packet, account);
+ && mXmppConnectionService.isMuc(getAccount(), packet.getFrom())) {
+ this.parseConferencePresence(packet);
} else {
- this.parseContactPresence(packet, account);
+ this.parseContactPresence(packet);
}
}
}
@@ -5,24 +5,34 @@ import androidx.annotation.NonNull;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Ordering;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Room;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.services.MuclumbusService;
-import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.disco.items.Item;
+import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.OkHttpClient;
import okhttp3.ResponseBody;
import retrofit2.Call;
@@ -71,7 +81,7 @@ public class ChannelDiscoveryService {
void discover(
@NonNull final String query,
Method method,
- Map<Jid, Account> mucServices,
+ Map<Jid, XmppConnection> mucServices,
OnChannelSearchResultsFound onChannelSearchResultsFound) {
final List<Room> result = cache.getIfPresent(key(method, mucServices, query));
if (result != null) {
@@ -164,8 +174,8 @@ public class ChannelDiscoveryService {
}
private void discoverChannelsLocalServers(
- final String query, Map<Jid, Account> mucServices, final OnChannelSearchResultsFound listener) {
- final Map<Jid, Account> localMucService = mucServices == null ? getLocalMucServices() : mucServices;
+ final String query, Map<Jid, XmppConnection> mucServices, final OnChannelSearchResultsFound listener) {
+ final var localMucService = mucServices == null ? getLocalMucServices() : mucServices;
Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services");
if (localMucService.isEmpty()) {
listener.onChannelSearchResultsFound(Collections.emptyList());
@@ -179,58 +189,105 @@ public class ChannelDiscoveryService {
listener.onChannelSearchResultsFound(results);
}
}
- final AtomicInteger queriesInFlight = new AtomicInteger();
- final List<Room> rooms = new ArrayList<>();
- for (final Map.Entry<Jid, Account> entry : localMucService.entrySet()) {
- Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey());
- queriesInFlight.incrementAndGet();
- final var account = entry.getValue();
- service.sendIqPacket(
- account,
- itemsRequest,
- (itemsResponse) -> {
- if (itemsResponse.getType() == Iq.Type.RESULT) {
- final List<Jid> items = IqParser.items(itemsResponse);
- for (final Jid item : items) {
- if (item.isDomainJid()) continue; // Only looking for MUCs for now, and by spec they have a localpart
- final Iq infoRequest =
- service.getIqGenerator().queryDiscoInfo(item);
- queriesInFlight.incrementAndGet();
- service.sendIqPacket(
- account,
- infoRequest,
- infoResponse -> {
- if (infoResponse.getType() == Iq.Type.RESULT) {
- final Room room = IqParser.parseRoom(infoResponse);
- if (room != null) {
- rooms.add(room);
- }
- if (queriesInFlight.decrementAndGet() <= 0) {
- finishDiscoSearch(rooms, query, mucServices, listener);
- }
- } else {
- queriesInFlight.decrementAndGet();
- }
- }, 20L);
+ final var roomsRoomsFuture =
+ Futures.successfulAsList(
+ Collections2.transform(
+ localMucService.entrySet(),
+ e -> discoverRooms(e.getValue(), e.getKey())));
+ final var roomsFuture =
+ Futures.transform(
+ roomsRoomsFuture,
+ rooms -> {
+ final var builder = new ImmutableList.Builder<Room>();
+ for (final var inner : rooms) {
+ if (inner == null) {
+ continue;
+ }
+ builder.addAll(inner);
}
- }
- if (queriesInFlight.decrementAndGet() <= 0) {
- finishDiscoSearch(rooms, query, mucServices, listener);
- }
- });
- }
+ return builder.build();
+ },
+ MoreExecutors.directExecutor());
+ Futures.addCallback(
+ roomsFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(ImmutableList<Room> rooms) {
+ finishDiscoSearch(rooms, query, mucServices, listener);
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "could not perform room search", throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Collection<Room>> discoverRooms(
+ final XmppConnection connection, final Jid server) {
+ final var request = new Iq(Iq.Type.GET);
+ request.addExtension(new ItemsQuery());
+ request.setTo(server);
+ final ListenableFuture<Collection<Item>> itemsFuture =
+ Futures.transform(
+ connection.sendIqPacket(request),
+ iq -> {
+ final var itemsQuery = iq.getExtension(ItemsQuery.class);
+ if (itemsQuery == null) {
+ return Collections.emptyList();
+ }
+ final var items = itemsQuery.getExtensions(Item.class);
+ return Collections2.filter(items, i -> Objects.nonNull(i.getJid()));
+ },
+ MoreExecutors.directExecutor());
+ final var roomsFutures =
+ Futures.transformAsync(
+ itemsFuture,
+ items -> {
+ final var infoFutures =
+ Collections2.transform(
+ items, i -> discoverRoom(connection, i.getJid()));
+ return Futures.successfulAsList(infoFutures);
+ },
+ MoreExecutors.directExecutor());
+ return Futures.transform(
+ roomsFutures,
+ rooms -> Collections2.filter(rooms, Objects::nonNull),
+ MoreExecutors.directExecutor());
+ }
+
+ private ListenableFuture<Room> discoverRoom(final XmppConnection connection, final Jid room) {
+ final var request = new Iq(Iq.Type.GET);
+ request.addExtension(new InfoQuery());
+ request.setTo(room);
+ final var infoQueryResponseFuture = connection.sendIqPacket(request);
+ return Futures.transform(
+ infoQueryResponseFuture,
+ result -> {
+ final var infoQuery = result.getExtension(InfoQuery.class);
+ if (infoQuery == null) {
+ return null;
+ }
+ return Room.of(room, infoQuery);
+ },
+ MoreExecutors.directExecutor());
}
private void finishDiscoSearch(
- List<Room> rooms, String query, Map<Jid, Account> mucServices, OnChannelSearchResultsFound listener) {
- Collections.sort(rooms);
- cache.put(key(Method.LOCAL_SERVER, mucServices, ""), rooms);
+ final List<Room> rooms,
+ final String query,
+ Map<Jid, XmppConnection> mucServices,
+ final OnChannelSearchResultsFound listener) {
+ Log.d(Config.LOGTAG, "finishDiscoSearch with " + rooms.size() + " rooms");
+ final var sorted = Ordering.natural().sortedCopy(rooms);
+ cache.put(key(Method.LOCAL_SERVER, mucServices, ""), sorted);
if (query.isEmpty()) {
- listener.onChannelSearchResultsFound(rooms);
+ listener.onChannelSearchResultsFound(sorted);
} else {
- List<Room> results = copyMatching(rooms, query);
+ List<Room> results = copyMatching(sorted, query);
cache.put(key(Method.LOCAL_SERVER, mucServices, query), results);
- listener.onChannelSearchResultsFound(rooms);
+ listener.onChannelSearchResultsFound(sorted);
}
}
@@ -244,26 +301,24 @@ public class ChannelDiscoveryService {
return result;
}
- private Map<Jid, Account> getLocalMucServices() {
- final HashMap<Jid, Account> localMucServices = new HashMap<>();
- for (Account account : service.getAccounts()) {
- if (account.isEnabled()) {
- final XmppConnection xmppConnection = account.getXmppConnection();
- if (xmppConnection == null) {
- continue;
- }
- for (final String mucService : xmppConnection.getMucServers()) {
- final Jid jid = Jid.of(mucService);
- if (!localMucServices.containsKey(jid)) {
- localMucServices.put(jid, account);
+ private Map<Jid, XmppConnection> getLocalMucServices() {
+ final ImmutableMap.Builder<Jid, XmppConnection> localMucServices =
+ new ImmutableMap.Builder<>();
+ for (final var account : service.getAccounts()) {
+ final var connection = account.getXmppConnection();
+ if (connection != null && account.isEnabled()) {
+ for (final String mucService : connection.getMucServers()) {
+ final Jid jid = Jid.ofOrInvalid(mucService);
+ if (Jid.Invalid.isValid(jid)) {
+ localMucServices.put(jid, connection);
}
}
}
}
- return localMucServices;
+ return localMucServices.buildKeepingLast();
}
- private static String key(Method method, Map<Jid, Account> mucServices, String query) {
+ private static String key(Method method, Map<Jid, XmppConnection> mucServices, String query) {
final String servicesKey = mucServices == null ? "\00" : String.join("\00", mucServices.keySet());
return String.format("%s\00%s\00%s", method, servicesKey, query);
}
@@ -1454,7 +1454,7 @@ public class XmppConnectionService extends Service {
public void discoverChannels(
String query,
ChannelDiscoveryService.Method method,
- Map<Jid, Account> mucServices,
+ Map<Jid, XmppConnection> mucServices,
ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) {
mChannelDiscoveryService.discover(
Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound);
@@ -4176,6 +4176,7 @@ public class XmppConnectionService extends Service {
return;
}
}
+ // TODO use PingManager
final Jid self = conversation.getMucOptions().getSelf().getFullJid();
final Iq ping = new Iq(Iq.Type.GET);
ping.setTo(self);
@@ -4795,6 +4796,10 @@ public class XmppConnectionService extends Service {
conversation.getAccount().getJid().asBareJid()
+ ": leaving muc "
+ conversation.getJid());
+ final var connection = account.getXmppConnection();
+ if (connection != null) {
+ connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
+ }
} else {
synchronized (account.pendingConferenceLeaves) {
account.pendingConferenceLeaves.add(conversation);
@@ -4957,14 +4962,34 @@ public class XmppConnectionService extends Service {
return;
}
- final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
- sendIqPacket(account, request, (reply) -> {
- final var result = reply.getExtension(InfoQuery.class);
- cb.accept(
- result.hasFeature("http://jabber.org/protocol/muc") &&
- result.hasIdentityWithCategory("conference")
- );
- });
+ final var connection = account.getXmppConnection();
+ if (connection == null) {
+ cb.accept(false); // hmmm...
+ return;
+ }
+ final ListenableFuture<InfoQuery> future =
+ connection
+ .getManager(DiscoManager.class)
+ .info(Entity.discoItem(jid), null);
+
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(InfoQuery result) {
+ cb.accept(
+ result.hasFeature("http://jabber.org/protocol/muc") &&
+ result.hasIdentityWithCategory("conference")
+ );
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ cb.accept(false);
+ }
+ },
+ MoreExecutors.directExecutor()
+ );
}
public void fetchConferenceConfiguration(final Conversation conversation) {
@@ -4973,7 +4998,6 @@ public class XmppConnectionService extends Service {
public void fetchConferenceConfiguration(
final Conversation conversation, final OnConferenceConfigurationFetched callback) {
- final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
final var account = conversation.getAccount();
final var connection = account.getXmppConnection();
if (connection == null) {
@@ -4985,7 +5009,7 @@ public class XmppConnectionService extends Service {
.info(Entity.discoItem(conversation.getJid().asBareJid()), null);
Futures.addCallback(
future,
- new FutureCallback<InfoQuery>() {
+ new FutureCallback<>() {
@Override
public void onSuccess(InfoQuery result) {
final MucOptions mucOptions = conversation.getMucOptions();
@@ -6181,11 +6205,11 @@ public class XmppConnectionService extends Service {
}
public boolean confirmMessages() {
- return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
+ return appSettings.isConfirmMessages();
}
public boolean allowMessageCorrection() {
- return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
+ return appSettings.isAllowMessageCorrection();
}
public boolean sendChatStates() {
@@ -6193,11 +6217,11 @@ public class XmppConnectionService extends Service {
}
public boolean useTorToConnect() {
- return getBooleanPreference("use_tor", R.bool.use_tor);
+ return appSettings.isUseTor();
}
public boolean broadcastLastActivity() {
- return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
+ return appSettings.isBroadcastLastActivity();
}
public int unreadCount() {
@@ -6979,11 +7003,6 @@ public class XmppConnectionService extends Service {
});
}
- public void fetchCommands(Account account, final Jid jid, Consumer<Iq> callback) {
- final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
- sendIqPacket(account, request, callback);
- }
-
public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
final Iq request = new Iq(Iq.Type.GET);
@@ -41,6 +41,7 @@ import eu.siacs.conversations.ui.util.PendingItem;
import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
import eu.siacs.conversations.utils.AccountUtils;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.XmppConnection;
public class ChannelDiscoveryActivity extends XmppActivity
implements MenuItem.OnActionExpandListener,
@@ -58,7 +59,7 @@ public class ChannelDiscoveryActivity extends XmppActivity
private String[] pendingServices = null;
private ChannelDiscoveryService.Method method = ChannelDiscoveryService.Method.LOCAL_SERVER;
- private HashMap<Jid, Account> mucServices = null;
+ private HashMap<Jid, XmppConnection> mucServices = null;
private boolean optedIn = false;
@@ -70,7 +71,7 @@ public class ChannelDiscoveryActivity extends XmppActivity
if (pendingServices != null) {
mucServices = new HashMap<>();
for (int i = 0; i < pendingServices.length; i += 2) {
- mucServices.put(Jid.of(pendingServices[i]), xmppConnectionService.findAccountByJid(Jid.of(pendingServices[i+1])));
+ mucServices.put(Jid.of(pendingServices[i]), xmppConnectionService.findAccountByJid(Jid.of(pendingServices[i+1])).getXmppConnection());
}
}
@@ -210,6 +210,7 @@ import eu.siacs.conversations.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.disco.items.Item;
import im.conversations.android.xmpp.model.stanza.Iq;
import org.jetbrains.annotations.NotNull;
@@ -3573,28 +3574,40 @@ public class ConversationFragment extends XmppFragment
} else {
if (!delayShow) conversation.showViewPager();
binding.commandsViewProgressbar.setVisibility(View.VISIBLE);
- activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (iq) -> {
- if (activity == null) return;
+ final var discoManager = conversation.getAccount().getXmppConnection().getManager(DiscoManager.class);
+ final var future = discoManager.items(Entity.discoItem(commandJid), Namespace.COMMANDS);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Collection<Item> result) {
+ if (activity == null) return;
+
+ activity.runOnUiThread(() -> {
+ binding.commandsViewProgressbar.setVisibility(View.GONE);
+ commandAdapter.clear();
+ for (final var command : result) {
+ commandAdapter.add(new CommandAdapter.Command0050(command));
+ }
- activity.runOnUiThread(() -> {
- binding.commandsViewProgressbar.setVisibility(View.GONE);
- commandAdapter.clear();
- if (iq.getType() == Iq.Type.RESULT) {
- for (Element child : iq.query().getChildren()) {
- if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue;
- commandAdapter.add(new CommandAdapter.Command0050(child));
- }
- }
+ if (mucConfig != null) commandAdapter.add(mucConfig);
+
+ if (commandAdapter.getCount() < 1) {
+ conversation.hideViewPager();
+ } else if (delayShow) {
+ conversation.showViewPager();
+ }
+ });
- if (mucConfig != null) commandAdapter.add(mucConfig);
+ }
- if (commandAdapter.getCount() < 1) {
- conversation.hideViewPager();
- } else if (delayShow) {
- conversation.showViewPager();
- }
- });
- });
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(Config.LOGTAG, "Failed to get commands: " + throwable);
+ }
+ },
+ MoreExecutors.directExecutor()
+ );
}
}
@@ -96,6 +96,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
@@ -1362,7 +1363,8 @@ public class EditAccountActivity extends OmemoActivity
this.binding.accountRegisterNew.setVisibility(View.GONE);
}
if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
- final Features features = this.mAccount.getXmppConnection().getFeatures();
+ final var connection = this.mAccount.getXmppConnection();
+ final Features features = connection.getFeatures();
this.binding.stats.setVisibility(View.VISIBLE);
boolean showBatteryWarning = isOptimizingBattery();
boolean showDataSaverWarning = isAffectedByDataSaver();
@@ -1375,7 +1377,7 @@ public class EditAccountActivity extends OmemoActivity
} else {
this.binding.serverInfoRosterVersion.setText(R.string.server_info_unavailable);
}
- if (features.carbons()) {
+ if (connection.getManager(CarbonsManager.class).isEnabled()) {
this.binding.serverInfoCarbons.setText(R.string.server_info_available);
} else {
this.binding.serverInfoCarbons.setText(R.string.server_info_unavailable);
@@ -0,0 +1,28 @@
+package eu.siacs.conversations.xmpp;
+
+import android.content.Context;
+import com.google.common.collect.ClassToInstanceMap;
+import com.google.common.collect.ImmutableClassToInstanceMap;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.manager.AbstractManager;
+import eu.siacs.conversations.xmpp.manager.CarbonsManager;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.PingManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
+
+public class Managers {
+
+ private Managers() {
+ throw new AssertionError("Do not instantiate me");
+ }
+
+ public static ClassToInstanceMap<AbstractManager> get(
+ final XmppConnectionService context, final XmppConnection connection) {
+ return new ImmutableClassToInstanceMap.Builder<AbstractManager>()
+ .put(CarbonsManager.class, new CarbonsManager(context, connection))
+ .put(DiscoManager.class, new DiscoManager(context, connection))
+ .put(PingManager.class, new PingManager(context, connection))
+ .put(PresenceManager.class, new PresenceManager(context, connection))
+ .build();
+ }
+}
@@ -20,7 +20,6 @@ import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ClassToInstanceMap;
-import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
@@ -121,11 +120,14 @@ import eu.siacs.conversations.xmpp.bind.Bind2;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import eu.siacs.conversations.xmpp.manager.AbstractManager;
+import eu.siacs.conversations.xmpp.manager.CarbonsManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.PingManager;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.AuthenticationFailure;
import im.conversations.android.xmpp.model.AuthenticationRequest;
import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
+import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.StreamElement;
import im.conversations.android.xmpp.model.bind2.Bind;
import im.conversations.android.xmpp.model.bind2.Bound;
@@ -264,16 +266,14 @@ public class XmppConnection implements Runnable {
this.account = account;
this.mXmppConnectionService = service;
this.appSettings = mXmppConnectionService.getAppSettings();
- this.presenceListener = new PresenceParser(service, account);
- this.unregisteredIqListener = new IqParser(service, account);
- this.messageListener = new MessageParser(service, account);
- this.bindListener = new BindProcessor(service, account);
- this.managers =
- new ImmutableClassToInstanceMap.Builder<AbstractManager>()
- .put(
- DiscoManager.class,
- new DiscoManager(service, this))
- .build();
+ this.presenceListener = new PresenceParser(service, this);
+ // TODO rename this to Iq request handler (it handles only IQ get and set; throw assert
+ // error in handler just to be safe)
+ // TODO requires roster and blocking not to be handled by this
+ this.unregisteredIqListener = new IqParser(service, this);
+ this.messageListener = new MessageParser(service, this);
+ this.bindListener = new BindProcessor(service, this);
+ this.managers = Managers.get(service, this);
}
private static void fixResource(final Context context, final Account account) {
@@ -956,7 +956,6 @@ public class XmppConnection implements Runnable {
Config.LOGTAG,
account.getJid().asBareJid()
+ ": successfully enabled carbons (via Bind 2.0)");
- features.carbonsEnabled = true;
} else if (currentLoginInfo.inlineBindFeatures != null
&& currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) {
negotiatedCarbons = true;
@@ -964,7 +963,6 @@ public class XmppConnection implements Runnable {
Config.LOGTAG,
account.getJid().asBareJid()
+ ": successfully enabled carbons (via Bind 2.0/implicit)");
- features.carbonsEnabled = true;
} else {
negotiatedCarbons = false;
}
@@ -2252,7 +2250,7 @@ public class XmppConnection implements Runnable {
private void sendPostBindInitialization(
final boolean waitForDisco, final boolean carbonsEnabled) {
- features.carbonsEnabled = carbonsEnabled;
+ getManager(CarbonsManager.class).setEnabledOnBind(carbonsEnabled);
features.blockListRequested = false;
getManager(DiscoManager.class).clear();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
@@ -2442,35 +2440,15 @@ public class XmppConnection implements Runnable {
advancedStreamFeaturesLoadedListeners) {
listener.onAdvancedStreamFeaturesAvailable(account);
}
- if (getFeatures().carbons() && !features.carbonsEnabled) {
- sendEnableCarbons();
+ final var carbonsManager = getManager(CarbonsManager.class);
+ if (carbonsManager.hasFeature() && !carbonsManager.isEnabled()) {
+ carbonsManager.enable();
}
if (getFeatures().commands()) {
discoverCommands();
}
}
- private void sendEnableCarbons() {
- final Iq iq = new Iq(Iq.Type.SET);
- iq.addChild("enable", Namespace.CARBONS);
- this.sendIqPacket(
- iq,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid() + ": successfully enabled carbons");
- features.carbonsEnabled = true;
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": could not enable carbons "
- + packet);
- }
- });
- }
-
private void processStreamError(final StreamError streamError) throws IOException {
final var loginInfo = this.loginInfo;
final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo);
@@ -2602,6 +2580,10 @@ public class XmppConnection implements Runnable {
return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
}
+ public void sendRequestStanza() {
+ this.sendPacket(new Request());
+ }
+
public ListenableFuture<Iq> sendIqPacket(final Iq request) {
final SettableFuture<Iq> settable = SettableFuture.create();
this.sendIqPacket(
@@ -2655,6 +2637,36 @@ public class XmppConnection implements Runnable {
return packet.getId();
}
+ public void sendResultFor(final Iq request, final Extension... extensions) {
+ final var from = request.getFrom();
+ final var id = request.getId();
+ final var response = new Iq(Iq.Type.RESULT);
+ response.setTo(from);
+ response.setId(id);
+ for (final Extension extension : extensions) {
+ response.addExtension(extension);
+ }
+ this.sendPacket(response);
+ }
+
+ public void sendErrorFor(
+ final Iq request,
+ final im.conversations.android.xmpp.model.error.Error.Type type,
+ final Condition condition,
+ final im.conversations.android.xmpp.model.error.Error.Extension... extensions) {
+ final var from = request.getFrom();
+ final var id = request.getId();
+ final var response = new Iq(Iq.Type.ERROR);
+ response.setTo(from);
+ response.setId(id);
+ final var error =
+ response.addExtension(new im.conversations.android.xmpp.model.error.Error());
+ error.setType(type);
+ error.setCondition(condition);
+ error.addExtensions(extensions);
+ this.sendPacket(response);
+ }
+
public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) {
this.sendPacket(packet);
}
@@ -2719,12 +2731,7 @@ public class XmppConnection implements Runnable {
}
public void sendPing() {
- if (!r()) {
- final Iq iq = new Iq(Iq.Type.GET);
- iq.setFrom(account.getJid());
- iq.addChild("ping", Namespace.PING);
- this.sendIqPacket(iq, null);
- }
+ this.getManager(PingManager.class).ping();
this.lastPingSent = SystemClock.elapsedRealtime();
}
@@ -2938,17 +2945,16 @@ public class XmppConnection implements Runnable {
public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) {
if (trackOfflineMessageRetrieval) {
- final Iq iqPing = new Iq(Iq.Type.GET);
- iqPing.addChild("ping", Namespace.PING);
- this.sendIqPacket(
- iqPing,
- (response) -> {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": got ping response after sending initial presence");
- XmppConnection.this.offlineMessagesRetrieved = true;
- });
+ getManager(PingManager.class)
+ .ping(
+ () -> {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": got ping response after sending initial"
+ + " presence");
+ this.offlineMessagesRetrieved = true;
+ });
} else {
this.offlineMessagesRetrieved = true;
}
@@ -2992,6 +2998,10 @@ public class XmppConnection implements Runnable {
return this.account;
}
+ public Features getStreamFeatures() {
+ return this.features;
+ }
+
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@@ -3139,8 +3149,9 @@ public class XmppConnection implements Runnable {
}
public class Features {
- XmppConnection connection;
- private boolean carbonsEnabled = false;
+ private final XmppConnection connection;
+
+ // TODO move these three into their respective managers or into XmppConnection
private boolean encryptionEnabled = false;
private boolean blockListRequested = false;
@@ -3153,10 +3164,6 @@ public class XmppConnection implements Runnable {
return infoQuery != null && infoQuery.getFeatureStrings().contains(feature);
}
- public boolean carbons() {
- return hasDiscoFeature(account.getDomain(), Namespace.CARBONS);
- }
-
public boolean commands() {
return hasDiscoFeature(account.getDomain(), Namespace.COMMANDS);
}
@@ -0,0 +1,62 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.carbons.Enable;
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class CarbonsManager extends AbstractManager {
+
+ private boolean enabled = false;
+
+ public CarbonsManager(final XmppConnectionService context, final XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public void setEnabledOnBind(final boolean enabledOnBind) {
+ this.enabled = enabledOnBind;
+ }
+
+ public void enable() {
+ final var request = new Iq(Iq.Type.SET);
+ request.addExtension(new Enable());
+ final var future = this.connection.sendIqPacket(request);
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(final Iq result) {
+ CarbonsManager.this.enabled = true;
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid()
+ + ": successfully enabled carbons");
+ }
+
+ @Override
+ public void onFailure(@NonNull final Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ getAccount().getJid().asBareJid() + ": could not enable carbons",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public boolean hasFeature() {
+ return getManager(DiscoManager.class).hasServerFeature(Namespace.CARBONS);
+ }
+}
@@ -4,27 +4,38 @@ import android.content.Context;
import android.util.Log;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.EntityCapabilities;
import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.ServiceDescription;
import im.conversations.android.xmpp.model.Hash;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.disco.items.Item;
import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.version.Version;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -34,7 +45,44 @@ import org.jspecify.annotations.Nullable;
public class DiscoManager extends AbstractManager {
- public static final String CAPABILITY_NODE = "http://conversations.im";
+ public static final String CAPABILITY_NODE = "https://cheogram.com";
+
+ private final List<String> STATIC_FEATURES =
+ Arrays.asList(
+ Namespace.JINGLE,
+ Namespace.JINGLE_APPS_FILE_TRANSFER,
+ Namespace.JINGLE_TRANSPORTS_S5B,
+ Namespace.JINGLE_TRANSPORTS_IBB,
+ Namespace.JINGLE_ENCRYPTED_TRANSPORT,
+ Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
+ "http://jabber.org/protocol/muc",
+ "jabber:x:conference",
+ Namespace.OOB,
+ Namespace.ENTITY_CAPABILITIES,
+ Namespace.ENTITY_CAPABILITIES_2,
+ Namespace.DISCO_INFO,
+ "urn:xmpp:avatar:metadata+notify",
+ Namespace.NICK + "+notify",
+ Namespace.PING,
+ Namespace.VERSION,
+ Namespace.CHAT_STATES,
+ Namespace.REACTIONS);
+ private final List<String> MESSAGE_CONFIRMATION_FEATURES =
+ Arrays.asList(Namespace.CHAT_MARKERS, Namespace.DELIVERY_RECEIPTS);
+ private final List<String> MESSAGE_CORRECTION_FEATURES =
+ Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION);
+ private final List<String> PRIVACY_SENSITIVE =
+ Collections.singletonList(
+ "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
+ );
+ private final List<String> VOIP_NAMESPACES =
+ Arrays.asList(
+ Namespace.JINGLE_TRANSPORT_ICE_UDP,
+ Namespace.JINGLE_FEATURE_AUDIO,
+ Namespace.JINGLE_FEATURE_VIDEO,
+ Namespace.JINGLE_APPS_RTP,
+ Namespace.JINGLE_APPS_DTLS,
+ Namespace.JINGLE_MESSAGE);
// this is the runtime cache that stores disco information for all entities seen during a
// session
@@ -93,7 +141,7 @@ public class DiscoManager extends AbstractManager {
public ListenableFuture<Void> infoOrCache(
final Entity entity, final String node, final EntityCapabilities.Hash hash) {
final var cached = getDatabase().getInfoQuery(hash);
- if (cached != null) {
+ if (cached != null && Config.ENABLE_CAPS_CACHE) {
if (node == null || hash != null) {
this.put(entity.address, cached);
}
@@ -110,7 +158,7 @@ public class DiscoManager extends AbstractManager {
public ListenableFuture<InfoQuery> info(
final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) {
- final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node;
+ final var requestNode = hash != null ? hash.capabilityNode(node) : node;
final var iqRequest = new Iq(Iq.Type.GET);
iqRequest.setTo(entity.address);
final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery());
@@ -261,6 +309,114 @@ public class DiscoManager extends AbstractManager {
MoreExecutors.directExecutor());
}
+ ServiceDescription getServiceDescription() {
+ final var appSettings = new AppSettings(context);
+ final var account = connection.getAccount();
+ final ImmutableList.Builder<String> features = ImmutableList.builder();
+ features.add("http://jabber.org/protocol/xhtml-im");
+ features.add("urn:xmpp:bob");
+ if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
+ features.add(Namespace.MDS_DISPLAYED + "+notify");
+ }
+ if (appSettings.isConfirmMessages()) {
+ features.addAll(MESSAGE_CONFIRMATION_FEATURES);
+ }
+ if (appSettings.isAllowMessageCorrection()) {
+ features.addAll(MESSAGE_CORRECTION_FEATURES);
+ }
+ if (Config.supportOmemo()) {
+ features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
+ }
+ if (!appSettings.isUseTor() && !account.isOnion()) {
+ features.addAll(PRIVACY_SENSITIVE);
+ features.addAll(VOIP_NAMESPACES);
+ features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
+ }
+ if (appSettings.isBroadcastLastActivity()) {
+ features.add(Namespace.IDLE);
+ }
+ if (connection.getFeatures().bookmarks2()) {
+ features.add(Namespace.BOOKMARKS2 + "+notify");
+ } else {
+ features.add(Namespace.BOOKMARKS + "+notify");
+ }
+ return new ServiceDescription(
+ features.build(),
+ new ServiceDescription.Identity(BuildConfig.APP_NAME, "client", getIdentityType()));
+ }
+
+ String getIdentityVersion() {
+ return BuildConfig.VERSION_NAME;
+ }
+
+ String getIdentityType() {
+ if ("chromium".equals(android.os.Build.BRAND)) {
+ return "pc";
+ } else if (context.getResources().getBoolean(R.bool.is_device_table)) {
+ return "tablet";
+ } else {
+ return "phone";
+ }
+ }
+
+ public void handleVersionRequest(final Iq request) {
+ final var version = new Version();
+ version.setSoftwareName(context.getString(R.string.app_name));
+ version.setVersion(getIdentityVersion());
+ final StringBuilder os = new StringBuilder();
+ if ("chromium".equals(android.os.Build.BRAND)) {
+ os.append("Chrome OS");
+ } else {
+ os.append("Android");
+ }
+ os.append(" ");
+ os.append(android.os.Build.VERSION.RELEASE);
+ if (QuickConversationsService.isPlayStoreFlavor()) {
+ os.append(" (");
+ os.append(android.os.Build.BOARD);
+ os.append(", ");
+ os.append(android.os.Build.FINGERPRINT);
+ os.append(")");
+ }
+ version.setOs(os.toString());
+
+ Log.d(Config.LOGTAG, "responding to version request from " + request.getFrom());
+ connection.sendResultFor(request, version);
+ }
+
+ public void handleInfoQuery(final Iq request) {
+ final var infoQueryRequest = request.getExtension(InfoQuery.class);
+ final var nodeRequest = infoQueryRequest.getNode();
+ final ServiceDescription serviceDescription;
+ if (Strings.isNullOrEmpty(nodeRequest)) {
+ serviceDescription = getServiceDescription();
+ Log.d(Config.LOGTAG, "responding to disco request w/o node from " + request.getFrom());
+ } else {
+ final var hash = buildHashFromNode(nodeRequest);
+ final var cachedServiceDescription =
+ hash != null
+ ? getManager(PresenceManager.class).getCachedServiceDescription(hash)
+ : null;
+ if (cachedServiceDescription != null) {
+ Log.d(
+ Config.LOGTAG,
+ "responding to disco request from "
+ + request.getFrom()
+ + " to node "
+ + nodeRequest
+ + " using hash "
+ + hash.getClass().getName());
+ serviceDescription = cachedServiceDescription;
+ } else {
+ connection.sendErrorFor(request, Error.Type.CANCEL, new Condition.ItemNotFound());
+ return;
+ }
+ }
+ final var infoQuery = serviceDescription.asInfoQuery();
+ infoQuery.setNode(nodeRequest);
+ connection.sendResultFor(request, infoQuery);
+ }
+
public Map<Jid, InfoQuery> getServerItems() {
final var builder = new ImmutableMap.Builder<Jid, InfoQuery>();
final var domain = connection.getAccount().getDomain();
@@ -282,6 +438,11 @@ public class DiscoManager extends AbstractManager {
return builder.buildKeepingLast();
}
+ public boolean hasServerFeature(final String feature) {
+ final var infoQuery = this.get(getAccount().getDomain());
+ return infoQuery != null && infoQuery.hasFeature(feature);
+ }
+
private void put(final Jid address, final InfoQuery infoQuery) {
synchronized (this.entityInformation) {
this.entityInformation.put(address, infoQuery);
@@ -306,6 +467,22 @@ public class DiscoManager extends AbstractManager {
}
}
+ public void clear(final Jid address) {
+ synchronized (this.entityInformation) {
+ if (address.isFullJid()) {
+ this.entityInformation.remove(address);
+ } else {
+ final var iterator = this.entityInformation.entrySet().iterator();
+ while (iterator.hasNext()) {
+ final var entry = iterator.next();
+ if (entry.getKey().asBareJid().equals(address)) {
+ iterator.remove();
+ }
+ }
+ }
+ }
+ }
+
public static final class CapsHashMismatchException extends IllegalStateException {
public CapsHashMismatchException(final String message) {
super(message);
@@ -0,0 +1,48 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.services.XmppConnectionService;
+import im.conversations.android.xmpp.model.ping.Ping;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.concurrent.TimeoutException;
+
+public class PingManager extends AbstractManager {
+
+ public PingManager(final XmppConnectionService context, final XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public void ping() {
+ if (connection.getStreamFeatures().sm()) {
+ this.connection.sendRequestStanza();
+ } else {
+ this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping()));
+ }
+ }
+
+ public void ping(final Runnable runnable) {
+ final var pingFuture = this.connection.sendIqPacket(new Iq(Iq.Type.GET, new Ping()));
+ Futures.addCallback(
+ pingFuture,
+ new FutureCallback<Iq>() {
+ @Override
+ public void onSuccess(Iq result) {
+ runnable.run();
+ }
+
+ @Override
+ public void onFailure(final @NonNull Throwable t) {
+ if (t instanceof TimeoutException) {
+ return;
+ }
+ runnable.run();
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+}
@@ -0,0 +1,59 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.EntityCapabilities;
+import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.ServiceDescription;
+import im.conversations.android.xmpp.model.capabilties.Capabilities;
+import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities;
+import im.conversations.android.xmpp.model.pgp.Signed;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PresenceManager extends AbstractManager {
+
+ private final Map<EntityCapabilities.Hash, ServiceDescription> serviceDescriptions =
+ new HashMap<>();
+
+ public PresenceManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public Presence getPresence(final Presence.Availability availability, final boolean personal) {
+ final var account = connection.getAccount();
+ final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription();
+ final var infoQuery = serviceDiscoveryFeatures.asInfoQuery();
+ final var capsHash = EntityCapabilities.hash(infoQuery);
+ final var caps2Hash = EntityCapabilities2.hash(infoQuery);
+ serviceDescriptions.put(capsHash, serviceDiscoveryFeatures);
+ serviceDescriptions.put(caps2Hash, serviceDiscoveryFeatures);
+ final var capabilities = new Capabilities();
+ capabilities.setHash(caps2Hash);
+ final var legacyCapabilities = new LegacyCapabilities();
+ legacyCapabilities.setNode(DiscoManager.CAPABILITY_NODE);
+ legacyCapabilities.setHash(capsHash);
+ final var presence = new Presence();
+ presence.addExtension(capabilities);
+ presence.addExtension(legacyCapabilities);
+
+ if (personal) {
+ final String pgpSignature = account.getPgpSignature();
+ final String message = account.getPresenceStatusMessage();
+ presence.setAvailability(availability);
+ presence.setStatus(message);
+ if (pgpSignature != null) {
+ final var signed = new Signed();
+ signed.setContent(pgpSignature);
+ presence.addExtension(new Signed());
+ }
+ }
+ return presence;
+ }
+
+ public ServiceDescription getCachedServiceDescription(final EntityCapabilities.Hash hash) {
+ return this.serviceDescriptions.get(hash);
+ }
+}
@@ -0,0 +1,49 @@
+package im.conversations.android.xmpp;
+
+import com.google.common.collect.Collections2;
+import im.conversations.android.xmpp.model.disco.info.Feature;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import java.util.Collection;
+import java.util.List;
+
+public class ServiceDescription {
+ public final List<String> features;
+ public final Identity identity;
+
+ public ServiceDescription(List<String> features, Identity identity) {
+ this.features = features;
+ this.identity = identity;
+ }
+
+ public InfoQuery asInfoQuery() {
+ final var infoQuery = new InfoQuery();
+ final Collection<Feature> features =
+ Collections2.transform(
+ this.features,
+ sf -> {
+ final var feature = new Feature();
+ feature.setVar(sf);
+ return feature;
+ });
+ infoQuery.addExtensions(features);
+ final var identity =
+ infoQuery.addExtension(
+ new im.conversations.android.xmpp.model.disco.info.Identity());
+ identity.setIdentityName(this.identity.name);
+ identity.setCategory(this.identity.category);
+ identity.setType(this.identity.type);
+ return infoQuery;
+ }
+
+ public static class Identity {
+ public final String name;
+ public final String category;
+ public final String type;
+
+ public Identity(String name, String category, String type) {
+ this.name = name;
+ this.category = category;
+ this.type = type;
+ }
+ }
+}
@@ -33,6 +33,11 @@ public class Data extends Extension {
return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null);
}
+ public String getValue(final String name) {
+ final var field = getFieldByName(name);
+ return field == null ? null : field.getValue();
+ }
+
private void addField(final String name, final Object value) {
addField(name, value, null);
}
@@ -1,12 +1,11 @@
package im.conversations.android.xmpp.model.stanza;
import com.google.common.base.Strings;
-
import eu.siacs.conversations.xml.Element;
-
import im.conversations.android.annotation.XmlElement;
+import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.error.Error;
-
+import java.util.Arrays;
import java.util.Locale;
@XmlElement
@@ -23,6 +22,11 @@ public class Iq extends Stanza {
this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT));
}
+ public Iq(final Type type, final Extension... extensions) {
+ this(type);
+ this.addExtensions(Arrays.asList(extensions));
+ }
+
// TODO get rid of timeout
public enum Type {
SET,
@@ -9,19 +9,18 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
import im.conversations.android.xmpp.model.stanza.Iq;
-public class BindProcessor implements Runnable {
+public class BindProcessor extends XmppConnection.Delegate implements Runnable {
private final XmppConnectionService service;
- private final Account account;
- public BindProcessor(XmppConnectionService service, Account account) {
+ public BindProcessor(final XmppConnectionService service, final XmppConnection connection) {
+ super(service, connection);
this.service = service;
- this.account = account;
}
@Override
public void run() {
- final XmppConnection connection = account.getXmppConnection();
+ final var account = connection.getAccount();
final var features = connection.getFeatures();
service.cancelAvatarFetches(account);
final boolean loggedInSuccessfully =
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="default_resource">Tablet</string>
<bool name="portrait_only">false</bool>
-</resources>
+</resources>
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="is_device_table">true</bool>
+</resources>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <string name="default_resource">Phone</string>
<bool name="portrait_only">true</bool>
<bool name="enter_is_send">false</bool>
<bool name="notifications_from_strangers">true</bool>
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="is_device_table">false</bool>
+</resources>