Detailed changes
@@ -91,6 +91,9 @@ dependencies {
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.2.0'
testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.robolectric:robolectric:4.14.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+
}
ext {
@@ -27,6 +27,7 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -101,7 +102,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
- private Presence.Status presenceStatus;
+ private im.conversations.android.xmpp.model.stanza.Presence.Availability presenceStatus;
private String presenceStatusMessage;
private String pinnedMechanism;
private String pinnedChannelBinding;
@@ -121,7 +122,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
null,
null,
Resolver.XMPP_PORT_STARTTLS,
- Presence.Status.ONLINE,
+ im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
null,
null,
null,
@@ -140,7 +141,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
String displayName,
String hostname,
int port,
- final Presence.Status status,
+ final im.conversations.android.xmpp.model.stanza.Presence.Availability status,
String statusMessage,
final String pinnedMechanism,
final String pinnedChannelBinding,
@@ -203,7 +204,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)),
cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)),
cursor.getInt(cursor.getColumnIndexOrThrow(PORT)),
- Presence.Status.fromShowString(
+ im.conversations.android.xmpp.model.stanza.Presence.Availability.valueOfShown(
cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)),
cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
@@ -451,11 +452,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
&& getXmppConnection().getAttempt() >= 3;
}
- public Presence.Status getPresenceStatus() {
+ public im.conversations.android.xmpp.model.stanza.Presence.Availability getPresenceStatus() {
return this.presenceStatus;
}
- public void setPresenceStatus(Presence.Status status) {
+ public void setPresenceStatus(
+ im.conversations.android.xmpp.model.stanza.Presence.Availability status) {
this.presenceStatus = status;
}
@@ -584,9 +586,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public int activeDevicesWithRtpCapability() {
+ final var connection = getXmppConnection();
+ if (connection == null) {
+ return 0;
+ }
int i = 0;
- for (Presence presence : getSelfContact().getPresences().getPresences()) {
- if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
+ for (String resource : getSelfContact().getPresences().getPresencesMap().keySet()) {
+ final var jid =
+ Strings.isNullOrEmpty(resource)
+ ? getJid().asBareJid()
+ : getJid().withResource(resource);
+ if (RtpCapability.check(connection.getManager(DiscoManager.class).get(jid))
+ != RtpCapability.Capability.NONE) {
i++;
}
}
@@ -17,6 +17,7 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import eu.siacs.conversations.xmpp.pep.Avatar;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@@ -55,7 +56,7 @@ public class Contact implements ListItem, Blockable {
private String photoUri;
private final JSONObject keys;
private JSONArray groups = new JSONArray();
- private final Presences presences = new Presences();
+ private final Presences presences = new Presences(this);
protected Account account;
protected Avatar avatar;
@@ -275,7 +276,7 @@ public class Contact implements ListItem, Blockable {
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
}
- public Presence.Status getShownStatus() {
+ public im.conversations.android.xmpp.model.stanza.Presence.Availability getShownStatus() {
return this.presences.getShownStatus();
}
@@ -14,9 +14,10 @@ import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
-import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.forms.Field;
import eu.siacs.conversations.xmpp.pep.Avatar;
+import im.conversations.android.xmpp.model.data.Data;
+import im.conversations.android.xmpp.model.data.Field;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -43,7 +44,7 @@ public class MucOptions {
public OnRenameListener onRenameListener = null;
private boolean mAutoPushConfiguration = true;
private final Account account;
- private ServiceDiscoveryResult serviceDiscoveryResult;
+ private InfoQuery infoQuery;
private boolean isOnline = false;
private Error error = Error.NONE;
private User self;
@@ -110,15 +111,24 @@ public class MucOptions {
return MessageArchiveService.Version.has(getFeatures());
}
- public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) {
- this.serviceDiscoveryResult = serviceDiscoveryResult;
+ private InfoQuery getServiceDiscoveryResult() {
+ return this.infoQuery;
+ }
+
+ public boolean updateConfiguration(final InfoQuery serviceDiscoveryResult) {
+ this.infoQuery = serviceDiscoveryResult;
+ final var roomInfo = getRoomInfoForm();
String name;
- Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname");
+ Field roomConfigName =
+ roomInfo == null ? null : roomInfo.getFieldByName("muc#roomconfig_roomname");
if (roomConfigName != null) {
name = roomConfigName.getValue();
} else {
final var identities = serviceDiscoveryResult.getIdentities();
- final String identityName = !identities.isEmpty() ? identities.get(0).getName() : null;
+ final String identityName =
+ !identities.isEmpty()
+ ? Iterables.getFirst(identities, null).getIdentityName()
+ : null;
final Jid jid = conversation.getJid();
if (identityName != null && !identityName.equals(jid == null ? null : jid.getLocal())) {
name = identityName;
@@ -140,11 +150,11 @@ public class MucOptions {
}
private Data getRoomInfoForm() {
- final List<Data> forms =
- serviceDiscoveryResult == null
- ? Collections.emptyList()
- : serviceDiscoveryResult.forms;
- return forms.isEmpty() ? new Data() : forms.get(0);
+ final var serviceDiscoveryResult = getServiceDiscoveryResult();
+ return serviceDiscoveryResult == null
+ ? null
+ : serviceDiscoveryResult.getServiceDiscoveryExtension(
+ "http://jabber.org/protocol/muc#roominfo");
}
public String getAvatar() {
@@ -152,8 +162,9 @@ public class MucOptions {
}
public boolean hasFeature(String feature) {
- return this.serviceDiscoveryResult != null
- && this.serviceDiscoveryResult.features.contains(feature);
+ final var serviceDiscoveryResult = getServiceDiscoveryResult();
+ return serviceDiscoveryResult != null
+ && serviceDiscoveryResult.getFeatureStrings().contains(feature);
}
public boolean hasVCards() {
@@ -211,9 +222,10 @@ public class MucOptions {
return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
}
- public List<String> getFeatures() {
- return this.serviceDiscoveryResult != null
- ? this.serviceDiscoveryResult.features
+ public Collection<String> getFeatures() {
+ final var serviceDiscoveryResult = getServiceDiscoveryResult();
+ return serviceDiscoveryResult != null
+ ? serviceDiscoveryResult.getFeatureStrings()
: Collections.emptyList();
}
@@ -1,101 +0,0 @@
-package eu.siacs.conversations.entities;
-
-import androidx.annotation.NonNull;
-
-import java.util.Locale;
-
-import eu.siacs.conversations.xml.Element;
-
-public class Presence implements Comparable<Presence> {
-
- public enum Status {
- CHAT, ONLINE, AWAY, XA, DND, OFFLINE;
-
- public String toShowString() {
- switch(this) {
- case CHAT: return "chat";
- case AWAY: return "away";
- case XA: return "xa";
- case DND: return "dnd";
- }
- return null;
- }
-
- public static Status fromShowString(String show) {
- if (show == null) {
- return ONLINE;
- } else {
- switch (show.toLowerCase(Locale.US)) {
- case "away":
- return AWAY;
- case "xa":
- return XA;
- case "dnd":
- return DND;
- case "chat":
- return CHAT;
- default:
- return ONLINE;
- }
- }
- }
- }
-
- private final Status status;
- private ServiceDiscoveryResult disco;
- private final String ver;
- private final String hash;
- private final String node;
- private final String message;
-
- private Presence(Status status, String ver, String hash, String node, String message) {
- this.status = status;
- this.ver = ver;
- this.hash = hash;
- this.node = node;
- this.message = message;
- }
-
- public static Presence parse(String show, Element caps, String message) {
- final String hash = caps == null ? null : caps.getAttribute("hash");
- final String ver = caps == null ? null : caps.getAttribute("ver");
- final String node = caps == null ? null : caps.getAttribute("node");
- return new Presence(Status.fromShowString(show), ver, hash, node, message);
- }
-
- public int compareTo(@NonNull Presence other) {
- return this.status.compareTo(other.status);
- }
-
- public Status getStatus() {
- return this.status;
- }
-
- public boolean hasCaps() {
- return ver != null && hash != null;
- }
-
- public String getVer() {
- return this.ver;
- }
-
- public String getNode() {
- return this.node;
- }
-
- public String getHash() {
- return this.hash;
- }
-
- public String getMessage() {
- return this.message;
- }
-
- public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) {
- this.disco = disco;
- }
-
- public ServiceDiscoveryResult getServiceDiscoveryResult() {
- return disco;
- }
-}
@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Objects;
public class PresenceTemplate extends AbstractEntity {
@@ -13,9 +14,9 @@ public class PresenceTemplate extends AbstractEntity {
private long lastUsed = 0;
private String statusMessage;
- private Presence.Status status = Presence.Status.ONLINE;
+ private Presence.Availability status = Presence.Availability.ONLINE;
- public PresenceTemplate(Presence.Status status, String statusMessage) {
+ public PresenceTemplate(Presence.Availability status, String statusMessage) {
this.status = status;
this.statusMessage = statusMessage;
this.lastUsed = System.currentTimeMillis();
@@ -41,11 +42,11 @@ public class PresenceTemplate extends AbstractEntity {
template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
template.status =
- Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
+ Presence.Availability.valueOfShown(cursor.getString(cursor.getColumnIndex(STATUS)));
return template;
}
- public Presence.Status getStatus() {
+ public Presence.Availability getStatus() {
return status;
}
@@ -1,15 +1,25 @@
package eu.siacs.conversations.entities;
import android.util.Pair;
-
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.disco.info.Identity;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Hashtable;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class Presences {
- private final Hashtable<String, Presence> presences = new Hashtable<>();
+ private final HashMap<String, Presence> presences = new HashMap<>();
+ private final Contact contact;
+
+ public Presences(final Contact contact) {
+ this.contact = contact;
+ }
private static String nameWithoutVersion(String name) {
String[] parts = name.split(" ");
@@ -63,18 +73,19 @@ public class Presences {
}
}
- public Presence.Status getShownStatus() {
- Presence.Status status = Presence.Status.OFFLINE;
+ public Presence.Availability getShownStatus() {
+ Presence.Availability highestAvailability = Presence.Availability.OFFLINE;
synchronized (this.presences) {
- for (Presence p : presences.values()) {
- if (p.getStatus() == Presence.Status.DND) {
- return p.getStatus();
- } else if (p.getStatus().compareTo(status) < 0) {
- status = p.getStatus();
+ for (final Presence p : presences.values()) {
+ final var availability = p.getAvailability();
+ if (availability == Presence.Availability.DND) {
+ return availability;
+ } else if (availability.compareTo(highestAvailability) < 0) {
+ highestAvailability = availability;
}
}
}
- return status;
+ return highestAvailability;
}
public int size() {
@@ -100,10 +111,12 @@ public class Presences {
public List<PresenceTemplate> asTemplates() {
synchronized (this.presences) {
ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
- for (Presence p : presences.values()) {
- if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
- templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
+ for (Presence presence : this.presences.values()) {
+ String message = Strings.nullToEmpty(presence.getStatus()).trim();
+ if (Strings.isNullOrEmpty(message)) {
+ continue;
}
+ templates.add(new PresenceTemplate(presence.getAvailability(), message));
}
return templates;
}
@@ -115,24 +128,35 @@ public class Presences {
}
}
- public List<String> getStatusMessages() {
- ArrayList<String> messages = new ArrayList<>();
+ public Set<String> getStatusMessages() {
+ Set<String> messages = new HashSet<>();
synchronized (this.presences) {
for (Presence presence : this.presences.values()) {
- String message = presence.getMessage() == null ? null : presence.getMessage().trim();
- if (message != null && !message.isEmpty() && !messages.contains(message)) {
- messages.add(message);
+ String message = Strings.nullToEmpty(presence.getStatus()).trim();
+ if (Strings.isNullOrEmpty(message)) {
+ continue;
}
+ messages.add(message);
}
}
return messages;
}
public boolean allOrNonSupport(String namespace) {
+ final var connection = this.contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return true;
+ }
synchronized (this.presences) {
- for (Presence presence : this.presences.values()) {
- ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
- if (disco == null || !disco.getFeatures().contains(namespace)) {
+ for (var resource : this.presences.keySet()) {
+ final var disco =
+ connection
+ .getManager(DiscoManager.class)
+ .get(
+ Strings.isNullOrEmpty(resource)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(resource));
+ if (disco == null || !disco.getFeatureStrings().contains(namespace)) {
return false;
}
}
@@ -140,19 +164,28 @@ public class Presences {
return true;
}
-
public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
Map<String, String> typeMap = new HashMap<>();
Map<String, String> nameMap = new HashMap<>();
+ final var connection = this.contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return new Pair<>(typeMap, nameMap);
+ }
synchronized (this.presences) {
- for (Map.Entry<String, Presence> presenceEntry : this.presences.entrySet()) {
- String resource = presenceEntry.getKey();
- Presence presence = presenceEntry.getValue();
- ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
- if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
- ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
+ for (final String resource : this.presences.keySet()) {
+ final var serviceDiscoveryResult =
+ connection
+ .getManager(DiscoManager.class)
+ .get(
+ Strings.isNullOrEmpty(resource)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(resource));
+ if (serviceDiscoveryResult != null
+ && !serviceDiscoveryResult.getIdentities().isEmpty()) {
+ final Identity identity =
+ Iterables.getFirst(serviceDiscoveryResult.getIdentities(), null);
String type = identity.getType();
- String name = identity.getName();
+ String name = identity.getIdentityName();
if (type != null) {
typeMap.put(resource, type);
}
@@ -1,349 +0,0 @@
-package eu.siacs.conversations.entities;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.util.Base64;
-import com.google.common.base.Strings;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.forms.Field;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class ServiceDiscoveryResult {
- public static final String TABLENAME = "discovery_results";
- public static final String HASH = "hash";
- public static final String VER = "ver";
- public static final String RESULT = "result";
- protected final String hash;
- protected final byte[] ver;
- protected final List<String> features;
- protected final List<Data> forms;
- private final List<Identity> identities;
-
- public ServiceDiscoveryResult(final Iq packet) {
- this.identities = new ArrayList<>();
- this.features = new ArrayList<>();
- this.forms = new ArrayList<>();
- this.hash = "sha-1"; // We only support sha-1 for now
-
- final List<Element> elements = packet.query().getChildren();
-
- for (final Element element : elements) {
- if (element.getName().equals("identity")) {
- Identity id = new Identity(element);
- if (id.getType() != null && id.getCategory() != null) {
- identities.add(id);
- }
- } else if (element.getName().equals("feature")) {
- if (element.getAttribute("var") != null) {
- features.add(element.getAttribute("var"));
- }
- } else if (element.getName().equals("x")
- && element.getAttribute("xmlns").equals(Namespace.DATA)) {
- forms.add(Data.parse(element));
- }
- }
- this.ver = this.mkCapHash();
- }
-
- private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
- this.identities = new ArrayList<>();
- this.features = new ArrayList<>();
- this.forms = new ArrayList<>();
- this.hash = hash;
- this.ver = ver;
-
- JSONArray identities = o.optJSONArray("identities");
- if (identities != null) {
- for (int i = 0; i < identities.length(); i++) {
- this.identities.add(new Identity(identities.getJSONObject(i)));
- }
- }
- JSONArray features = o.optJSONArray("features");
- if (features != null) {
- for (int i = 0; i < features.length(); i++) {
- this.features.add(features.getString(i));
- }
- }
- JSONArray forms = o.optJSONArray("forms");
- if (forms != null) {
- for (int i = 0; i < forms.length(); i++) {
- this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
- }
- }
- }
-
- private ServiceDiscoveryResult() {
- this.hash = "sha-1";
- this.features = Collections.emptyList();
- this.identities = Collections.emptyList();
- this.ver = null;
- this.forms = Collections.emptyList();
- }
-
- public static ServiceDiscoveryResult empty() {
- return new ServiceDiscoveryResult();
- }
-
- public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
- this(
- cursor.getString(cursor.getColumnIndexOrThrow(HASH)),
- Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT),
- new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT))));
- }
-
- private static String clean(String s) {
- return s.replace("<", "<");
- }
-
- private static String blankNull(String s) {
- return s == null ? "" : clean(s);
- }
-
- private static Data createFormFromJSONObject(JSONObject o) {
- Data data = new Data();
- JSONArray names = o.names();
- for (int i = 0; i < names.length(); ++i) {
- try {
- String name = names.getString(i);
- JSONArray jsonValues = o.getJSONArray(name);
- ArrayList<String> values = new ArrayList<>(jsonValues.length());
- for (int j = 0; j < jsonValues.length(); ++j) {
- values.add(jsonValues.getString(j));
- }
- data.put(name, values);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- return data;
- }
-
- private static JSONObject createJSONFromForm(Data data) {
- JSONObject object = new JSONObject();
- for (Field field : data.getFields()) {
- try {
- JSONArray jsonValues = new JSONArray();
- for (String value : field.getValues()) {
- jsonValues.put(value);
- }
- object.put(field.getFieldName(), jsonValues);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- try {
- JSONArray jsonValues = new JSONArray();
- jsonValues.put(data.getFormType());
- object.put(Data.FORM_TYPE, jsonValues);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return object;
- }
-
- public String getVer() {
- return Base64.encodeToString(this.ver, Base64.NO_WRAP);
- }
-
- public List<Identity> getIdentities() {
- return this.identities;
- }
-
- public List<String> getFeatures() {
- return this.features;
- }
-
- public boolean hasIdentity(String category, String type) {
- for (Identity id : this.getIdentities()) {
- if ((category == null || id.getCategory().equals(category))
- && (type == null || id.getType().equals(type))) {
- return true;
- }
- }
-
- return false;
- }
-
- public String getExtendedDiscoInformation(final String formType, final String name) {
- for (final Data form : this.forms) {
- if (formType.equals(form.getFormType())) {
- for (final Field field : form.getFields()) {
- if (name.equals(field.getFieldName())) {
- return field.getValue();
- }
- }
- }
- }
- return null;
- }
-
- private byte[] mkCapHash() {
- StringBuilder s = new StringBuilder();
-
- List<Identity> identities = this.getIdentities();
- Collections.sort(identities);
-
- for (Identity id : identities) {
- s.append(blankNull(id.getCategory()))
- .append("/")
- .append(blankNull(id.getType()))
- .append("/")
- .append(blankNull(id.getLang()))
- .append("/")
- .append(blankNull(id.getName()))
- .append("<");
- }
-
- final List<String> features = this.getFeatures();
- Collections.sort(features);
- for (final String feature : features) {
- s.append(clean(feature)).append("<");
- }
-
- Collections.sort(forms, Comparator.comparing(Data::getFormType));
- for (final Data form : forms) {
- s.append(clean(form.getFormType())).append("<");
- final List<Field> fields = form.getFields();
- Collections.sort(
- fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName())));
- for (final Field field : fields) {
- s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
- final List<String> values = field.getValues();
- Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull));
- for (final String value : values) {
- s.append(blankNull(value)).append("<");
- }
- }
- }
-
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- return null;
- }
-
- return md.digest(s.toString().getBytes(StandardCharsets.UTF_8));
- }
-
- private JSONObject toJSON() {
- try {
- JSONObject o = new JSONObject();
-
- JSONArray ids = new JSONArray();
- for (Identity id : this.getIdentities()) {
- ids.put(id.toJSON());
- }
- o.put("identities", ids);
-
- o.put("features", new JSONArray(this.getFeatures()));
-
- JSONArray forms = new JSONArray();
- for (Data data : this.forms) {
- forms.put(createJSONFromForm(data));
- }
- o.put("forms", forms);
-
- return o;
- } catch (JSONException e) {
- return null;
- }
- }
-
- public ContentValues getContentValues() {
- final ContentValues values = new ContentValues();
- values.put(HASH, this.hash);
- values.put(VER, getVer());
- JSONObject jsonObject = toJSON();
- values.put(RESULT, jsonObject == null ? "" : jsonObject.toString());
- return values;
- }
-
- public static class Identity implements Comparable<Identity> {
- protected final String type;
- protected final String lang;
- protected final String name;
- final String category;
-
- Identity(final String category, final String type, final String lang, final String name) {
- this.category = category;
- this.type = type;
- this.lang = lang;
- this.name = name;
- }
-
- Identity(final Element el) {
- this(
- el.getAttribute("category"),
- el.getAttribute("type"),
- el.getAttribute("xml:lang"),
- el.getAttribute("name"));
- }
-
- Identity(final JSONObject o) {
-
- this(
- o.optString("category", null),
- o.optString("type", null),
- o.optString("lang", null),
- o.optString("name", null));
- }
-
- public String getCategory() {
- return this.category;
- }
-
- public String getType() {
- return this.type;
- }
-
- public String getLang() {
- return this.lang;
- }
-
- public String getName() {
- return this.name;
- }
-
- JSONObject toJSON() {
- try {
- JSONObject o = new JSONObject();
- o.put("category", this.getCategory());
- o.put("type", this.getType());
- o.put("lang", this.getLang());
- o.put("name", this.getName());
- return o;
- } catch (JSONException e) {
- return null;
- }
- }
-
- @Override
- public int compareTo(final Identity o) {
- int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
- if (r == 0) {
- r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
- }
- if (r == 0) {
- r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
- }
- if (r == 0) {
- r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
- }
-
- return r;
- }
- }
-}
@@ -1,14 +1,13 @@
package eu.siacs.conversations.generator;
import android.text.TextUtils;
-
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.stanza.Presence;
public class PresenceGenerator extends AbstractGenerator {
@@ -16,20 +15,25 @@ public class PresenceGenerator extends AbstractGenerator {
super(service);
}
- private im.conversations.android.xmpp.model.stanza.Presence subscription(String type, Contact contact) {
- im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
+ private im.conversations.android.xmpp.model.stanza.Presence subscription(
+ String type, Contact contact) {
+ im.conversations.android.xmpp.model.stanza.Presence packet =
+ new im.conversations.android.xmpp.model.stanza.Presence();
packet.setAttribute("type", type);
packet.setTo(contact.getJid());
packet.setFrom(contact.getAccount().getJid().asBareJid());
return packet;
}
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) {
+ public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
+ final Contact contact) {
return requestPresenceUpdatesFrom(contact, null);
}
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact, final String preAuth) {
- im.conversations.android.xmpp.model.stanza.Presence packet = subscription("subscribe", contact);
+ public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
+ final Contact contact, final String preAuth) {
+ im.conversations.android.xmpp.model.stanza.Presence packet =
+ subscription("subscribe", contact);
String displayName = contact.getAccount().getDisplayName();
if (!TextUtils.isEmpty(displayName)) {
packet.addChild("nick", Namespace.NICK).setContent(displayName);
@@ -40,41 +44,42 @@ public class PresenceGenerator extends AbstractGenerator {
return packet;
}
- public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(Contact contact) {
+ public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(
+ Contact contact) {
return subscription("unsubscribe", contact);
}
- public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(Contact contact) {
+ public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(
+ Contact contact) {
return subscription("unsubscribed", contact);
}
- public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(Contact contact) {
+ public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(
+ Contact contact) {
return subscription("subscribed", contact);
}
- public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Status status) {
+ public im.conversations.android.xmpp.model.stanza.Presence selfPresence(
+ Account account, Presence.Availability status) {
return selfPresence(account, status, true);
}
- public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Status status, final boolean personal) {
- final im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
+ 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();
- if (status.toShowString() != null) {
- packet.addChild("show").setContent(status.toShowString());
- }
- if (!TextUtils.isEmpty(message)) {
- packet.addChild(new Element("status").setContent(message));
- }
+ 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");
+ 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);
@@ -83,15 +88,18 @@ public class PresenceGenerator extends AbstractGenerator {
}
public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) {
- im.conversations.android.xmpp.model.stanza.Presence presence = new im.conversations.android.xmpp.model.stanza.Presence();
+ im.conversations.android.xmpp.model.stanza.Presence presence =
+ new im.conversations.android.xmpp.model.stanza.Presence();
presence.setTo(mucOptions.getSelf().getFullJid());
presence.setFrom(mucOptions.getAccount().getJid());
presence.setAttribute("type", "unavailable");
return presence;
}
- public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(Account account) {
- im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
+ public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(
+ Account account) {
+ im.conversations.android.xmpp.model.stanza.Presence packet =
+ new im.conversations.android.xmpp.model.stanza.Presence();
packet.setFrom(account.getJid());
packet.setAttribute("type", "unavailable");
return packet;
@@ -1,7 +1,12 @@
package eu.siacs.conversations.parser;
import android.util.Log;
+import androidx.annotation.NonNull;
import com.google.common.base.Strings;
+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.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -10,7 +15,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -18,10 +22,13 @@ 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.manager.DiscoManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
+import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.occupant.OccupantId;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.openintents.openpgp.util.OpenPgpUtils;
@@ -359,13 +366,17 @@ public class PresenceParser extends AbstractParser
final int sizeBefore = contact.getPresences().size();
- final String show = packet.findChildContent("show");
- final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
- final String message = packet.findChildContent("status");
- final Presence presence = Presence.parse(show, caps, message);
- contact.updatePresence(resource, presence);
- if (presence.hasCaps()) {
- mXmppConnectionService.fetchCaps(account, from, presence);
+ contact.updatePresence(resource, packet);
+
+ final var nodeHash = packet.getCapabilities();
+ final var connection = account.getXmppConnection();
+ if (nodeHash != null && connection != null) {
+ final var discoFuture =
+ connection
+ .getManager(DiscoManager.class)
+ .infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash);
+
+ logDiscoFailure(from, discoFuture);
}
final Element idle = packet.findChild("idle", Namespace.IDLE);
@@ -412,7 +423,8 @@ public class PresenceParser extends AbstractParser
} else {
contact.removePresence(from.getResource());
}
- if (contact.getShownStatus() == Presence.Status.OFFLINE) {
+ if (contact.getShownStatus()
+ == im.conversations.android.xmpp.model.stanza.Presence.Availability.OFFLINE) {
contact.flagInactive();
}
mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false);
@@ -453,6 +465,24 @@ public class PresenceParser extends AbstractParser
mXmppConnectionService.updateRosterUi();
}
+ private static void logDiscoFailure(final Jid from, ListenableFuture<Void> discoFuture) {
+ Futures.addCallback(
+ discoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {}
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (throwable instanceof TimeoutException) {
+ return;
+ }
+ Log.d(Config.LOGTAG, "could not retrieve disco from " + from, throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
@Override
public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) {
if (packet.hasChild("x", Namespace.MUC_USER)) {
@@ -21,7 +21,6 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.ShortcutService;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -31,9 +30,14 @@ import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.Resolver;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.mam.MamReference;
+import im.conversations.android.xml.XmlElementReader;
+import im.conversations.android.xmpp.EntityCapabilities;
+import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
@@ -46,7 +50,6 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
-import org.json.JSONException;
import org.json.JSONObject;
import org.jxmpp.jid.parts.Localpart;
import org.jxmpp.stringprep.XmppStringprepException;
@@ -61,11 +64,11 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
public class DatabaseBackend extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history";
- private static final int DATABASE_VERSION = 53;
+ private static final int DATABASE_VERSION = 54;
private static boolean requiresMessageIndexRebuild = false;
private static DatabaseBackend instance = null;
- private static final String CREATE_CONTATCS_STATEMENT =
+ private static final String CREATE_CONTACTS_STATEMENT =
"create table "
+ Contact.TABLENAME
+ "("
@@ -108,22 +111,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Contact.JID
+ ") ON CONFLICT REPLACE);";
- private static final String CREATE_DISCOVERY_RESULTS_STATEMENT =
- "create table "
- + ServiceDiscoveryResult.TABLENAME
- + "("
- + ServiceDiscoveryResult.HASH
- + " TEXT, "
- + ServiceDiscoveryResult.VER
- + " TEXT, "
- + ServiceDiscoveryResult.RESULT
- + " TEXT, "
- + "UNIQUE("
- + ServiceDiscoveryResult.HASH
- + ", "
- + ServiceDiscoveryResult.VER
- + ") ON CONFLICT REPLACE);";
-
private static final String CREATE_PRESENCE_TEMPLATES_STATEMENT =
"CREATE TABLE "
+ PresenceTemplate.TABELNAME
@@ -252,6 +239,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON CONFLICT IGNORE"
+ ");";
+ private static final String CREATE_CAPS_CACHE_TABLE =
+ "CREATE TABLE caps_cache (caps TEXT, caps2 TEXT, disco_info TEXT, UNIQUE (caps), UNIQUE"
+ + " (caps2));";
+ private static final String CREATE_CAPS_CACHE_INDEX_CAPS =
+ "CREATE INDEX idx_caps ON caps_cache(caps);";
+ private static final String CREATE_CAPS_CACHE_INDEX_CAPS2 =
+ "CREATE INDEX idx_caps2 ON caps_cache(caps2);";
+
private static final String RESOLVER_RESULTS_TABLENAME = "resolver_results";
private static final String CREATE_RESOLVER_RESULTS_TABLE =
@@ -495,8 +490,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_MESSAGE_DELETED_INDEX);
db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX);
db.execSQL(CREATE_MESSAGE_TYPE_INDEX);
- db.execSQL(CREATE_CONTATCS_STATEMENT);
- db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
+ db.execSQL(CREATE_CONTACTS_STATEMENT);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
@@ -507,6 +501,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER);
db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER);
+ db.execSQL(CREATE_CAPS_CACHE_TABLE);
+ db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS);
+ db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS2);
}
@Override
@@ -527,7 +524,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
if (oldVersion < 5 && newVersion >= 5) {
db.execSQL("DROP TABLE " + Contact.TABLENAME);
- db.execSQL(CREATE_CONTATCS_STATEMENT);
+ db.execSQL(CREATE_CONTACTS_STATEMENT);
db.execSQL("UPDATE " + Account.TABLENAME + " SET " + Account.ROSTERVERSION + " = NULL");
}
if (oldVersion < 6 && newVersion >= 6) {
@@ -727,10 +724,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ SQLiteAxolotlStore.CERTIFICATE);
}
- if (oldVersion < 23 && newVersion >= 23) {
- db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
- }
-
if (oldVersion < 24 && newVersion >= 24) {
db.execSQL(
"ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
@@ -745,10 +738,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
}
- if (oldVersion < 27 && newVersion >= 27) {
- db.execSQL("DELETE FROM " + ServiceDiscoveryResult.TABLENAME);
- }
-
if (oldVersion < 28 && newVersion >= 28) {
canonicalizeJids(db);
}
@@ -1086,6 +1075,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
}
+ if (oldVersion < 54 && newVersion >= 54) {
+ db.execSQL("DROP TABLE discovery_results");
+ db.execSQL(CREATE_CAPS_CACHE_TABLE);
+ db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS);
+ db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS2);
+ }
}
private void canonicalizeJids(SQLiteDatabase db) {
@@ -1224,40 +1219,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.insert(Account.TABLENAME, null, account.getContentValues());
}
- public void insertDiscoveryResult(ServiceDiscoveryResult result) {
- SQLiteDatabase db = this.getWritableDatabase();
- db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues());
- }
-
- public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) {
- SQLiteDatabase db = this.getReadableDatabase();
- String[] selectionArgs = {hash, ver};
- Cursor cursor =
- db.query(
- ServiceDiscoveryResult.TABLENAME,
- null,
- ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?",
- selectionArgs,
- null,
- null,
- null);
- if (cursor.getCount() == 0) {
- cursor.close();
- return null;
- }
- cursor.moveToFirst();
-
- ServiceDiscoveryResult result = null;
- try {
- result = new ServiceDiscoveryResult(cursor);
- } catch (JSONException e) {
- /* result is still null */
- }
-
- cursor.close();
- return result;
- }
-
public void saveResolverResult(String domain, Resolver.Result result) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues contentValues = result.toContentValues();
@@ -1612,6 +1573,60 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return message;
}
+ public void insertCapsCache(
+ EntityCapabilities.EntityCapsHash caps,
+ EntityCapabilities2.EntityCaps2Hash caps2,
+ InfoQuery infoQuery) {
+ final var contentValues = new ContentValues();
+ contentValues.put("caps", caps.encoded());
+ contentValues.put("caps2", caps2.encoded());
+ contentValues.put("disco_info", infoQuery.toString());
+ getWritableDatabase()
+ .insertWithOnConflict(
+ "caps_cache", null, contentValues, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public InfoQuery getInfoQuery(final EntityCapabilities.Hash hash) {
+ final String selection;
+ final String[] args;
+ if (hash instanceof EntityCapabilities.EntityCapsHash) {
+ selection = "caps=?";
+ args = new String[] {hash.encoded()};
+ } else if (hash instanceof EntityCapabilities2.EntityCaps2Hash) {
+ selection = "caps2=?";
+ args = new String[] {hash.encoded()};
+ } else {
+ return null;
+ }
+ try (final Cursor cursor =
+ getReadableDatabase()
+ .query(
+ "caps_cache",
+ new String[] {"disco_info"},
+ selection,
+ args,
+ null,
+ null,
+ null)) {
+ if (cursor.moveToFirst()) {
+ final var cached = cursor.getString(0);
+ try {
+ final var element =
+ XmlElementReader.read(cached.getBytes(StandardCharsets.UTF_8));
+ if (element instanceof InfoQuery infoQuery) {
+ return infoQuery;
+ }
+ } catch (final IOException e) {
+ Log.e(Config.LOGTAG, "could not restore info query from cache", e);
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+
public static class FilePath {
public final UUID uuid;
public final String path;
@@ -19,6 +19,7 @@ import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Message;
import java.math.BigInteger;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -55,7 +56,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
- private static Version get(List<String> features) {
+ private static Version get(final Collection<String> features) {
final Version[] values = values();
for (int i = values.length - 1; i >= 0; --i) {
for (String feature : features) {
@@ -67,7 +68,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
return MAM_0;
}
- public static boolean has(List<String> features) {
+ public static boolean has(final Collection<String> features) {
for (String feature : features) {
for (Version version : values()) {
if (version.namespace.equals(feature)) {
@@ -81,11 +81,8 @@ import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Reaction;
-import eu.siacs.conversations.entities.Roster;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.generator.AbstractGenerator;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator;
@@ -125,6 +122,7 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.LocalizedContent;
import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.IqErrorResponseException;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
@@ -140,10 +138,13 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import eu.siacs.conversations.xmpp.mam.MamReference;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.pep.PublishOptions;
+import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.avatar.Metadata;
import im.conversations.android.xmpp.model.bookmark.Storage;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.mds.Displayed;
import im.conversations.android.xmpp.model.pubsub.PubSub;
import im.conversations.android.xmpp.model.stanza.Iq;
@@ -358,8 +359,6 @@ public class XmppConnectionService extends Service {
public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
private final AtomicLong mLastExpiryRun = new AtomicLong(0);
- private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache =
- new LruCache<>(20);
private final OnStatusChanged statusListener =
new OnStatusChanged() {
@@ -1268,13 +1267,13 @@ public class XmppConnectionService extends Service {
getResources().getString(R.string.picture_compression));
}
- private Presence.Status getTargetPresence() {
+ private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() {
if (dndOnSilentMode() && isPhoneSilenced()) {
- return Presence.Status.DND;
+ return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND;
} else if (awayWhenScreenLocked() && isScreenLocked()) {
- return Presence.Status.AWAY;
+ return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY;
} else {
- return Presence.Status.ONLINE;
+ return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE;
}
}
@@ -3721,7 +3720,8 @@ public class XmppConnectionService extends Service {
final var packet =
mPresenceGenerator.selfPresence(
account,
- Presence.Status.ONLINE,
+ im.conversations.android.xmpp.model.stanza.Presence
+ .Availability.ONLINE,
mucOptions.nonanonymous()
|| onConferenceJoined != null);
packet.setTo(joinJid);
@@ -4117,7 +4117,9 @@ public class XmppConnectionService extends Service {
final var packet =
mPresenceGenerator.selfPresence(
- account, Presence.Status.ONLINE, options.nonanonymous());
+ account,
+ im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
+ options.nonanonymous());
packet.setTo(joinJid);
sendPresencePacket(account, packet);
if (nick.equals(MucOptions.defaultNick(account))
@@ -4174,7 +4176,9 @@ public class XmppConnectionService extends Service {
account.getJid().asBareJid(), joinJid, current));
final var packet =
mPresenceGenerator.selfPresence(
- account, Presence.Status.ONLINE, options.nonanonymous());
+ account,
+ im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
+ options.nonanonymous());
packet.setTo(joinJid);
sendPresencePacket(account, packet);
}
@@ -4367,11 +4371,19 @@ public class XmppConnectionService extends Service {
final Conversation conversation, final OnConferenceConfigurationFetched callback) {
final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
final var account = conversation.getAccount();
- sendIqPacket(
- account,
- request,
- response -> {
- if (response.getType() == Iq.Type.RESULT) {
+ final var connection = account.getXmppConnection();
+ if (connection == null) {
+ return;
+ }
+ final var future =
+ connection
+ .getManager(DiscoManager.class)
+ .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
+ Futures.addCallback(
+ future,
+ new FutureCallback<InfoQuery>() {
+ @Override
+ public void onSuccess(InfoQuery result) {
final MucOptions mucOptions = conversation.getMucOptions();
final Bookmark bookmark = conversation.getBookmark();
final boolean sameBefore =
@@ -4380,7 +4392,7 @@ public class XmppConnectionService extends Service {
mucOptions.getName());
final var hadOccupantId = mucOptions.occupantId();
- if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
+ if (mucOptions.updateConfiguration(result)) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@@ -4402,7 +4414,8 @@ public class XmppConnectionService extends Service {
final var packet =
mPresenceGenerator.selfPresence(
account,
- Presence.Status.ONLINE,
+ im.conversations.android.xmpp.model.stanza.Presence
+ .Availability.ONLINE,
mucOptions.nonanonymous());
packet.setTo(me);
sendPresencePacket(account, packet);
@@ -4421,18 +4434,27 @@ public class XmppConnectionService extends Service {
}
updateConversationUi();
- } else if (response.getType() == Iq.Type.TIMEOUT) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": received timeout waiting for conference configuration"
- + " fetch");
- } else {
- if (callback != null) {
- callback.onFetchFailed(conversation, response.getErrorCondition());
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (throwable instanceof TimeoutException) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": received timeout waiting for conference"
+ + " configuration fetch");
+ } else if (throwable
+ instanceof IqErrorResponseException errorResponseException) {
+ if (callback != null) {
+ callback.onFetchFailed(
+ conversation,
+ errorResponseException.getResponse().getErrorCondition());
+ }
}
}
- });
+ },
+ MoreExecutors.directExecutor());
}
public void pushNodeConfiguration(
@@ -5270,7 +5292,10 @@ public class XmppConnectionService extends Service {
if (mucOptions.online()) {
final var packet =
mPresenceGenerator.selfPresence(
- account, Presence.Status.ONLINE, mucOptions.nonanonymous());
+ account,
+ im.conversations.android.xmpp.model.stanza.Presence
+ .Availability.ONLINE,
+ mucOptions.nonanonymous());
packet.setTo(mucOptions.getSelf().getFullJid());
connection.sendPresencePacket(packet);
}
@@ -5982,7 +6007,7 @@ public class XmppConnectionService extends Service {
}
private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
- final Presence.Status status;
+ final im.conversations.android.xmpp.model.stanza.Presence.Availability status;
if (manuallyChangePresence()) {
status = account.getPresenceStatus();
} else {
@@ -6217,100 +6242,6 @@ public class XmppConnectionService extends Service {
});
}
- public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
- ServiceDiscoveryResult result = discoCache.get(key);
- if (result != null) {
- return result;
- } else {
- result = databaseBackend.findDiscoveryResult(key.first, key.second);
- if (result != null) {
- discoCache.put(key, result);
- }
- return result;
- }
- }
-
- public void fetchCaps(final Account account, final Jid jid, final Presence presence) {
- final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
- final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
- if (disco != null) {
- presence.setServiceDiscoveryResult(disco);
- final Contact contact = account.getRoster().getContact(jid);
- if (contact.refreshRtpCapability()) {
- syncRoster(account);
- }
- } else {
- final Iq request = new Iq(Iq.Type.GET);
- request.setTo(jid);
- final String node = presence.getNode();
- final String ver = presence.getVer();
- final Element query = request.query(Namespace.DISCO_INFO);
- if (node != null && ver != null) {
- query.setAttribute("node", node + "#" + ver);
- }
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": making disco request for "
- + key.second
- + " to "
- + jid);
- sendIqPacket(
- account,
- request,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final ServiceDiscoveryResult discoveryResult =
- new ServiceDiscoveryResult(response);
- if (presence.getVer().equals(discoveryResult.getVer())) {
- databaseBackend.insertDiscoveryResult(discoveryResult);
- injectServiceDiscoveryResult(
- account.getRoster(),
- presence.getHash(),
- presence.getVer(),
- discoveryResult);
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": mismatch in caps for contact "
- + jid
- + " "
- + presence.getVer()
- + " vs "
- + discoveryResult.getVer());
- }
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": unable to fetch caps from "
- + jid);
- }
- });
- }
- }
-
- private void injectServiceDiscoveryResult(
- Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
- boolean rosterNeedsSync = false;
- for (final Contact contact : roster.getContacts()) {
- boolean serviceDiscoverySet = false;
- for (final Presence presence : contact.getPresences().getPresences()) {
- if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
- presence.setServiceDiscoveryResult(disco);
- serviceDiscoverySet = true;
- }
- }
- if (serviceDiscoverySet) {
- rosterNeedsSync |= contact.refreshRtpCapability();
- }
- }
- if (rosterNeedsSync) {
- syncRoster(roster.getAccount());
- }
- }
-
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);
@@ -32,7 +32,9 @@ import androidx.core.view.ViewCompat;
import androidx.databinding.DataBindingUtil;
import com.google.android.material.color.MaterialColors;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.common.primitives.Ints;
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
@@ -44,7 +46,6 @@ import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.AbstractQuickConversationsService;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@@ -69,6 +70,7 @@ import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -424,11 +426,11 @@ public class ContactDetailsActivity extends OmemoActivity
binding.detailsSendPresence.setOnCheckedChangeListener(null);
binding.detailsReceivePresence.setOnCheckedChangeListener(null);
- List<String> statusMessages = contact.getPresences().getStatusMessages();
- if (statusMessages.size() == 0) {
+ Collection<String> statusMessages = contact.getPresences().getStatusMessages();
+ if (statusMessages.isEmpty()) {
binding.statusMessage.setVisibility(View.GONE);
} else if (statusMessages.size() == 1) {
- final String message = statusMessages.get(0);
+ final String message = Iterables.getOnlyElement(statusMessages);
binding.statusMessage.setVisibility(View.VISIBLE);
final Spannable span = new SpannableString(message);
if (Emoticons.isOnlyEmoji(message)) {
@@ -440,16 +442,7 @@ public class ContactDetailsActivity extends OmemoActivity
}
binding.statusMessage.setText(span);
} else {
- StringBuilder builder = new StringBuilder();
- binding.statusMessage.setVisibility(View.VISIBLE);
- int s = statusMessages.size();
- for (int i = 0; i < s; ++i) {
- builder.append(statusMessages.get(i));
- if (i < s - 1) {
- builder.append("\n");
- }
- }
- binding.statusMessage.setText(builder);
+ binding.statusMessage.setText(Joiner.on('\n').join(statusMessages));
}
if (contact.getOption(Contact.Options.FROM)) {
@@ -592,7 +585,7 @@ public class ContactDetailsActivity extends OmemoActivity
final List<ListItem.Tag> tagList = contact.getTags(this);
final boolean hasMetaTags =
- contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
+ contact.isBlocked() || contact.getShownStatus() != Presence.Availability.OFFLINE;
if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
binding.tags.setVisibility(View.GONE);
} else {
@@ -628,8 +621,8 @@ public class ContactDetailsActivity extends OmemoActivity
viewIdBuilder.add(id);
binding.tags.addView(tv);
} else {
- final Presence.Status status = contact.getShownStatus();
- if (status != Presence.Status.OFFLINE) {
+ final Presence.Availability status = contact.getShownStatus();
+ if (status != Presence.Availability.OFFLINE) {
final TextView tv =
(TextView) inflater.inflate(R.layout.item_tag, binding.tags, false);
UIHelper.setStatus(tv, status);
@@ -81,7 +81,6 @@ import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.User;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
@@ -128,6 +127,7 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -2938,7 +2938,7 @@ public class ConversationFragment extends XmppFragment
boolean hasAttachments =
mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments();
final Conversation c = this.conversation;
- final Presence.Status status;
+ final Presence.Availability status;
final String text =
this.binding.textinput == null ? "" : this.binding.textinput.getText().toString();
final SendButtonAction action;
@@ -2951,17 +2951,17 @@ public class ConversationFragment extends XmppFragment
if (activity != null
&& activity.xmppConnectionService != null
&& activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) {
- status = Presence.Status.OFFLINE;
+ status = Presence.Availability.OFFLINE;
} else if (c.getMode() == Conversation.MODE_SINGLE) {
status = c.getContact().getShownStatus();
} else {
status =
c.getMucOptions().online()
- ? Presence.Status.ONLINE
- : Presence.Status.OFFLINE;
+ ? Presence.Availability.ONLINE
+ : Presence.Availability.OFFLINE;
}
} else {
- status = Presence.Status.OFFLINE;
+ status = Presence.Availability.OFFLINE;
}
this.binding.textSendButton.setTag(action);
this.binding.textSendButton.setIconResource(
@@ -51,7 +51,6 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.databinding.ActivityEditAccountBinding;
import eu.siacs.conversations.databinding.DialogPresenceBinding;
import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.QuickConversationsService;
@@ -80,6 +79,7 @@ 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.pep.Avatar;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@@ -411,7 +411,7 @@ public class EditAccountActivity extends OmemoActivity
};
private static void setAvailabilityRadioButton(
- Presence.Status status, DialogPresenceBinding binding) {
+ Presence.Availability status, DialogPresenceBinding binding) {
if (status == null) {
binding.online.setChecked(true);
return;
@@ -431,15 +431,15 @@ public class EditAccountActivity extends OmemoActivity
}
}
- private static Presence.Status getAvailabilityRadioButton(DialogPresenceBinding binding) {
+ private static Presence.Availability getAvailabilityRadioButton(DialogPresenceBinding binding) {
if (binding.dnd.isChecked()) {
- return Presence.Status.DND;
+ return Presence.Availability.DND;
} else if (binding.xa.isChecked()) {
- return Presence.Status.XA;
+ return Presence.Availability.XA;
} else if (binding.away.isChecked()) {
- return Presence.Status.AWAY;
+ return Presence.Availability.AWAY;
} else {
- return Presence.Status.ONLINE;
+ return Presence.Availability.ONLINE;
}
}
@@ -68,7 +68,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.QuickConversationsService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
@@ -84,6 +83,7 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -1152,12 +1152,12 @@ public class StartConversationActivity extends XmppActivity
for (final Account account : accounts) {
if (account.isEnabled()) {
for (Contact contact : account.getRoster().getContacts()) {
- Presence.Status s = contact.getShownStatus();
+ Presence.Availability s = contact.getShownStatus();
if (contact.showInContactList()
&& contact.match(this, needle)
&& (!this.mHideOfflineContacts
|| (needle != null && !needle.trim().isEmpty())
- || s.compareTo(Presence.Status.OFFLINE) < 0)) {
+ || s.compareTo(Presence.Availability.OFFLINE) < 0)) {
this.contacts.add(contact);
}
}
@@ -10,169 +10,170 @@ import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
-
import androidx.annotation.NonNull;
import androidx.constraintlayout.helper.widget.Flow;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.databinding.DataBindingUtil;
-
import com.google.android.material.color.MaterialColors;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
-
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ItemContactBinding;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XEP0392Helper;
import eu.siacs.conversations.xmpp.Jid;
-
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.List;
public class ListItemAdapter extends ArrayAdapter<ListItem> {
- protected XmppActivity activity;
- private boolean showDynamicTags = false;
- private OnTagClickedListener mOnTagClickedListener = null;
- private final View.OnClickListener onTagTvClick = view -> {
- if (view instanceof TextView tv && mOnTagClickedListener != null) {
- final String tag = tv.getText().toString();
- mOnTagClickedListener.onTagClicked(tag);
- }
- };
-
- public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
- super(activity, 0, objects);
- this.activity = activity;
- }
-
-
- public void refreshSettings() {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
- this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
- }
-
- @NonNull
- @Override
- public View getView(int position, View view, @NonNull ViewGroup parent) {
- LayoutInflater inflater = activity.getLayoutInflater();
- final ListItem item = getItem(position);
- ViewHolder viewHolder;
- if (view == null) {
- final ItemContactBinding binding = DataBindingUtil.inflate(inflater,R.layout.item_contact,parent,false);
- viewHolder = ViewHolder.get(binding);
- view = binding.getRoot();
- } else {
- viewHolder = (ViewHolder) view.getTag();
- }
- if (view.isActivated()) {
- Log.d(Config.LOGTAG,"item "+item.getDisplayName()+" is activated");
- }
- //view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background));
- final List<ListItem.Tag> tags = item.getTags(activity);
- final boolean hasMetaTags;
- if (item instanceof Contact contact) {
- hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
- } else {
- hasMetaTags = false;
- }
- if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
- viewHolder.tags.setVisibility(View.GONE);
- } else {
- viewHolder.tags.setVisibility(View.VISIBLE);
- viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1);
- final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
- for (final ListItem.Tag tag : tags) {
- final String name = tag.getName();
- final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
- tv.setText(name);
- tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(name))));
- tv.setOnClickListener(this.onTagTvClick);
- final int id = ViewCompat.generateViewId();
- tv.setId(id);
- viewIdBuilder.add(id);
- viewHolder.tags.addView(tv);
- }
- if (item instanceof Contact contact) {
- if (contact.isBlocked()) {
- final TextView tv =
- (TextView)
- inflater.inflate(
- R.layout.item_tag, viewHolder.tags, false);
- tv.setText(R.string.blocked);
- tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(),ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
- final int id = ViewCompat.generateViewId();
- tv.setId(id);
- viewIdBuilder.add(id);
- viewHolder.tags.addView(tv);
- } else {
- final Presence.Status status = contact.getShownStatus();
- if (status != Presence.Status.OFFLINE) {
+ protected XmppActivity activity;
+ private boolean showDynamicTags = false;
+ private OnTagClickedListener mOnTagClickedListener = null;
+ private final View.OnClickListener onTagTvClick =
+ view -> {
+ if (view instanceof TextView tv && mOnTagClickedListener != null) {
+ final String tag = tv.getText().toString();
+ mOnTagClickedListener.onTagClicked(tag);
+ }
+ };
+
+ public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ }
+
+ public void refreshSettings() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+ this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, View view, @NonNull ViewGroup parent) {
+ LayoutInflater inflater = activity.getLayoutInflater();
+ final ListItem item = getItem(position);
+ ViewHolder viewHolder;
+ if (view == null) {
+ final ItemContactBinding binding =
+ DataBindingUtil.inflate(inflater, R.layout.item_contact, parent, false);
+ viewHolder = ViewHolder.get(binding);
+ view = binding.getRoot();
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+ if (view.isActivated()) {
+ Log.d(Config.LOGTAG, "item " + item.getDisplayName() + " is activated");
+ }
+ // view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background));
+ final List<ListItem.Tag> tags = item.getTags(activity);
+ final boolean hasMetaTags;
+ if (item instanceof Contact contact) {
+ hasMetaTags =
+ contact.isBlocked()
+ || contact.getShownStatus() != Presence.Availability.OFFLINE;
+ } else {
+ hasMetaTags = false;
+ }
+ if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
+ viewHolder.tags.setVisibility(View.GONE);
+ } else {
+ viewHolder.tags.setVisibility(View.VISIBLE);
+ viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1);
+ final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
+ for (final ListItem.Tag tag : tags) {
+ final String name = tag.getName();
+ final TextView tv =
+ (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
+ tv.setText(name);
+ tv.setBackgroundTintList(
+ ColorStateList.valueOf(
+ MaterialColors.harmonizeWithPrimary(
+ getContext(), XEP0392Helper.rgbFromNick(name))));
+ tv.setOnClickListener(this.onTagTvClick);
+ final int id = ViewCompat.generateViewId();
+ tv.setId(id);
+ viewIdBuilder.add(id);
+ viewHolder.tags.addView(tv);
+ }
+ if (item instanceof Contact contact) {
+ if (contact.isBlocked()) {
+ final TextView tv =
+ (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
+ tv.setText(R.string.blocked);
+ tv.setBackgroundTintList(
+ ColorStateList.valueOf(
+ MaterialColors.harmonizeWithPrimary(
+ tv.getContext(),
+ ContextCompat.getColor(
+ tv.getContext(), R.color.gray_800))));
+ final int id = ViewCompat.generateViewId();
+ tv.setId(id);
+ viewIdBuilder.add(id);
+ viewHolder.tags.addView(tv);
+ } else {
+ final Presence.Availability status = contact.getShownStatus();
+ if (status != Presence.Availability.OFFLINE) {
final TextView tv =
(TextView)
- inflater.inflate(
- R.layout.item_tag, viewHolder.tags, false);
- UIHelper.setStatus(tv, status);
- final int id = ViewCompat.generateViewId();
- tv.setId(id);
- viewIdBuilder.add(id);
- viewHolder.tags.addView(tv);
+ inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
+ UIHelper.setStatus(tv, status);
+ final int id = ViewCompat.generateViewId();
+ tv.setId(id);
+ viewIdBuilder.add(id);
+ viewHolder.tags.addView(tv);
}
}
- }
- viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
- }
- final Jid jid = item.getJid();
- if (jid != null) {
- viewHolder.jid.setVisibility(View.VISIBLE);
- viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid));
- } else {
- viewHolder.jid.setVisibility(View.GONE);
- }
- viewHolder.name.setText(item.getDisplayName());
- AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar);
- return view;
- }
-
- public void setOnTagClickedListener(OnTagClickedListener listener) {
- this.mOnTagClickedListener = listener;
- }
-
-
- public interface OnTagClickedListener {
- void onTagClicked(String tag);
- }
-
- private static class ViewHolder {
- private TextView name;
- private TextView jid;
- private ImageView avatar;
- private ConstraintLayout tags;
- private Flow flowWidget;
-
- private ViewHolder() {
-
- }
-
- public static ViewHolder get(final ItemContactBinding binding) {
- ViewHolder viewHolder = new ViewHolder();
- viewHolder.name = binding.contactDisplayName;
- viewHolder.jid = binding.contactJid;
- viewHolder.avatar = binding.contactPhoto;
- viewHolder.tags = binding.tags;
- viewHolder.flowWidget = binding.flowWidget;
- binding.getRoot().setTag(viewHolder);
- return viewHolder;
- }
- }
-
+ }
+ viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
+ }
+ final Jid jid = item.getJid();
+ if (jid != null) {
+ viewHolder.jid.setVisibility(View.VISIBLE);
+ viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid));
+ } else {
+ viewHolder.jid.setVisibility(View.GONE);
+ }
+ viewHolder.name.setText(item.getDisplayName());
+ AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar);
+ return view;
+ }
+
+ public void setOnTagClickedListener(OnTagClickedListener listener) {
+ this.mOnTagClickedListener = listener;
+ }
+
+ public interface OnTagClickedListener {
+ void onTagClicked(String tag);
+ }
+
+ private static class ViewHolder {
+ private TextView name;
+ private TextView jid;
+ private ImageView avatar;
+ private ConstraintLayout tags;
+ private Flow flowWidget;
+
+ private ViewHolder() {}
+
+ public static ViewHolder get(final ItemContactBinding binding) {
+ ViewHolder viewHolder = new ViewHolder();
+ viewHolder.name = binding.contactDisplayName;
+ viewHolder.jid = binding.contactJid;
+ viewHolder.avatar = binding.contactPhoto;
+ viewHolder.tags = binding.tags;
+ viewHolder.flowWidget = binding.flowWidget;
+ binding.getRoot().setTag(viewHolder);
+ return viewHolder;
+ }
+ }
}
@@ -31,22 +31,18 @@ package eu.siacs.conversations.ui.util;
import android.app.Activity;
import android.content.SharedPreferences;
-import android.content.res.Configuration;
import android.preference.PreferenceManager;
import android.view.View;
-
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.core.content.ContextCompat;
-
import com.google.android.material.color.MaterialColors;
-
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.ui.Activities;
import eu.siacs.conversations.ui.ConversationFragment;
import eu.siacs.conversations.utils.UIHelper;
+import im.conversations.android.xmpp.model.stanza.Presence;
public class SendButtonTool {
@@ -110,28 +106,37 @@ public class SendButtonTool {
};
}
- public @ColorInt static int getSendButtonColor(final View view, final Presence.Status status) {
+ public @ColorInt static int getSendButtonColor(
+ final View view, final Presence.Availability status) {
final boolean nightMode = Activities.isNightMode(view.getContext());
return switch (status) {
- case OFFLINE -> MaterialColors.getColor(
- view, com.google.android.material.R.attr.colorOnSurface);
- case ONLINE, CHAT -> MaterialColors.harmonizeWithPrimary(
- view.getContext(),
- ContextCompat.getColor(
- view.getContext(), nightMode ? R.color.green_300 : R.color.green_800));
- case AWAY -> MaterialColors.harmonizeWithPrimary(
- view.getContext(),
- ContextCompat.getColor(
- view.getContext(), nightMode ? R.color.amber_300 : R.color.amber_800));
- case XA -> MaterialColors.harmonizeWithPrimary(
- view.getContext(),
- ContextCompat.getColor(
+ case OFFLINE ->
+ MaterialColors.getColor(
+ view, com.google.android.material.R.attr.colorOnSurface);
+ case ONLINE, CHAT ->
+ MaterialColors.harmonizeWithPrimary(
+ view.getContext(),
+ ContextCompat.getColor(
+ view.getContext(),
+ nightMode ? R.color.green_300 : R.color.green_800));
+ case AWAY ->
+ MaterialColors.harmonizeWithPrimary(
+ view.getContext(),
+ ContextCompat.getColor(
+ view.getContext(),
+ nightMode ? R.color.amber_300 : R.color.amber_800));
+ case XA ->
+ MaterialColors.harmonizeWithPrimary(
+ view.getContext(),
+ ContextCompat.getColor(
+ view.getContext(),
+ nightMode ? R.color.orange_300 : R.color.orange_800));
+ case DND ->
+ MaterialColors.harmonizeWithPrimary(
view.getContext(),
- nightMode ? R.color.orange_300 : R.color.orange_800));
- case DND -> MaterialColors.harmonizeWithPrimary(
- view.getContext(),
- ContextCompat.getColor(
- view.getContext(), nightMode ? R.color.red_300 : R.color.red_800));
+ ContextCompat.getColor(
+ view.getContext(),
+ nightMode ? R.color.red_300 : R.color.red_800));
};
}
}
@@ -22,12 +22,12 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Conversational;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.util.QuoteHelper;
import eu.siacs.conversations.worker.ExportBackupWorker;
import eu.siacs.conversations.xmpp.Jid;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
@@ -523,7 +523,7 @@ public class UIHelper {
return LOCATION_QUESTIONS.contains(body);
}
- public static void setStatus(final TextView textView, Presence.Status status) {
+ public static void setStatus(final TextView textView, Presence.Availability status) {
final @StringRes int text;
final @ColorRes int color =
switch (status) {
@@ -1,20 +0,0 @@
-package eu.siacs.conversations.xml;
-
-import com.google.common.io.ByteSource;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class XmlElementReader {
-
- public static Element read(byte[] bytes) throws IOException {
- return read(ByteSource.wrap(bytes).openStream());
- }
-
- public static Element read(InputStream inputStream) throws IOException {
- final XmlReader xmlReader = new XmlReader();
- xmlReader.setInputStream(inputStream);
- return xmlReader.readElement(xmlReader.readTag());
- }
-
-}
@@ -18,10 +18,16 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
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;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import de.gultsch.common.Patterns;
import eu.siacs.conversations.AppSettings;
@@ -38,12 +44,12 @@ import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramMechanism;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser;
+import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MemorizingTrustManager;
import eu.siacs.conversations.services.MessageArchiveService;
@@ -66,6 +72,9 @@ import eu.siacs.conversations.xml.XmlReader;
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.DiscoManager;
+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;
@@ -75,6 +84,7 @@ import im.conversations.android.xmpp.model.bind2.Bound;
import im.conversations.android.xmpp.model.cb.SaslChannelBinding;
import im.conversations.android.xmpp.model.csi.Active;
import im.conversations.android.xmpp.model.csi.Inactive;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.error.Condition;
import im.conversations.android.xmpp.model.fast.Fast;
import im.conversations.android.xmpp.model.fast.RequestToken;
@@ -126,6 +136,7 @@ import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
@@ -149,7 +160,6 @@ public class XmppConnection implements Runnable {
protected final Account account;
private final Features features = new Features(this);
- private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
private final HashMap<String, Jid> commands = new HashMap<>();
private final SparseArray<Stanza> mStanzaQueue = new SparseArray<>();
private final Hashtable<String, Pair<Iq, Consumer<Iq>>> packetCallbacks = new Hashtable<>();
@@ -177,7 +187,6 @@ public class XmppConnection implements Runnable {
private long lastSessionStarted = 0;
private long lastDiscoStarted = 0;
private boolean isMamPreferenceAlways = false;
- private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
@@ -200,6 +209,7 @@ public class XmppConnection implements Runnable {
private Resolver.Result seeOtherHostResolverResult;
private volatile Thread mThread;
private CountDownLatch mStreamCountDownLatch;
+ private final ClassToInstanceMap<AbstractManager> managers;
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
@@ -209,6 +219,12 @@ public class XmppConnection implements Runnable {
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.getApplicationContext(), this))
+ .build();
}
private static void fixResource(final Context context, final Account account) {
@@ -2000,9 +2016,7 @@ public class XmppConnection implements Runnable {
this.mStanzaQueue.clear();
}
this.redirectionUrl = null;
- synchronized (this.disco) {
- disco.clear();
- }
+ getManager(DiscoManager.class).clear();
synchronized (this.commands) {
this.commands.clear();
}
@@ -2165,41 +2179,99 @@ public class XmppConnection implements Runnable {
final boolean waitForDisco, final boolean carbonsEnabled) {
features.carbonsEnabled = carbonsEnabled;
features.blockListRequested = false;
- synchronized (this.disco) {
- this.disco.clear();
- }
+ getManager(DiscoManager.class).clear();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
- mPendingServiceDiscoveries.set(0);
mWaitForDisco.set(waitForDisco);
this.lastDiscoStarted = SystemClock.elapsedRealtime();
mXmppConnectionService.scheduleWakeUpCall(
Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode());
- final Element caps = streamFeatures.findChild("c");
- final String hash = caps == null ? null : caps.getAttribute("hash");
- final String ver = caps == null ? null : caps.getAttribute("ver");
- ServiceDiscoveryResult discoveryResult = null;
- if (hash != null && ver != null) {
- discoveryResult =
- mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
- }
- final boolean requestDiscoItemsFirst =
- !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
- if (requestDiscoItemsFirst) {
- sendServiceDiscoveryItems(account.getDomain());
- }
- if (discoveryResult == null) {
- sendServiceDiscoveryInfo(account.getDomain());
- } else {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
- disco.put(account.getDomain(), discoveryResult);
- }
+
+ final var nodeHash = streamFeatures.getCapabilities();
+ final var serverInfoFuture =
+ getManager(DiscoManager.class)
+ .infoOrCache(Entity.discoItem(account.getDomain()), nodeHash);
+
final var features = getFeatures();
if (!features.bind2()) {
discoverMamPreferences();
}
- sendServiceDiscoveryInfo(account.getJid().asBareJid());
- if (!requestDiscoItemsFirst) {
- sendServiceDiscoveryItems(account.getDomain());
+
+ final var accountInfoFuture =
+ getManager(DiscoManager.class)
+ .info(Entity.discoItem(account.getJid().asBareJid()), null);
+
+ final var itemsFuture =
+ getManager(DiscoManager.class).itemsWithInfo(Entity.discoItem(account.getDomain()));
+
+ final var catchingServerFuture =
+ Futures.catching(
+ serverInfoFuture,
+ DiscoManager.CapsHashMismatchException.class,
+ input -> {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": error in server caps",
+ input);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+
+ Futures.addCallback(
+ Futures.allAsList(accountInfoFuture, catchingServerFuture),
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(List<Object> result) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": advanced stream future done");
+ enableAdvancedStreamFeatures();
+ }
+
+ @Override
+ public void onFailure(@Nullable Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ "could not fetch disco for advanced stream features",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+
+ if (mWaitForDisco.get()) {
+ final ListenableFuture<Void> discoComplete =
+ Futures.whenAllComplete(serverInfoFuture, accountInfoFuture, itemsFuture)
+ .call(() -> null, MoreExecutors.directExecutor());
+ Futures.addCallback(
+ discoComplete,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ if (timeout(serverInfoFuture, accountInfoFuture, itemsFuture)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": reached timeout while waiting for disco");
+ return;
+ }
+ if (mWaitForDisco.compareAndSet(true, false)) {
+ finalizeBindOrError();
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": disco complete but bind was already"
+ + " finalized");
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "error in disco: ", t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ finalizeBind();
}
if (!mWaitForDisco.get()) {
@@ -2208,63 +2280,19 @@ public class XmppConnection implements Runnable {
this.lastSessionStarted = SystemClock.elapsedRealtime();
}
- private void sendServiceDiscoveryInfo(final Jid jid) {
- mPendingServiceDiscoveries.incrementAndGet();
- final Iq iq = new Iq(Iq.Type.GET);
- iq.setTo(jid);
- iq.query("http://jabber.org/protocol/disco#info");
- this.sendIqPacket(
- iq,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
- boolean advancedStreamFeaturesLoaded;
- synchronized (XmppConnection.this.disco) {
- ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
- if (jid.equals(account.getDomain())) {
- mXmppConnectionService.databaseBackend.insertDiscoveryResult(
- result);
- }
- disco.put(jid, result);
- advancedStreamFeaturesLoaded =
- disco.containsKey(account.getDomain())
- && disco.containsKey(account.getJid().asBareJid());
- }
- if (advancedStreamFeaturesLoaded
- && (jid.equals(account.getDomain())
- || jid.equals(account.getJid().asBareJid()))) {
- enableAdvancedStreamFeatures();
- }
- } else if (packet.getType() == Iq.Type.ERROR) {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": could not query disco info for "
- + jid.toString());
- final boolean serverOrAccount =
- jid.equals(account.getDomain())
- || jid.equals(account.getJid().asBareJid());
- final boolean advancedStreamFeaturesLoaded;
- if (serverOrAccount) {
- synchronized (XmppConnection.this.disco) {
- disco.put(jid, ServiceDiscoveryResult.empty());
- advancedStreamFeaturesLoaded =
- disco.containsKey(account.getDomain())
- && disco.containsKey(account.getJid().asBareJid());
- }
- } else {
- advancedStreamFeaturesLoaded = false;
- }
- if (advancedStreamFeaturesLoaded) {
- enableAdvancedStreamFeatures();
- }
- }
- if (packet.getType() != Iq.Type.TIMEOUT) {
- if (mPendingServiceDiscoveries.decrementAndGet() == 0
- && mWaitForDisco.compareAndSet(true, false)) {
- finalizeBind();
- }
+ private boolean timeout(final ListenableFuture<?>... futures) {
+ for (final ListenableFuture<?> future : futures) {
+ if (future.isDone()) {
+ try {
+ future.get();
+ } catch (final Exception e) {
+ if (Throwables.getRootCause(e) instanceof TimeoutException) {
+ return true;
}
- });
+ }
+ }
+ }
+ return false;
}
private void discoverMamPreferences() {
@@ -2288,39 +2316,42 @@ public class XmppConnection implements Runnable {
}
private void discoverCommands() {
- final Iq request = new Iq(Iq.Type.GET);
- request.setTo(account.getDomain());
- request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
- sendIqPacket(
- request,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
- if (query == null) {
- return;
- }
- final HashMap<String, Jid> commands = new HashMap<>();
- for (final Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- final String node = child.getAttribute("node");
- final Jid jid = child.getAttributeAsJid("jid");
- if (node != null && jid != null) {
- commands.put(node, jid);
- }
- }
- }
- synchronized (this.commands) {
- this.commands.clear();
- this.commands.putAll(commands);
+ final var future =
+ getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain()));
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Map<String, Jid> result) {
+ synchronized (XmppConnection.this.commands) {
+ XmppConnection.this.commands.clear();
+ XmppConnection.this.commands.putAll(result);
}
}
- });
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not fetch commands",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
}
public boolean isMamPreferenceAlways() {
return isMamPreferenceAlways;
}
+ private void finalizeBindOrError() {
+ try {
+ finalizeBind();
+ } catch (final Exception e) {
+ throw new Error(e);
+ }
+ }
+
private void finalizeBind() {
this.offlineMessagesRetrieved = false;
this.bindListener.run();
@@ -2344,46 +2375,6 @@ public class XmppConnection implements Runnable {
}
}
- private void sendServiceDiscoveryItems(final Jid server) {
- mPendingServiceDiscoveries.incrementAndGet();
- final Iq iq = new Iq(Iq.Type.GET);
- iq.setTo(server.getDomain());
- iq.query("http://jabber.org/protocol/disco#items");
- this.sendIqPacket(
- iq,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
- final HashSet<Jid> items = new HashSet<>();
- final List<Element> elements = packet.query().getChildren();
- for (final Element element : elements) {
- if (element.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(
- element.getAttributeAsJid("jid"));
- if (jid != null && !jid.equals(account.getDomain())) {
- items.add(jid);
- }
- }
- }
- for (Jid jid : items) {
- sendServiceDiscoveryInfo(jid);
- }
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": could not query disco items of "
- + server);
- }
- if (packet.getType() != Iq.Type.TIMEOUT) {
- if (mPendingServiceDiscoveries.decrementAndGet() == 0
- && mWaitForDisco.compareAndSet(true, false)) {
- finalizeBind();
- }
- }
- });
- }
-
private void sendEnableCarbons() {
final Iq iq = new Iq(Iq.Type.SET);
iq.addChild("enable", Namespace.CARBONS);
@@ -2726,28 +2717,23 @@ public class XmppConnection implements Runnable {
this.boundStreamFeatures = null;
}
- private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
- synchronized (this.disco) {
- final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
- for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
- if (cursor.getValue().getFeatures().contains(feature)) {
- items.add(cursor);
- }
- }
- return items;
- }
+ public <M extends AbstractManager> M getManager(final Class<M> clazz) {
+ return this.managers.getInstance(clazz);
}
- public Entry<Jid, ServiceDiscoveryResult> getServiceDiscoveryResultByFeature(
- final String feature) {
- synchronized (this.disco) {
- for (final var cursor : this.disco.entrySet()) {
- if (cursor.getValue().getFeatures().contains(feature)) {
- return cursor;
- }
+ private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
+ final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
+ for (final Entry<Jid, InfoQuery> cursor :
+ getManager(DiscoManager.class).getServerItems().entrySet()) {
+ if (cursor.getValue().getFeatureStrings().contains(feature)) {
+ items.add(cursor);
}
- return null;
}
+ return items;
+ }
+
+ public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
+ return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
}
public Jid findDiscoItemByFeature(final String feature) {
@@ -2775,15 +2761,14 @@ public class XmppConnection implements Runnable {
public List<String> getMucServers() {
List<String> servers = new ArrayList<>();
- synchronized (this.disco) {
- for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
- final ServiceDiscoveryResult value = cursor.getValue();
- if (value.getFeatures().contains("http://jabber.org/protocol/muc")
- && value.hasIdentity("conference", "text")
- && !value.getFeatures().contains("jabber:iq:gateway")
- && !value.hasIdentity("conference", "irc")) {
- servers.add(cursor.getKey().toString());
- }
+ for (final Entry<Jid, InfoQuery> entry :
+ getManager(DiscoManager.class).getServerItems().entrySet()) {
+ final var value = entry.getValue();
+ if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc")
+ && value.hasIdentityWithCategoryAndType("conference", "text")
+ && !value.getFeatureStrings().contains("jabber:iq:gateway")
+ && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
+ servers.add(entry.getKey().toString());
}
}
return servers;
@@ -2910,6 +2895,10 @@ public class XmppConnection implements Runnable {
this.changeStatus(Account.State.CONNECTION_TIMEOUT);
}
+ public Account getAccount() {
+ return this.account;
+ }
+
private class MyKeyManager implements X509KeyManager {
@Override
public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) {
@@ -3033,6 +3022,29 @@ public class XmppConnection implements Runnable {
}
}
+ public abstract static class Delegate {
+
+ protected final Context context;
+ protected final XmppConnection connection;
+
+ protected Delegate(final Context context, final XmppConnection connection) {
+ this.context = context;
+ this.connection = connection;
+ }
+
+ protected Account getAccount() {
+ return connection.account;
+ }
+
+ protected DatabaseBackend getDatabase() {
+ return DatabaseBackend.getInstance(context);
+ }
+
+ protected <T extends AbstractManager> T getManager(final Class<T> type) {
+ return connection.getManager(type);
+ }
+ }
+
public class Features {
XmppConnection connection;
private boolean carbonsEnabled = false;
@@ -3044,10 +3056,8 @@ public class XmppConnection implements Runnable {
}
private boolean hasDiscoFeature(final Jid server, final String feature) {
- synchronized (XmppConnection.this.disco) {
- final ServiceDiscoveryResult sdr = connection.disco.get(server);
- return sdr != null && sdr.getFeatures().contains(feature);
- }
+ final var infoQuery = getManager(DiscoManager.class).get(server);
+ return infoQuery != null && infoQuery.getFeatureStrings().contains(feature);
}
public boolean carbons() {
@@ -3103,19 +3113,16 @@ public class XmppConnection implements Runnable {
}
public boolean pep() {
- synchronized (XmppConnection.this.disco) {
- ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
- return info != null && info.hasIdentity("pubsub", "pep");
- }
+ final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
+ return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep");
}
public boolean pepPersistent() {
- synchronized (XmppConnection.this.disco) {
- ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid());
- return info != null
- && info.getFeatures()
- .contains("http://jabber.org/protocol/pubsub#persistent-items");
- }
+ final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
+ return infoQuery != null
+ && infoQuery
+ .getFeatureStrings()
+ .contains("http://jabber.org/protocol/pubsub#persistent-items");
}
public boolean bind2() {
@@ -3150,9 +3157,9 @@ public class XmppConnection implements Runnable {
return MessageArchiveService.Version.has(getAccountFeatures());
}
- public List<String> getAccountFeatures() {
- ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
- return result == null ? Collections.emptyList() : result.getFeatures();
+ public Collection<String> getAccountFeatures() {
+ final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
+ return infoQuery == null ? Collections.emptyList() : infoQuery.getFeatureStrings();
}
public boolean push() {
@@ -3169,12 +3176,12 @@ public class XmppConnection implements Runnable {
}
public HttpUrl getServiceOutageStatus() {
- final var disco = connection.disco.get(account.getDomain());
+ final var disco = getManager(DiscoManager.class).get(account.getDomain());
if (disco == null) {
return null;
}
final var address =
- disco.getExtendedDiscoInformation(
+ disco.getServiceDiscoveryExtension(
Namespace.SERVICE_OUTAGE_STATUS, "external-status-addresses");
if (Strings.isNullOrEmpty(address)) {
return null;
@@ -3195,7 +3202,7 @@ public class XmppConnection implements Runnable {
maxSize =
Long.parseLong(
result.getValue()
- .getExtendedDiscoInformation(
+ .getServiceDiscoveryExtension(
Namespace.HTTP_UPLOAD, "max-file-size"));
} catch (final Exception e) {
return true;
@@ -3224,7 +3231,7 @@ public class XmppConnection implements Runnable {
try {
return Long.parseLong(
result.getValue()
- .getExtendedDiscoInformation(
+ .getServiceDiscoveryExtension(
Namespace.HTTP_UPLOAD, "max-file-size"));
} catch (final Exception e) {
return -1;
@@ -5,18 +5,16 @@ import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.model.jingle.Jingle;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.util.Arrays;
@@ -331,14 +329,15 @@ public abstract class AbstractJingleConnection {
}
protected boolean remoteHasFeature(final String feature) {
- final Contact contact = id.getContact();
- final Presence presence =
- contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
- final ServiceDiscoveryResult serviceDiscoveryResult =
- presence == null ? null : presence.getServiceDiscoveryResult();
- final List<String> features =
- serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
- return features != null && features.contains(feature);
+ final var connection = id.account.getXmppConnection();
+ if (connection == null) {
+ return false;
+ }
+ final var infoQuery = connection.getManager(DiscoManager.class).get(id.with);
+ if (infoQuery == null) {
+ return false;
+ }
+ return infoQuery.hasFeature(feature);
}
public static class Id {
@@ -3,37 +3,34 @@ package eu.siacs.conversations.xmpp.jingle;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableSet;
-
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.Set;
-import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.Presences;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
-import eu.siacs.conversations.xml.Namespace;
-
public class RtpCapability {
- private static final List<String> BASIC_RTP_REQUIREMENTS = Arrays.asList(
- Namespace.JINGLE,
- Namespace.JINGLE_TRANSPORT_ICE_UDP,
- Namespace.JINGLE_APPS_RTP,
- Namespace.JINGLE_APPS_DTLS
- );
- private static final Collection<String> VIDEO_REQUIREMENTS = Arrays.asList(
- Namespace.JINGLE_FEATURE_AUDIO,
- Namespace.JINGLE_FEATURE_VIDEO
- );
+ private static final List<String> BASIC_RTP_REQUIREMENTS =
+ Arrays.asList(
+ Namespace.JINGLE,
+ Namespace.JINGLE_TRANSPORT_ICE_UDP,
+ Namespace.JINGLE_APPS_RTP,
+ Namespace.JINGLE_APPS_DTLS);
+ private static final Collection<String> VIDEO_REQUIREMENTS =
+ Arrays.asList(Namespace.JINGLE_FEATURE_AUDIO, Namespace.JINGLE_FEATURE_VIDEO);
- public static Capability check(final Presence presence) {
- final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
- final Set<String> features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures());
+ public static Capability check(final InfoQuery infoQuery) {
+ final Set<String> features =
+ infoQuery == null
+ ? Collections.emptySet()
+ : ImmutableSet.copyOf(infoQuery.getFeatureStrings());
if (features.containsAll(BASIC_RTP_REQUIREMENTS)) {
if (features.containsAll(VIDEO_REQUIREMENTS)) {
return Capability.VIDEO;
@@ -46,15 +43,23 @@ public class RtpCapability {
}
public static String[] filterPresences(final Contact contact, Capability required) {
+ final var connection = contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return new String[0];
+ }
final Presences presences = contact.getPresences();
final ArrayList<String> resources = new ArrayList<>();
- for (final Map.Entry<String, Presence> presence : presences.getPresencesMap().entrySet()) {
- final Capability capability = check(presence.getValue());
+ for (final String resource : presences.getPresencesMap().keySet()) {
+ final var jid =
+ Strings.isNullOrEmpty(resource)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(resource);
+ final Capability capability = check(connection.getManager(DiscoManager.class).get(jid));
if (capability == Capability.NONE) {
continue;
}
if (required == Capability.AUDIO || capability == required) {
- resources.add(presence.getKey());
+ resources.add(resource);
}
}
return resources.toArray(new String[0]);
@@ -69,9 +74,17 @@ public class RtpCapability {
if (presences.isEmpty() && allowFallback && contact.getAccount().isEnabled()) {
return contact.getRtpCapability();
}
+ final var connection = contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return Capability.NONE;
+ }
Capability result = Capability.NONE;
- for (final Presence presence : presences.getPresences()) {
- Capability capability = check(presence);
+ for (final String resource : presences.getPresencesMap().keySet()) {
+ final var jid =
+ Strings.isNullOrEmpty(resource)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(resource);
+ final Capability capability = check(connection.getManager(DiscoManager.class).get(jid));
if (capability == Capability.VIDEO) {
result = capability;
} else if (capability == Capability.AUDIO && result == Capability.NONE) {
@@ -83,18 +96,43 @@ public class RtpCapability {
// do all devices that support Rtp Call also support JMI?
public static boolean jmiSupport(final Contact contact) {
+ final var connection = contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return false;
+ }
return !Collections2.transform(
- Collections2.filter(
- contact.getPresences().getPresences(),
- p -> RtpCapability.check(p) != RtpCapability.Capability.NONE),
- p -> {
- ServiceDiscoveryResult disco = p.getServiceDiscoveryResult();
- return disco != null && disco.getFeatures().contains(Namespace.JINGLE_MESSAGE);
- }).contains(false);
+ Collections2.filter(
+ contact.getPresences().getPresencesMap().keySet(),
+ p ->
+ RtpCapability.check(
+ connection
+ .getManager(DiscoManager.class)
+ .get(
+ Strings.isNullOrEmpty(p)
+ ? contact.getJid()
+ .asBareJid()
+ : contact.getJid()
+ .withResource(
+ p)))
+ != Capability.NONE),
+ p -> {
+ final var disco =
+ connection
+ .getManager(DiscoManager.class)
+ .get(
+ Strings.isNullOrEmpty(p)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(p));
+ return disco != null
+ && disco.getFeatureStrings().contains(Namespace.JINGLE_MESSAGE);
+ })
+ .contains(false);
}
public enum Capability {
- NONE, AUDIO, VIDEO;
+ NONE,
+ AUDIO,
+ VIDEO;
public static Capability of(String value) {
if (Strings.isNullOrEmpty(value)) {
@@ -107,5 +145,4 @@ public class RtpCapability {
}
}
}
-
}
@@ -0,0 +1,11 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+public abstract class AbstractManager extends XmppConnection.Delegate {
+
+ protected AbstractManager(final Context context, final XmppConnection connection) {
+ super(context, connection);
+ }
+}
@@ -0,0 +1,307 @@
+package eu.siacs.conversations.xmpp.manager;
+
+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.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.Config;
+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.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.stanza.Iq;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+public class DiscoManager extends AbstractManager {
+
+ public static final String CAPABILITY_NODE = "http://conversations.im";
+
+ // this is the runtime cache that stores disco information for all entities seen during a
+ // session
+
+ // a caps cache will be build in the database
+
+ private final Map<Jid, InfoQuery> entityInformation = new HashMap<>();
+ private final Map<Jid, ImmutableSet<Jid>> discoItems = new HashMap<>();
+
+ public DiscoManager(Context context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public static EntityCapabilities.Hash buildHashFromNode(final String node) {
+ final var capsPrefix = CAPABILITY_NODE + "#";
+ final var caps2Prefix = Namespace.ENTITY_CAPABILITIES_2 + "#";
+ if (node.startsWith(capsPrefix)) {
+ final String hash = node.substring(capsPrefix.length());
+ if (Strings.isNullOrEmpty(hash)) {
+ return null;
+ }
+ if (BaseEncoding.base64().canDecode(hash)) {
+ return EntityCapabilities.EntityCapsHash.of(hash);
+ }
+ } else if (node.startsWith(caps2Prefix)) {
+ final String caps = node.substring(caps2Prefix.length());
+ if (Strings.isNullOrEmpty(caps)) {
+ return null;
+ }
+ final int separator = caps.lastIndexOf('.');
+ if (separator < 0) {
+ return null;
+ }
+ final Hash.Algorithm algorithm = Hash.Algorithm.tryParse(caps.substring(0, separator));
+ final String hash = caps.substring(separator + 1);
+ if (algorithm == null || Strings.isNullOrEmpty(hash)) {
+ return null;
+ }
+ if (BaseEncoding.base64().canDecode(hash)) {
+ return EntityCapabilities2.EntityCaps2Hash.of(algorithm, hash);
+ }
+ }
+ return null;
+ }
+
+ public ListenableFuture<Void> infoOrCache(
+ final Entity entity,
+ final im.conversations.android.xmpp.model.capabilties.EntityCapabilities.NodeHash
+ nodeHash) {
+ if (nodeHash == null) {
+ return infoOrCache(entity, null, null);
+ }
+ return infoOrCache(entity, nodeHash.node, nodeHash.hash);
+ }
+
+ public ListenableFuture<Void> infoOrCache(
+ final Entity entity, final String node, final EntityCapabilities.Hash hash) {
+ final var cached = getDatabase().getInfoQuery(hash);
+ if (cached != null) {
+ if (node == null || hash != null) {
+ this.put(entity.address, cached);
+ }
+ return Futures.immediateFuture(null);
+ }
+ return Futures.transform(
+ info(entity, node, hash), f -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<InfoQuery> info(
+ @NonNull final Entity entity, @Nullable final String node) {
+ return info(entity, node, null);
+ }
+
+ 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 iqRequest = new Iq(Iq.Type.GET);
+ iqRequest.setTo(entity.address);
+ final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery());
+ if (requestNode != null) {
+ infoQueryRequest.setNode(requestNode);
+ }
+ final var future = connection.sendIqPacket(iqRequest);
+ return Futures.transform(
+ future,
+ iqResult -> {
+ final var infoQuery = iqResult.getExtension(InfoQuery.class);
+ if (infoQuery == null) {
+ throw new IllegalStateException("Response did not have query child");
+ }
+ if (!Objects.equals(requestNode, infoQuery.getNode())) {
+ throw new IllegalStateException(
+ "Node in response did not match node in request");
+ }
+
+ if (node == null
+ || (hash != null
+ && hash.capabilityNode(node).equals(infoQuery.getNode()))) {
+ // only storing results w/o nodes
+ this.put(entity.address, infoQuery);
+ }
+
+ final var caps = EntityCapabilities.hash(infoQuery);
+ final var caps2 = EntityCapabilities2.hash(infoQuery);
+ if (hash instanceof EntityCapabilities.EntityCapsHash) {
+ checkMatch(
+ (EntityCapabilities.EntityCapsHash) hash,
+ caps,
+ EntityCapabilities.EntityCapsHash.class);
+ }
+ if (hash instanceof EntityCapabilities2.EntityCaps2Hash) {
+ checkMatch(
+ (EntityCapabilities2.EntityCaps2Hash) hash,
+ caps2,
+ EntityCapabilities2.EntityCaps2Hash.class);
+ }
+ // we want to avoid caching disco info for entities that put variable data (like
+ // number of occupants in a MUC) into it
+ final boolean cache =
+ Objects.nonNull(hash)
+ || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES)
+ || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES_2);
+
+ if (cache) {
+ getDatabase().insertCapsCache(caps, caps2, infoQuery);
+ }
+
+ return infoQuery;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private <H extends EntityCapabilities.Hash> void checkMatch(
+ final H expected, final H was, final Class<H> clazz) {
+ if (Arrays.equals(expected.hash, was.hash)) {
+ return;
+ }
+ throw new CapsHashMismatchException(
+ String.format(
+ "%s mismatch. Expected %s was %s",
+ clazz.getSimpleName(),
+ BaseEncoding.base64().encode(expected.hash),
+ BaseEncoding.base64().encode(was.hash)));
+ }
+
+ public ListenableFuture<Collection<Item>> items(final Entity.DiscoItem entity) {
+ return items(entity, null);
+ }
+
+ public ListenableFuture<Collection<Item>> items(
+ final Entity.DiscoItem entity, @Nullable final String node) {
+ final var requestNode = Strings.emptyToNull(node);
+ final var iqPacket = new Iq(Iq.Type.GET);
+ iqPacket.setTo(entity.address);
+ final ItemsQuery itemsQueryRequest = iqPacket.addExtension(new ItemsQuery());
+ if (requestNode != null) {
+ itemsQueryRequest.setNode(requestNode);
+ }
+ final var future = connection.sendIqPacket(iqPacket);
+ return Futures.transform(
+ future,
+ iqResult -> {
+ final var itemsQuery = iqResult.getExtension(ItemsQuery.class);
+ if (itemsQuery == null) {
+ throw new IllegalStateException();
+ }
+ if (!Objects.equals(requestNode, itemsQuery.getNode())) {
+ throw new IllegalStateException(
+ "Node in response did not match node in request");
+ }
+ final var items = itemsQuery.getExtensions(Item.class);
+
+ final var validItems =
+ Collections2.filter(items, i -> Objects.nonNull(i.getJid()));
+
+ final var itemsAsAddresses =
+ ImmutableSet.copyOf(Collections2.transform(validItems, Item::getJid));
+ if (node == null) {
+ this.discoItems.put(entity.address, itemsAsAddresses);
+ }
+ return validItems;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<List<InfoQuery>> itemsWithInfo(final Entity.DiscoItem entity) {
+ final var itemsFutures = items(entity);
+ final var filtered =
+ Futures.transform(
+ itemsFutures,
+ items ->
+ Collections2.filter(
+ items,
+ i ->
+ i.getNode() == null
+ && !entity.address.equals(i.getJid())),
+ MoreExecutors.directExecutor());
+ return Futures.transformAsync(
+ filtered,
+ items -> {
+ Collection<ListenableFuture<InfoQuery>> infoFutures =
+ Collections2.transform(
+ items, i -> info(Entity.discoItem(i.getJid()), i.getNode()));
+ return Futures.allAsList(infoFutures);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Map<String, Jid>> commands(final Entity.DiscoItem entity) {
+ final var itemsFuture = items(entity, Namespace.COMMANDS);
+ return Futures.transform(
+ itemsFuture,
+ items -> {
+ final var builder = new ImmutableMap.Builder<String, Jid>();
+ for (final var item : items) {
+ final var jid = item.getJid();
+ final var node = item.getNode();
+ if (Jid.Invalid.isValid(jid) && node != null) {
+ builder.put(node, jid);
+ }
+ }
+ return builder.buildKeepingLast();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public Map<Jid, InfoQuery> getServerItems() {
+ final var builder = new ImmutableMap.Builder<Jid, InfoQuery>();
+ final var domain = connection.getAccount().getDomain();
+ final var domainInfoQuery = get(domain);
+ if (domainInfoQuery != null) {
+ builder.put(domain, domainInfoQuery);
+ }
+ final var items = this.discoItems.get(domain);
+ if (items == null) {
+ return builder.build();
+ }
+ for (final var item : items) {
+ final var infoQuery = get(item);
+ if (infoQuery == null) {
+ continue;
+ }
+ builder.put(item, infoQuery);
+ }
+ return builder.buildKeepingLast();
+ }
+
+ private void put(final Jid address, final InfoQuery infoQuery) {
+ synchronized (this.entityInformation) {
+ this.entityInformation.put(address, infoQuery);
+ }
+ }
+
+ public InfoQuery get(final Jid address) {
+ synchronized (this.entityInformation) {
+ return this.entityInformation.get(address);
+ }
+ }
+
+ public void clear() {
+ synchronized (this.entityInformation) {
+ this.entityInformation.clear();
+ }
+ }
+
+ public static final class CapsHashMismatchException extends IllegalStateException {
+ public CapsHashMismatchException(final String message) {
+ super(message);
+ }
+ }
+}
@@ -0,0 +1,21 @@
+package im.conversations.android.xml;
+
+import com.google.common.io.ByteSource;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.XmlReader;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class XmlElementReader {
+
+ public static Element read(byte[] bytes) throws IOException {
+ return read(ByteSource.wrap(bytes).openStream());
+ }
+
+ public static Element read(final InputStream inputStream) throws IOException {
+ try (final XmlReader xmlReader = new XmlReader()) {
+ xmlReader.setInputStream(inputStream);
+ return xmlReader.readElement(xmlReader.readTag());
+ }
+ }
+}
@@ -1,6 +1,6 @@
package im.conversations.android.xmpp;
-import org.jxmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.Jid;
public abstract class Entity {
@@ -8,7 +8,6 @@ import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Bytes;
-
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.xmpp.model.Hash;
import im.conversations.android.xmpp.model.data.Data;
@@ -42,16 +41,12 @@ public class EntityCapabilities2 {
}
private static HashFunction toHashFunction(final Hash.Algorithm algorithm) {
- switch (algorithm) {
- case SHA_1:
- return Hashing.sha1();
- case SHA_256:
- return Hashing.sha256();
- case SHA_512:
- return Hashing.sha512();
- default:
- throw new IllegalArgumentException("Unknown hash algorithm");
- }
+ return switch (algorithm) {
+ case SHA_1 -> Hashing.sha1();
+ case SHA_256 -> Hashing.sha256();
+ case SHA_512 -> Hashing.sha512();
+ default -> throw new IllegalArgumentException("Unknown hash algorithm");
+ };
}
private static String asHex(final String message) {
@@ -29,6 +29,10 @@ public class Data extends Extension {
this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName()));
}
+ public Field getFieldByName(final String name) {
+ return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null);
+ }
+
private void addField(final String name, final Object value) {
addField(name, value, null);
}
@@ -1,6 +1,8 @@
package im.conversations.android.xmpp.model.data;
-import eu.siacs.conversations.xml.Element;
+
import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import eu.siacs.conversations.xml.Element;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
@@ -19,6 +21,10 @@ public class Field extends Extension {
return Collections2.transform(getExtensions(Value.class), Element::getContent);
}
+ public String getValue() {
+ return Iterables.getFirst(getValues(), null);
+ }
+
public void setFieldName(String name) {
this.setAttribute("var", name);
}
@@ -1,9 +1,12 @@
package im.conversations.android.xmpp.model.disco.info;
+import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
import java.util.Collection;
+import java.util.Objects;
@XmlElement(name = "query")
public class InfoQuery extends Extension {
@@ -35,4 +38,39 @@ public class InfoQuery extends Extension {
public boolean hasIdentityWithCategory(final String category) {
return Iterables.any(getIdentities(), i -> category.equals(i.getCategory()));
}
+
+ public boolean hasIdentityWithCategoryAndType(final String category, final String type) {
+ return Iterables.any(
+ getIdentities(), i -> category.equals(i.getCategory()) && type.equals(i.getType()));
+ }
+
+ public Collection<String> getFeatureStrings() {
+ return Collections2.filter(
+ Collections2.transform(getFeatures(), Feature::getVar), Objects::nonNull);
+ }
+
+ public Collection<Data> getServiceDiscoveryExtensions() {
+ return getExtensions(Data.class);
+ }
+
+ public Data getServiceDiscoveryExtension(final String formType) {
+ return Iterables.find(
+ getServiceDiscoveryExtensions(), e -> formType.equals(e.getFormType()), null);
+ }
+
+ public String getServiceDiscoveryExtension(final String formType, final String fieldName) {
+ final var extension =
+ Iterables.find(
+ getServiceDiscoveryExtensions(),
+ e -> formType.equals(e.getFormType()),
+ null);
+ if (extension == null) {
+ return null;
+ }
+ final var field = extension.getFieldByName(fieldName);
+ if (field == null) {
+ return null;
+ }
+ return field.getValue();
+ }
}
@@ -1,7 +1,10 @@
package im.conversations.android.xmpp.model.stanza;
+import com.google.common.base.Strings;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
+import im.conversations.android.xmpp.model.jabber.Show;
+import im.conversations.android.xmpp.model.jabber.Status;
@XmlElement
public class Presence extends Stanza implements EntityCapabilities {
@@ -9,4 +12,63 @@ public class Presence extends Stanza implements EntityCapabilities {
public Presence() {
super(Presence.class);
}
+
+ public Availability getAvailability() {
+ final var show = getExtension(Show.class);
+ if (show == null) {
+ return Availability.ONLINE;
+ }
+ return Availability.valueOfShown(show.getContent());
+ }
+
+ public void setAvailability(final Availability availability) {
+ if (availability == null || availability == Availability.ONLINE) {
+ return;
+ }
+ this.addExtension(new Show()).setContent(availability.toShowString());
+ }
+
+ public void setStatus(final String status) {
+ if (Strings.isNullOrEmpty(status)) {
+ return;
+ }
+ this.addExtension(new Status()).setContent(status);
+ }
+
+ public String getStatus() {
+ final var status = getExtension(Status.class);
+ return status == null ? null : status.getContent();
+ }
+
+ public enum Availability {
+ CHAT,
+ ONLINE,
+ AWAY,
+ XA,
+ DND,
+ OFFLINE;
+
+ public String toShowString() {
+ return switch (this) {
+ case CHAT -> "chat";
+ case AWAY -> "away";
+ case XA -> "xa";
+ case DND -> "dnd";
+ default -> null;
+ };
+ }
+
+ public static Availability valueOfShown(final String content) {
+ if (Strings.isNullOrEmpty(content)) {
+ return Availability.ONLINE;
+ }
+ return switch (content) {
+ case "away" -> Availability.AWAY;
+ case "xa" -> Availability.XA;
+ case "dnd" -> Availability.DND;
+ case "chat" -> Availability.CHAT;
+ default -> Availability.ONLINE;
+ };
+ }
+ }
}
@@ -0,0 +1,339 @@
+package im.conversations.android.xmpp;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNull;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xml.XmlElementReader;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.ConscryptMode;
+
+@RunWith(RobolectricTestRunner.class)
+@ConscryptMode(ConscryptMode.Mode.OFF)
+public class EntityCapabilitiesTest {
+
+ @Test
+ public void entityCaps() throws IOException {
+ final String xml =
+ """
+ <query xmlns='http://jabber.org/protocol/disco#info'
+ node='http://code.google.com/p/exodus#QgayPKawpkPSDYmwT/WM94uAlu0='>
+ <identity category='client' name='Exodus 0.9.1' type='pc'/>
+ <feature var='http://jabber.org/protocol/caps'/>
+ <feature var='http://jabber.org/protocol/disco#items'/>
+ <feature var='http://jabber.org/protocol/disco#info'/>
+ <feature var='http://jabber.org/protocol/muc'/>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", var);
+ }
+
+ @Test
+ public void entityCapsComplexExample() throws IOException {
+ final String xml =
+ """
+ <query xmlns='http://jabber.org/protocol/disco#info'
+ node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
+ <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
+ <identity xml:lang='el' category='client' name='Ξ¨ 0.11' type='pc'/>
+ <feature var='http://jabber.org/protocol/caps'/>
+ <feature var='http://jabber.org/protocol/disco#info'/>
+ <feature var='http://jabber.org/protocol/disco#items'/>
+ <feature var='http://jabber.org/protocol/muc'/>
+ <x xmlns='jabber:x:data' type='result'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field var='ip_version' type='text-multi' >
+ <value>ipv4</value>
+ <value>ipv6</value>
+ </field>
+ <field var='os'>
+ <value>Mac</value>
+ </field>
+ <field var='os_version'>
+ <value>10.5.1</value>
+ </field>
+ <field var='software'>
+ <value>Psi</value>
+ </field>
+ <field var='software_version'>
+ <value>0.11</value>
+ </field>
+ </x>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", var);
+ }
+
+ @Test
+ public void entityCapsOpenFire() throws IOException {
+ final String xml =
+ """
+ <iq type="result" xmlns="jabber:client" to="inputmice3@igniterealtime.org/Conversations.cI4W" from="igniterealtime.org" id="L3xl8X8_kzvx">
+ <query node="https://www.igniterealtime.org/projects/openfire/#Cd91QBSG4JGOCEvRsSz64xeJPMk=" xmlns="http://jabber.org/protocol/disco#info">
+ <identity name="Openfire Server" type="im" category="server" xmlns="http://jabber.org/protocol/disco#info"/>
+ <identity type="pep" category="pubsub" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0#embed-presence-directed" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/caps" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-default" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#purge-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscription-options" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0#embed-message " xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#outcast-affiliation" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="msgoffline" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:register" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#config-node" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-create" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:mam:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:mam:1" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:mam:2" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:fulltext:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#persistent-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-and-configure" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:time" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#manage-subscriptions" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:bookmarks-conversion:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/offline" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish-options" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:carbons:2" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/address" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#collections" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="vcard-temp" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#get-pending" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:blocking" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#presence-notifications" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:ping" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:archive:manage" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#filtered-notifications" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:push:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#meta-data" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#item-ids" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:roster" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#instant-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#modify-affiliations" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0#embed-presence-sub" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#publisher-affiliation" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#access-open" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:version" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retract-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:extdisco:1" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:privacy" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:extdisco:2" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/commands" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:last" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:serverinfo:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:archive:auto" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#info" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:private" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/rsm" xmlns="http://jabber.org/protocol/disco#info"/>
+ <x type="result" xmlns="jabber:x:data">
+ <field var="FORM_TYPE" type="hidden" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field var="os" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">Linux</value>
+ </field>
+ <field var="os_version" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">4.14.355-276.618.amzn2.x86_64 amd64 - Java 17.0.14</value>
+ </field>
+ <field var="software" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">Openfire</value>
+ </field>
+ <field var="software_version" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">5.0.0 Alpha</value>
+ </field>
+ </x>
+ <x type="result" xmlns="jabber:x:data">
+ <field var="FORM_TYPE" type="hidden" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">http://jabber.org/network/serverinfo</value>
+ </field>
+ <field var="admin-addresses" type="list-multi" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">xmpp:dwd@dave.cridland.net</value>
+ <value xmlns="jabber:x:data">xmpp:akrherz@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:benjamin@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:benjamin@holyarmy.org</value>
+ <value xmlns="jabber:x:data">xmpp:csh@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:dan.caseley@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:dan.caseley@surevine.com</value>
+ <value xmlns="jabber:x:data">xmpp:flow@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:gdt@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:greg.d.thomas@gmail.com</value>
+ <value xmlns="jabber:x:data">xmpp:guus.der.kinderen@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:guus.der.kinderen@gmail.com</value>
+ <value xmlns="jabber:x:data">xmpp:lg@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:rcollier@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:robincollier@hotmail.com</value>
+ </field>
+ </x>
+ </query>
+</iq>
+""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(Iq.class));
+ final var iq = (Iq) element;
+ final InfoQuery info = iq.getExtension(InfoQuery.class);
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var);
+ }
+
+ @Test
+ public void caps2() throws IOException {
+ final String xml =
+ """
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" name="BombusMod" type="mobile"/>
+ <feature var="http://jabber.org/protocol/si"/>
+ <feature var="http://jabber.org/protocol/bytestreams"/>
+ <feature var="http://jabber.org/protocol/chatstates"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="jabber:iq:time"/>
+ <feature var="jabber:iq:privacy"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="http://jabber.org/protocol/rosterx"/>
+ <feature var="urn:xmpp:time"/>
+ <feature var="jabber:x:oob"/>
+ <feature var="http://jabber.org/protocol/ibb"/>
+ <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
+ <feature var="urn:xmpp:receipts"/>
+ <feature var="jabber:iq:roster"/>
+ <feature var="jabber:iq:last"/>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities2.hash(info).encoded();
+ Assert.assertEquals("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=", var);
+ }
+
+ @Test
+ public void caps2complex() throws IOException {
+ final String xml =
+ """
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" name="Tkabber" type="pc" xml:lang="en"/>
+ <identity category="client" name="Π’ΠΊΠ°Π±Π±Π΅Ρ" type="pc" xml:lang="ru"/>
+ <feature var="games:board"/>
+ <feature var="http://jabber.org/protocol/activity"/>
+ <feature var="http://jabber.org/protocol/activity+notify"/>
+ <feature var="http://jabber.org/protocol/bytestreams"/>
+ <feature var="http://jabber.org/protocol/chatstates"/>
+ <feature var="http://jabber.org/protocol/commands"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="http://jabber.org/protocol/evil"/>
+ <feature var="http://jabber.org/protocol/feature-neg"/>
+ <feature var="http://jabber.org/protocol/geoloc"/>
+ <feature var="http://jabber.org/protocol/geoloc+notify"/>
+ <feature var="http://jabber.org/protocol/ibb"/>
+ <feature var="http://jabber.org/protocol/iqibb"/>
+ <feature var="http://jabber.org/protocol/mood"/>
+ <feature var="http://jabber.org/protocol/mood+notify"/>
+ <feature var="http://jabber.org/protocol/rosterx"/>
+ <feature var="http://jabber.org/protocol/si"/>
+ <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
+ <feature var="http://jabber.org/protocol/tune"/>
+ <feature var="http://www.facebook.com/xmpp/messages"/>
+ <feature var="http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"/>
+ <feature var="jabber:iq:avatar"/>
+ <feature var="jabber:iq:browse"/>
+ <feature var="jabber:iq:dtcp"/>
+ <feature var="jabber:iq:filexfer"/>
+ <feature var="jabber:iq:ibb"/>
+ <feature var="jabber:iq:inband"/>
+ <feature var="jabber:iq:jidlink"/>
+ <feature var="jabber:iq:last"/>
+ <feature var="jabber:iq:oob"/>
+ <feature var="jabber:iq:privacy"/>
+ <feature var="jabber:iq:roster"/>
+ <feature var="jabber:iq:time"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="jabber:x:data"/>
+ <feature var="jabber:x:event"/>
+ <feature var="jabber:x:oob"/>
+ <feature var="urn:xmpp:avatar:metadata+notify"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="urn:xmpp:receipts"/>
+ <feature var="urn:xmpp:time"/>
+ <x xmlns="jabber:x:data" type="result">
+ <field type="hidden" var="FORM_TYPE">
+ <value>urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field var="software">
+ <value>Tkabber</value>
+ </field>
+ <field var="software_version">
+ <value>0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)</value>
+ </field>
+ <field var="os">
+ <value>Windows</value>
+ </field>
+ <field var="os_version">
+ <value>XP</value>
+ </field>
+ </x>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities2.hash(info).encoded();
+ Assert.assertEquals("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=", var);
+ }
+
+ @Test
+ public void parseCaps2Node() {
+ final var caps =
+ DiscoManager.buildHashFromNode(
+ "urn:xmpp:caps#sha-256.u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=");
+ assertThat(caps, instanceOf(EntityCapabilities2.EntityCaps2Hash.class));
+ }
+
+ @Test
+ public void parseCaps2NodeMissingHash() {
+ final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#sha-256.");
+ assertNull(caps);
+ }
+
+ @Test
+ public void parseCaps2NodeInvalid() {
+ final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#-");
+ assertNull(caps);
+ }
+
+ @Test
+ public void parseCaps2NodeUnknownAlgo() {
+ final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#test.test");
+ assertNull(caps);
+ }
+}