diff --git a/build.gradle b/build.gradle index 6b5c6655aa5dc1d1c7ffddda0f2f24897edf3efb..4de6bf686a763d81bb5992abc78577e23e1a5838 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 66ec318071198811dfcd9387c1f2acd07700ffa0..5f38e1a51c50f17f24bd1a622660f953ece6e593 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -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 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++; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 6508d00a966b88fccc8c66e61095c9c49be3c30f..f13ffa973ad35a5b69bcedeeb4fa14aea134c2ec 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -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(); } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 7ba6f8a23276a8878c87e7735d2c10ff7871dd8b..2d939d153d600958bc5ecb9769132f0859062281 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -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 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 getFeatures() { - return this.serviceDiscoveryResult != null - ? this.serviceDiscoveryResult.features + public Collection getFeatures() { + final var serviceDiscoveryResult = getServiceDiscoveryResult(); + return serviceDiscoveryResult != null + ? serviceDiscoveryResult.getFeatureStrings() : Collections.emptyList(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java deleted file mode 100644 index edafd95dedb79daa3ba310298540651107180b47..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/entities/Presence.java +++ /dev/null @@ -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 { - - 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; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java index 958891d34d4b8915fdc3309a4827c37b41001347..0c36a42cf994bc84f0ff51559c2c0fb4450b7e95 100644 --- a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java +++ b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java @@ -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; } diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index d3bd706f87e42e3263c7693c77a063863468b1b5..98ab79efce3aa9532b9b708abe0c83b041b86377 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -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 presences = new Hashtable<>(); + private final HashMap 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 asTemplates() { synchronized (this.presences) { ArrayList 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 getStatusMessages() { - ArrayList messages = new ArrayList<>(); + public Set getStatusMessages() { + Set 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> toTypeAndNameMap() { Map typeMap = new HashMap<>(); Map nameMap = new HashMap<>(); + final var connection = this.contact.getAccount().getXmppConnection(); + if (connection == null) { + return new Pair<>(typeMap, nameMap); + } synchronized (this.presences) { - for (Map.Entry 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); } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java deleted file mode 100644 index 3a99f6ca28e5b26872119b6ba188c31e1231232e..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ /dev/null @@ -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 features; - protected final List forms; - private final List 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 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 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 getIdentities() { - return this.identities; - } - - public List 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 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 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 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 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 { - 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; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 7bb7341842997906b131073e91a010cad3777b0b..9fca46bd2fca91c8c0d144b0386a8c15ce9888f4 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 244da8e40807e568c07f9427c16e4e8a2708dd91..d3c269f965ee0dfdc6cf864178cb1a78f4a892dc 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -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 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)) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e4787999c275e3fd7e546e70d3e0eccb081fd630..3673e6dcf6585a329b4d66ecfb98d36b469a8f06 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 11580e20fa4442cb5ffc9ec0c0c3ffa7d19d9abc..502b51ef89b55bedd2386954cda5f4530f908ad4 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -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 features) { + private static Version get(final Collection 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 features) { + public static boolean has(final Collection features) { for (String feature : features) { for (Version version : values()) { if (version.namespace.equals(feature)) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ff38d2ed57d57de0d58ef928beba1465e282182e..5ec73cc419c4ecf4064aa1a9bc789f97ea05afe9 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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 FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private final LruCache, 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() { + @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 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 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); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index a21fd5360e77f618d75e1a80f87eb45f1609e57e..039c26e47fc303e7f4fbea9b7b8e676df7b92261 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -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 statusMessages = contact.getPresences().getStatusMessages(); - if (statusMessages.size() == 0) { + Collection 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 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); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 459ef2f0be6248eab923febd321012cefe683722..bf87e21271952bc9f7b145836e91f7895e8ad84b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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( diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 02e3963b966be2843d45b597db96f066c4ec2e06..494e75dd257ed6e04d955b20044067c0c02b280c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -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; } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index a2ec5fe949feecda5381f086e489acc5fda5c969..9adb4e7bec4c8fb430916c548344386cd8c4352f 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -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); } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 290c52079d9a39476b56d63eb1bfb29f5fc72981..577e66b42c930d9a795f1f1e74108edf6e161fc3 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -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 { - 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 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 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 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 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 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 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; + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java index a71300d64cab125e4e97aa6eb560bb3a6d62e9c2..344cfd1b5f50501a34010cbf5d5cd528ce99d4ea 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java +++ b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java @@ -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)); }; } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 9f3aab8bc9ca937c35f530e91ec494e580c6006e..bf59a83e8fa01e7162a91bdae2ebf4caf28019a9 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java b/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java deleted file mode 100644 index cce6fc163c6b88849335d7fe048346c50f3b0ccd..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java +++ /dev/null @@ -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()); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e8826c96aa46558a283b2e81adac42076427c1d8..987ffb1b0330f153d7179084ef2f83851c639774 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -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 disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); private final SparseArray mStanzaQueue = new SparseArray<>(); private final Hashtable>> 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 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() + .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 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 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 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 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 items = new HashSet<>(); - final List 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> findDiscoItemsByFeature(final String feature) { - synchronized (this.disco) { - final List> items = new ArrayList<>(); - for (final Entry cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - items.add(cursor); - } - } - return items; - } + public M getManager(final Class clazz) { + return this.managers.getInstance(clazz); } - public Entry getServiceDiscoveryResultByFeature( - final String feature) { - synchronized (this.disco) { - for (final var cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - return cursor; - } + private List> findDiscoItemsByFeature(final String feature) { + final List> items = new ArrayList<>(); + for (final Entry cursor : + getManager(DiscoManager.class).getServerItems().entrySet()) { + if (cursor.getValue().getFeatureStrings().contains(feature)) { + items.add(cursor); } - return null; } + return items; + } + + public Entry 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 getMucServers() { List servers = new ArrayList<>(); - synchronized (this.disco) { - for (final Entry 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 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 getManager(final Class 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 getAccountFeatures() { - ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid()); - return result == null ? Collections.emptyList() : result.getFeatures(); + public Collection 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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index de325534e3a5c45f9bc6acc4aa0a3b8a985aed82..9a80ecabec409f7a9ccd053eb81b5f05a2b61a3a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -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 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 { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java index 484b6b5c892c7314047e42241c91d391c6648235..49ec84af131b1209148854d49b7322310bfeee6a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java @@ -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 BASIC_RTP_REQUIREMENTS = Arrays.asList( - Namespace.JINGLE, - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS - ); - private static final Collection VIDEO_REQUIREMENTS = Arrays.asList( - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO - ); + private static final List BASIC_RTP_REQUIREMENTS = + Arrays.asList( + Namespace.JINGLE, + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS); + private static final Collection 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 features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures()); + public static Capability check(final InfoQuery infoQuery) { + final Set 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 resources = new ArrayList<>(); - for (final Map.Entry 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 { } } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java new file mode 100644 index 0000000000000000000000000000000000000000..b5c129e36dcb79aacb246ceffaeeadbaf5d0f924 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java @@ -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); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java new file mode 100644 index 0000000000000000000000000000000000000000..e773d617bcd8c0f0dcab8794fc0a94788b7ad2dd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -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 entityInformation = new HashMap<>(); + private final Map> 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 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 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 info( + @NonNull final Entity entity, @Nullable final String node) { + return info(entity, node, null); + } + + public ListenableFuture info( + final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) { + final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node; + final var 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 void checkMatch( + final H expected, final H was, final Class 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> items(final Entity.DiscoItem entity) { + return items(entity, null); + } + + public ListenableFuture> 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> 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> infoFutures = + Collections2.transform( + items, i -> info(Entity.discoItem(i.getJid()), i.getNode())); + return Futures.allAsList(infoFutures); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture> commands(final Entity.DiscoItem entity) { + final var itemsFuture = items(entity, Namespace.COMMANDS); + return Futures.transform( + itemsFuture, + items -> { + final var builder = new ImmutableMap.Builder(); + 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 getServerItems() { + final var builder = new ImmutableMap.Builder(); + 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); + } + } +} diff --git a/src/main/java/im/conversations/android/xml/XmlElementReader.java b/src/main/java/im/conversations/android/xml/XmlElementReader.java new file mode 100644 index 0000000000000000000000000000000000000000..d7c0a0424014066e5f1473a2eac1282c06415a74 --- /dev/null +++ b/src/main/java/im/conversations/android/xml/XmlElementReader.java @@ -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()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Entity.java b/src/main/java/im/conversations/android/xmpp/Entity.java index a578d250780e40c8b5a588a74f1ea43496f2c515..799d7a5eadd99032c3690fe318370540de1497cf 100644 --- a/src/main/java/im/conversations/android/xmpp/Entity.java +++ b/src/main/java/im/conversations/android/xmpp/Entity.java @@ -1,6 +1,6 @@ package im.conversations.android.xmpp; -import org.jxmpp.jid.Jid; +import eu.siacs.conversations.xmpp.Jid; public abstract class Entity { diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java index 1d8a35a68d2d5822c2d0ada756b91e473bb698e9..d891870aa41af45d96daaf6ec8ceaf89d0635689 100644 --- a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -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) { diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index c754ee48de9364084628b26fc596d2645741ead3..7fc03360d3d275ec27c20d7dc0df8b1987c4298a 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -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); } diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index f3f72fab86e7924e107674d02de4707030ebbf13..0c6b96dff7f5c8c37590fe212937560314e99a2d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -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); } diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java index 55f104e25bb547419e1e74ffa077ec563a354ea3..da5bc7f7bc8502371e55521dcf8df66cf816acb0 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java @@ -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 getFeatureStrings() { + return Collections2.filter( + Collections2.transform(getFeatures(), Feature::getVar), Objects::nonNull); + } + + public Collection 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(); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java index 129660b000cd1565956f10703afe828599eb8bf8..0887fde2c649978fec81669efd7ba2673d519e4f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java @@ -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; + }; + } + } } diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..021978dfa354e0edb4e3debbda0c89d7978d267e --- /dev/null +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -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 = + """ + + + + + + + """; + 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 = + """ + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + ipv4 + ipv6 + + + Mac + + + 10.5.1 + + + Psi + + + 0.11 + + + """; + 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 = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Linux + + + 4.14.355-276.618.amzn2.x86_64 amd64 - Java 17.0.14 + + + Openfire + + + 5.0.0 Alpha + + + + + http://jabber.org/network/serverinfo + + + xmpp:dwd@dave.cridland.net + xmpp:akrherz@igniterealtime.org + xmpp:benjamin@igniterealtime.org + mailto:benjamin@holyarmy.org + xmpp:csh@igniterealtime.org + xmpp:dan.caseley@igniterealtime.org + mailto:dan.caseley@surevine.com + xmpp:flow@igniterealtime.org + xmpp:gdt@igniterealtime.org + mailto:greg.d.thomas@gmail.com + xmpp:guus.der.kinderen@igniterealtime.org + mailto:guus.der.kinderen@gmail.com + xmpp:lg@igniterealtime.org + xmpp:rcollier@igniterealtime.org + mailto:robincollier@hotmail.com + + + + +"""; + 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 = + """ + + + + + + + + + + + + + + + + + + + + """; + 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 = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Tkabber + + + 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2) + + + Windows + + + XP + + + """; + 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); + } +}