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 +