diff --git a/conversations.doap b/conversations.doap
index e402ff875d9811b2e758457e9aceaa4b5a441ef4..71f508bc889eab383b735fad06a9134fef5a019b 100644
--- a/conversations.doap
+++ b/conversations.doap
@@ -399,6 +399,13 @@
0.4.0
+
+
+
+ complete
+ 0.3.2
+
+
diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java
index 40aad2758609f26e4397e3cbed172d18b5cc2026..7318091ea0d97689fa4b2824f03070435a39b434 100644
--- a/src/main/java/eu/siacs/conversations/AppSettings.java
+++ b/src/main/java/eu/siacs/conversations/AppSettings.java
@@ -130,6 +130,18 @@ public class AppSettings {
return getBooleanPreference(ALIGN_START, R.bool.align_start);
}
+ public boolean isConfirmMessages() {
+ return getBooleanPreference(CONFIRM_MESSAGES, R.bool.confirm_messages);
+ }
+
+ public boolean isAllowMessageCorrection() {
+ return getBooleanPreference(ALLOW_MESSAGE_CORRECTION, R.bool.allow_message_correction);
+ }
+
+ public boolean isBroadcastLastActivity() {
+ return getBooleanPreference(BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
+ }
+
public boolean isUseTor() {
return QuickConversationsService.isConversations()
&& getBooleanPreference(USE_TOR, R.bool.use_tor);
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 628b3c3531b389c3f6dcd623bd241f2d61e1e519..571c02c348de4da5bcac6dea2c1e0a52323f47a0 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/src/main/java/eu/siacs/conversations/Config.java
@@ -114,6 +114,8 @@ public final class Config {
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean USE_JINGLE_MESSAGE_INIT = true;
+ public static final boolean ENABLE_CAPS_CACHE = true;
+
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
public static final boolean BACKGROUND_STANZA_LOGGING =
diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
index de092f5978126a75e42ae4298b6c95bb712e4f58..34018f6b85f7a91119ac236057c84f4a30287f7d 100644
--- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java
@@ -1,61 +1,15 @@
package eu.siacs.conversations.generator;
-import android.util.Base64;
import eu.siacs.conversations.BuildConfig;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.XmppConnection;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
public abstract class AbstractGenerator {
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
- private final String[] STATIC_FEATURES = {
- Namespace.JINGLE,
- Namespace.JINGLE_APPS_FILE_TRANSFER,
- Namespace.JINGLE_TRANSPORTS_S5B,
- Namespace.JINGLE_TRANSPORTS_IBB,
- Namespace.JINGLE_ENCRYPTED_TRANSPORT,
- Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
- "http://jabber.org/protocol/muc",
- "jabber:x:conference",
- Namespace.OOB,
- "http://jabber.org/protocol/caps",
- "http://jabber.org/protocol/disco#info",
- "urn:xmpp:avatar:metadata+notify",
- Namespace.NICK + "+notify",
- "urn:xmpp:ping",
- "jabber:iq:version",
- "http://jabber.org/protocol/chatstates",
- Namespace.REACTIONS
- };
- private final String[] MESSAGE_CONFIRMATION_FEATURES = {
- "urn:xmpp:chat-markers:0", "urn:xmpp:receipts"
- };
- private final String[] MESSAGE_CORRECTION_FEATURES = {"urn:xmpp:message-correct:0"};
- private final String[] PRIVACY_SENSITIVE = {
- "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
- };
- private final String[] VOIP_NAMESPACES = {
- Namespace.JINGLE_TRANSPORT_ICE_UDP,
- Namespace.JINGLE_FEATURE_AUDIO,
- Namespace.JINGLE_FEATURE_VIDEO,
- Namespace.JINGLE_APPS_RTP,
- Namespace.JINGLE_APPS_DTLS,
- Namespace.JINGLE_MESSAGE
- };
+
protected XmppConnectionService mXmppConnectionService;
AbstractGenerator(XmppConnectionService service) {
@@ -70,70 +24,4 @@ public abstract class AbstractGenerator {
String getIdentityVersion() {
return BuildConfig.VERSION_NAME;
}
-
- String getIdentityName() {
- return BuildConfig.APP_NAME;
- }
-
- String getIdentityType() {
- if ("chromium".equals(android.os.Build.BRAND)) {
- return "pc";
- } else {
- return mXmppConnectionService.getString(R.string.default_resource).toLowerCase();
- }
- }
-
- String getCapHash(final Account account) {
- StringBuilder s = new StringBuilder();
- s.append("client/")
- .append(getIdentityType())
- .append("//")
- .append(getIdentityName())
- .append('<');
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- return null;
- }
-
- for (String feature : getFeatures(account)) {
- s.append(feature).append('<');
- }
- final byte[] sha1 = md.digest(s.toString().getBytes());
- return Base64.encodeToString(sha1, Base64.NO_WRAP);
- }
-
- public List getFeatures(final Account account) {
- final XmppConnection connection = account.getXmppConnection();
- final ArrayList features = new ArrayList<>(Arrays.asList(STATIC_FEATURES));
- if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
- features.add(Namespace.MDS_DISPLAYED + "+notify");
- }
- if (mXmppConnectionService.confirmMessages()) {
- features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
- }
- if (mXmppConnectionService.allowMessageCorrection()) {
- features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES));
- }
- if (Config.supportOmemo()) {
- features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
- }
- if (!mXmppConnectionService.useTorToConnect() && !account.isOnion()) {
- features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
- features.addAll(Arrays.asList(VOIP_NAMESPACES));
- features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
- }
- if (mXmppConnectionService.broadcastLastActivity()) {
- features.add(Namespace.IDLE);
- }
- if (connection != null && connection.getFeatures().bookmarks2()) {
- features.add(Namespace.BOOKMARKS2 + "+notify");
- } else {
- features.add(Namespace.BOOKMARKS + "+notify");
- }
-
- Collections.sort(features);
- return features;
- }
}
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index 60da01c49a82514d04274e69dd5e25966b246e9c..e2b2638efbab5563079a606d68afaf4cf05fd7e2 100644
--- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -39,22 +39,6 @@ public class IqGenerator extends AbstractGenerator {
super(service);
}
- public Iq discoResponse(final Account account, final Iq request) {
- final var packet = new Iq(Iq.Type.RESULT);
- packet.setId(request.getId());
- packet.setTo(request.getFrom());
- final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info");
- query.setAttribute("node", request.query().getAttribute("node"));
- final Element identity = query.addChild("identity");
- identity.setAttribute("category", "client");
- identity.setAttribute("type", getIdentityType());
- identity.setAttribute("name", getIdentityName());
- for (final String feature : getFeatures(account)) {
- query.addChild("feature").setAttribute("var", feature);
- }
- return packet;
- }
-
public Iq versionResponse(final Iq request) {
final var packet = request.generateResponse(Iq.Type.RESULT);
Element query = packet.query("jabber:iq:version");
diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
index 9fca46bd2fca91c8c0d144b0386a8c15ce9888f4..60fa01872680da3e9e2248e6b9056887dbc9a1e3 100644
--- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java
@@ -5,8 +5,8 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.model.stanza.Presence;
public class PresenceGenerator extends AbstractGenerator {
@@ -66,25 +66,11 @@ public class PresenceGenerator extends AbstractGenerator {
public im.conversations.android.xmpp.model.stanza.Presence selfPresence(
final Account account, final Presence.Availability status, final boolean personal) {
- final im.conversations.android.xmpp.model.stanza.Presence packet =
- new im.conversations.android.xmpp.model.stanza.Presence();
- if (personal) {
- final String sig = account.getPgpSignature();
- final String message = account.getPresenceStatusMessage();
- packet.setAvailability(status);
- packet.setStatus(message);
- if (sig != null && mXmppConnectionService.getPgpEngine() != null) {
- packet.addChild("x", "jabber:x:signed").setContent(sig);
- }
- }
- final String capHash = getCapHash(account);
- if (capHash != null) {
- Element cap = packet.addChild("c", "http://jabber.org/protocol/caps");
- cap.setAttribute("hash", "sha-1");
- cap.setAttribute("node", "http://conversations.im");
- cap.setAttribute("ver", capHash);
+ final var connection = account.getXmppConnection();
+ if (connection == null) {
+ return new Presence();
}
- return packet;
+ return connection.getManager(PresenceManager.class).getPresence(status, personal);
}
public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) {
diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java
index 7a7f9c3a0aa031e431991ef9398cde8b02e1d9c1..37809e5c4402145ee42ecd54bf0ab329f4b8aefb 100644
--- a/src/main/java/eu/siacs/conversations/parser/IqParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java
@@ -17,6 +17,8 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
@@ -412,6 +414,7 @@ public class IqParser extends AbstractParser implements Consumer {
@Override
public void accept(final Iq packet) {
+ final var connection = account.getXmppConnection();
final boolean isGet = packet.getType() == Iq.Type.GET;
if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) {
return;
@@ -439,7 +442,7 @@ public class IqParser extends AbstractParser implements Consumer {
// Otherwise, just update the existing blocklist.
if (packet.getType() == Iq.Type.RESULT) {
account.clearBlocklist();
- account.getXmppConnection().getFeatures().setBlockListRequested(true);
+ connection.getFeatures().setBlockListRequested(true);
}
if (items != null) {
final Collection jids = new ArrayList<>(items.size());
@@ -499,10 +502,8 @@ public class IqParser extends AbstractParser implements Consumer {
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")
|| packet.hasChild("close", "http://jabber.org/protocol/ibb")) {
mXmppConnectionService.getJingleConnectionManager().deliverIbbPacket(account, packet);
- } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
- final Iq response =
- mXmppConnectionService.getIqGenerator().discoResponse(account, packet);
- mXmppConnectionService.sendIqPacket(account, response, null);
+ } else if (packet.hasExtension(InfoQuery.class)) {
+ connection.getManager(DiscoManager.class).handleInfoQuery(packet);
} else if (packet.hasChild("query", "jabber:iq:version") && isGet) {
final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
mXmppConnectionService.sendIqPacket(account, response, null);
@@ -545,7 +546,7 @@ public class IqParser extends AbstractParser implements Consumer {
final Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas");
- account.getXmppConnection().sendIqPacket(response, null);
+ connection.sendIqPacket(response, null);
}
}
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 5ec73cc419c4ecf4064aa1a9bc789f97ea05afe9..0181a2f76509df3ffd6f63a58c061379cabe5e61 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -5527,11 +5527,11 @@ public class XmppConnectionService extends Service {
}
public boolean confirmMessages() {
- return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
+ return appSettings.isConfirmMessages();
}
public boolean allowMessageCorrection() {
- return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
+ return appSettings.isAllowMessageCorrection();
}
public boolean sendChatStates() {
@@ -5539,12 +5539,11 @@ public class XmppConnectionService extends Service {
}
public boolean useTorToConnect() {
- return QuickConversationsService.isConversations()
- && getBooleanPreference("use_tor", R.bool.use_tor);
+ return appSettings.isUseTor();
}
public boolean broadcastLastActivity() {
- return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
+ return appSettings.isBroadcastLastActivity();
}
public int unreadCount() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index 987ffb1b0330f153d7179084ef2f83851c639774..5ddce911ef6ae77c8b8c5809c319c22692b0d910 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -74,10 +74,12 @@ import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import eu.siacs.conversations.xmpp.manager.AbstractManager;
import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import eu.siacs.conversations.xmpp.manager.PresenceManager;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.AuthenticationFailure;
import im.conversations.android.xmpp.model.AuthenticationRequest;
import im.conversations.android.xmpp.model.AuthenticationStreamFeature;
+import im.conversations.android.xmpp.model.Extension;
import im.conversations.android.xmpp.model.StreamElement;
import im.conversations.android.xmpp.model.bind2.Bind;
import im.conversations.android.xmpp.model.bind2.Bound;
@@ -224,6 +226,9 @@ public class XmppConnection implements Runnable {
.put(
DiscoManager.class,
new DiscoManager(service.getApplicationContext(), this))
+ .put(
+ PresenceManager.class,
+ new PresenceManager(service.getApplicationContext(), this))
.build();
}
@@ -2562,6 +2567,36 @@ public class XmppConnection implements Runnable {
return packet.getId();
}
+ public void sendResultFor(final Iq request, final Extension... extensions) {
+ final var from = request.getFrom();
+ final var id = request.getId();
+ final var response = new Iq(Iq.Type.RESULT);
+ response.setTo(from);
+ response.setId(id);
+ for (final Extension extension : extensions) {
+ response.addExtension(extension);
+ }
+ this.sendPacket(response);
+ }
+
+ public void sendErrorFor(
+ final Iq request,
+ final im.conversations.android.xmpp.model.error.Error.Type type,
+ final Condition condition,
+ final im.conversations.android.xmpp.model.error.Error.Extension... extensions) {
+ final var from = request.getFrom();
+ final var id = request.getId();
+ final var response = new Iq(Iq.Type.ERROR);
+ response.setTo(from);
+ response.setId(id);
+ final var error =
+ response.addExtension(new im.conversations.android.xmpp.model.error.Error());
+ error.setType(type);
+ error.setCondition(condition);
+ error.addExtensions(extensions);
+ this.sendPacket(response);
+ }
+
public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) {
this.sendPacket(packet);
}
diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java
index e773d617bcd8c0f0dcab8794fc0a94788b7ad2dd..c615010a58ebeb63af092eadb651d31604a53740 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java
@@ -4,26 +4,35 @@ import android.content.Context;
import android.util.Log;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.BaseEncoding;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.AppSettings;
+import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.EntityCapabilities;
import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.ServiceDescription;
import im.conversations.android.xmpp.model.Hash;
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.disco.items.Item;
import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
+import im.conversations.android.xmpp.model.error.Condition;
+import im.conversations.android.xmpp.model.error.Error;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -35,6 +44,43 @@ public class DiscoManager extends AbstractManager {
public static final String CAPABILITY_NODE = "http://conversations.im";
+ private final List STATIC_FEATURES =
+ Arrays.asList(
+ Namespace.JINGLE,
+ Namespace.JINGLE_APPS_FILE_TRANSFER,
+ Namespace.JINGLE_TRANSPORTS_S5B,
+ Namespace.JINGLE_TRANSPORTS_IBB,
+ Namespace.JINGLE_ENCRYPTED_TRANSPORT,
+ Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
+ "http://jabber.org/protocol/muc",
+ "jabber:x:conference",
+ Namespace.OOB,
+ Namespace.ENTITY_CAPABILITIES,
+ Namespace.ENTITY_CAPABILITIES_2,
+ Namespace.DISCO_INFO,
+ "urn:xmpp:avatar:metadata+notify",
+ Namespace.NICK + "+notify",
+ Namespace.PING,
+ Namespace.VERSION,
+ Namespace.CHAT_STATES,
+ Namespace.REACTIONS);
+ private final List MESSAGE_CONFIRMATION_FEATURES =
+ Arrays.asList(Namespace.CHAT_MARKERS, Namespace.DELIVERY_RECEIPTS);
+ private final List MESSAGE_CORRECTION_FEATURES =
+ Collections.singletonList(Namespace.LAST_MESSAGE_CORRECTION);
+ private final List PRIVACY_SENSITIVE =
+ Collections.singletonList(
+ "urn:xmpp:time" // XEP-0202: Entity Time leaks time zone
+ );
+ private final List VOIP_NAMESPACES =
+ Arrays.asList(
+ Namespace.JINGLE_TRANSPORT_ICE_UDP,
+ Namespace.JINGLE_FEATURE_AUDIO,
+ Namespace.JINGLE_FEATURE_VIDEO,
+ Namespace.JINGLE_APPS_RTP,
+ Namespace.JINGLE_APPS_DTLS,
+ Namespace.JINGLE_MESSAGE);
+
// this is the runtime cache that stores disco information for all entities seen during a
// session
@@ -92,7 +138,7 @@ public class DiscoManager extends AbstractManager {
public ListenableFuture infoOrCache(
final Entity entity, final String node, final EntityCapabilities.Hash hash) {
final var cached = getDatabase().getInfoQuery(hash);
- if (cached != null) {
+ if (cached != null && Config.ENABLE_CAPS_CACHE) {
if (node == null || hash != null) {
this.put(entity.address, cached);
}
@@ -109,7 +155,7 @@ public class DiscoManager extends AbstractManager {
public ListenableFuture info(
final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) {
- final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node;
+ final var requestNode = hash != null ? hash.capabilityNode(node) : node;
final var iqRequest = new Iq(Iq.Type.GET);
iqRequest.setTo(entity.address);
final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery());
@@ -260,6 +306,87 @@ public class DiscoManager extends AbstractManager {
MoreExecutors.directExecutor());
}
+ ServiceDescription getServiceDescription() {
+ final var appSettings = new AppSettings(context);
+ final var account = connection.getAccount();
+ final ImmutableList.Builder features = ImmutableList.builder();
+ if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION) {
+ features.add(Namespace.MDS_DISPLAYED + "+notify");
+ }
+ if (appSettings.isConfirmMessages()) {
+ features.addAll(MESSAGE_CONFIRMATION_FEATURES);
+ }
+ if (appSettings.isAllowMessageCorrection()) {
+ features.addAll(MESSAGE_CORRECTION_FEATURES);
+ }
+ if (Config.supportOmemo()) {
+ features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
+ }
+ if (!appSettings.isUseTor() && !account.isOnion()) {
+ features.addAll(PRIVACY_SENSITIVE);
+ features.addAll(VOIP_NAMESPACES);
+ features.add(Namespace.JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL);
+ }
+ if (appSettings.isBroadcastLastActivity()) {
+ features.add(Namespace.IDLE);
+ }
+ if (connection.getFeatures().bookmarks2()) {
+ features.add(Namespace.BOOKMARKS2 + "+notify");
+ } else {
+ features.add(Namespace.BOOKMARKS + "+notify");
+ }
+ return new ServiceDescription(
+ features.build(),
+ new ServiceDescription.Identity(BuildConfig.APP_NAME, "client", getIdentityType()));
+ }
+
+ String getIdentityVersion() {
+ return BuildConfig.VERSION_NAME;
+ }
+
+ String getIdentityType() {
+ if ("chromium".equals(android.os.Build.BRAND)) {
+ return "pc";
+ } else if (context.getResources().getBoolean(R.bool.is_device_table)) {
+ return "tablet";
+ } else {
+ return "phone";
+ }
+ }
+
+ public void handleInfoQuery(final Iq request) {
+ final var infoQueryRequest = request.getExtension(InfoQuery.class);
+ final var nodeRequest = infoQueryRequest.getNode();
+ final ServiceDescription serviceDescription;
+ if (Strings.isNullOrEmpty(nodeRequest)) {
+ serviceDescription = getServiceDescription();
+ Log.d(Config.LOGTAG, "responding to disco request w/o node from " + request.getFrom());
+ } else {
+ final var hash = buildHashFromNode(nodeRequest);
+ final var cachedServiceDescription =
+ hash != null
+ ? getManager(PresenceManager.class).getCachedServiceDescription(hash)
+ : null;
+ if (cachedServiceDescription != null) {
+ Log.d(
+ Config.LOGTAG,
+ "responding to disco request from "
+ + request.getFrom()
+ + " to node "
+ + nodeRequest
+ + " using hash "
+ + hash.getClass().getName());
+ serviceDescription = cachedServiceDescription;
+ } else {
+ connection.sendErrorFor(request, Error.Type.CANCEL, new Condition.ItemNotFound());
+ return;
+ }
+ }
+ final var infoQuery = serviceDescription.asInfoQuery();
+ infoQuery.setNode(nodeRequest);
+ connection.sendResultFor(request, infoQuery);
+ }
+
public Map getServerItems() {
final var builder = new ImmutableMap.Builder();
final var domain = connection.getAccount().getDomain();
diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..14de9f576f8590bb886091717411d5005af247b3
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/xmpp/manager/PresenceManager.java
@@ -0,0 +1,58 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.EntityCapabilities;
+import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.ServiceDescription;
+import im.conversations.android.xmpp.model.capabilties.Capabilities;
+import im.conversations.android.xmpp.model.capabilties.LegacyCapabilities;
+import im.conversations.android.xmpp.model.pgp.Signed;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PresenceManager extends AbstractManager {
+
+ private final Map serviceDescriptions =
+ new HashMap<>();
+
+ public PresenceManager(Context context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public Presence getPresence(final Presence.Availability availability, final boolean personal) {
+ final var account = connection.getAccount();
+ final var serviceDiscoveryFeatures = getManager(DiscoManager.class).getServiceDescription();
+ final var infoQuery = serviceDiscoveryFeatures.asInfoQuery();
+ final var capsHash = EntityCapabilities.hash(infoQuery);
+ final var caps2Hash = EntityCapabilities2.hash(infoQuery);
+ serviceDescriptions.put(capsHash, serviceDiscoveryFeatures);
+ serviceDescriptions.put(caps2Hash, serviceDiscoveryFeatures);
+ final var capabilities = new Capabilities();
+ capabilities.setHash(caps2Hash);
+ final var legacyCapabilities = new LegacyCapabilities();
+ legacyCapabilities.setNode(DiscoManager.CAPABILITY_NODE);
+ legacyCapabilities.setHash(capsHash);
+ final var presence = new Presence();
+ presence.addExtension(capabilities);
+ presence.addExtension(legacyCapabilities);
+
+ if (personal) {
+ final String pgpSignature = account.getPgpSignature();
+ final String message = account.getPresenceStatusMessage();
+ presence.setAvailability(availability);
+ presence.setStatus(message);
+ if (pgpSignature != null) {
+ final var signed = new Signed();
+ signed.setContent(pgpSignature);
+ presence.addExtension(new Signed());
+ }
+ }
+ return presence;
+ }
+
+ public ServiceDescription getCachedServiceDescription(final EntityCapabilities.Hash hash) {
+ return this.serviceDescriptions.get(hash);
+ }
+}
diff --git a/src/main/java/im/conversations/android/xmpp/ServiceDescription.java b/src/main/java/im/conversations/android/xmpp/ServiceDescription.java
new file mode 100644
index 0000000000000000000000000000000000000000..283a0a615650ffd92d2197733af79bc4d3173082
--- /dev/null
+++ b/src/main/java/im/conversations/android/xmpp/ServiceDescription.java
@@ -0,0 +1,49 @@
+package im.conversations.android.xmpp;
+
+import com.google.common.collect.Collections2;
+import im.conversations.android.xmpp.model.disco.info.Feature;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import java.util.Collection;
+import java.util.List;
+
+public class ServiceDescription {
+ public final List features;
+ public final Identity identity;
+
+ public ServiceDescription(List features, Identity identity) {
+ this.features = features;
+ this.identity = identity;
+ }
+
+ public InfoQuery asInfoQuery() {
+ final var infoQuery = new InfoQuery();
+ final Collection features =
+ Collections2.transform(
+ this.features,
+ sf -> {
+ final var feature = new Feature();
+ feature.setVar(sf);
+ return feature;
+ });
+ infoQuery.addExtensions(features);
+ final var identity =
+ infoQuery.addExtension(
+ new im.conversations.android.xmpp.model.disco.info.Identity());
+ identity.setIdentityName(this.identity.name);
+ identity.setCategory(this.identity.category);
+ identity.setType(this.identity.type);
+ return infoQuery;
+ }
+
+ public static class Identity {
+ public final String name;
+ public final String category;
+ public final String type;
+
+ public Identity(String name, String category, String type) {
+ this.name = name;
+ this.category = category;
+ this.type = type;
+ }
+ }
+}
diff --git a/src/main/res/values-sw600dp/defaults.xml b/src/main/res/values-sw600dp/defaults.xml
index b22a4ca8ab3718760fcdbe08a6b05ef59155fd43..c06e0147ee732340df3dba9567aa90076c64aa47 100644
--- a/src/main/res/values-sw600dp/defaults.xml
+++ b/src/main/res/values-sw600dp/defaults.xml
@@ -1,5 +1,4 @@
- Tablet
false
-
\ No newline at end of file
+
diff --git a/src/main/res/values-sw600dp/device.xml b/src/main/res/values-sw600dp/device.xml
new file mode 100644
index 0000000000000000000000000000000000000000..76b11950560dd1b7d881600c0372ff3d4ecd7563
--- /dev/null
+++ b/src/main/res/values-sw600dp/device.xml
@@ -0,0 +1,4 @@
+
+
+ true
+
diff --git a/src/main/res/values/defaults.xml b/src/main/res/values/defaults.xml
index eb01b23a3d1c02cdb2eae30b495dc8e09e15e749..1f1b87c4732421f88da703a160bf1df3275c9094 100644
--- a/src/main/res/values/defaults.xml
+++ b/src/main/res/values/defaults.xml
@@ -1,6 +1,5 @@
- Phone
true
false
true
diff --git a/src/main/res/values/device.xml b/src/main/res/values/device.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f99fe3e59674a1876bf0e12233b89077de641667
--- /dev/null
+++ b/src/main/res/values/device.xml
@@ -0,0 +1,4 @@
+
+
+ false
+