Detailed changes
@@ -399,6 +399,13 @@
<xmpp:version>0.4.0</xmpp:version>
</xmpp:SupportedXep>
</implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0390.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>0.3.2</xmpp:version>
+ </xmpp:SupportedXep>
+ </implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0391.html"/>
@@ -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);
@@ -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 =
@@ -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<String> getFeatures(final Account account) {
- final XmppConnection connection = account.getXmppConnection();
- final ArrayList<String> 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;
- }
}
@@ -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");
@@ -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) {
@@ -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<Iq> {
@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<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());
@@ -499,10 +502,8 @@ 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.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<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);
}
}
}
@@ -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() {
@@ -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);
}
@@ -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<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
@@ -92,7 +138,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);
}
@@ -109,7 +155,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());
@@ -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<String> 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<Jid, InfoQuery> getServerItems() {
final var builder = new ImmutableMap.Builder<Jid, InfoQuery>();
final var domain = connection.getAccount().getDomain();
@@ -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<EntityCapabilities.Hash, ServiceDescription> 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);
+ }
+}
@@ -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;
+ }
+ }
+}
@@ -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>