Detailed changes
@@ -130,6 +130,9 @@ dependencies {
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
testImplementation 'junit:junit:4.13.2'
+ testImplementation 'org.robolectric:robolectric:4.14.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+
}
ext {
@@ -0,0 +1,2 @@
+* compatibilidade con 'Service Outage Status'
+* arranxo de problemas menores de seguridade ao procesar varios corpos da mensaxe, occupant-ids e stanza-id
@@ -0,0 +1 @@
+Chamadas de Áudio/Vídeo (Requer suporte do servidor na forma de servidores STUN e TURN descobertos via XEP-0215)
@@ -0,0 +1,2 @@
+* Feedback audível (discando, chamada iniciada, chamada encerrada) para chamadas de voz.
+* Correção de problema com a tentativa de reintentar chamadas de vídeo falhadas
@@ -0,0 +1 @@
+* Desabilitar a abertura de arquivos de backup (.ceb) pelo gerenciador de arquivos
@@ -0,0 +1 @@
+* Introduzir novo formato de arquivo de backup
@@ -0,0 +1 @@
+* Correções de bugs menores
@@ -0,0 +1 @@
+* Adicionar tempo limite para a iniciação da chamada
@@ -0,0 +1 @@
+* Suporte a Reações de Mensagens
@@ -0,0 +1 @@
+* Melhorar o manuseio de algumas reações com emojis
@@ -0,0 +1 @@
+* Adicionar a capacidade de exibir as bolhas de mensagem alinhadas à esquerda
@@ -1 +1 @@
-* Додано таймаут для ініціювання виклику
+* Додано тайм-аут для ініціювання виклику
@@ -3,4 +3,5 @@
<string name="pick_a_server">Roghnaigh do freastalaí XMPP</string>
<string name="use_conversations.im">Bain úsáid as conversations.im</string>
<string name="create_new_account">Oscail cuntas nua</string>
+ <string name="do_you_have_an_account">An bhfuil cuntas XMPP agat?</string>
</resources>
@@ -46,6 +46,17 @@ import eu.siacs.conversations.xml.Element;
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;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import org.json.JSONException;
+import org.json.JSONObject;
public class Account extends AbstractEntity implements AvatarService.Avatarable {
@@ -111,7 +122,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
private long mEndGracePeriod = 0L;
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
private boolean bookmarksLoaded = false;
- private Presence.Status presenceStatus;
+ private im.conversations.android.xmpp.model.stanza.Presence.Availability presenceStatus;
private String presenceStatusMessage;
private String pinnedMechanism;
private String pinnedChannelBinding;
@@ -134,7 +145,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,
@@ -153,7 +164,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,
@@ -216,7 +227,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)),
@@ -489,11 +500,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;
}
@@ -622,9 +634,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++;
}
}
@@ -679,12 +700,15 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
}
public void refreshCapsFor(Contact contact) {
+ final var connection = getXmppConnection();
+ if (connection == null) return;
+
synchronized (gateways) {
for (final var k : new HashSet<>(gateways.keySet())) {
gateways.remove(k, contact);
}
- for (final var p : contact.getPresences().getPresences()) {
- final var disco = p.getServiceDiscoveryResult();
+ for (final var jid : contact.getPresences().getFullJids()) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
if (disco == null) continue;
for (final var identity : disco.getIdentities()) {
if ("gateway".equals(identity.getCategory())) {
@@ -22,6 +22,17 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.android.AbstractPhoneContact;
+import eu.siacs.conversations.android.JabberIdContact;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.utils.JidHelper;
+import eu.siacs.conversations.utils.UIHelper;
+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;
@@ -73,7 +84,7 @@ public class Contact implements ListItem, Blockable {
private final JSONObject keys;
private JSONArray groups = new JSONArray();
private JSONArray systemTags = new JSONArray();
- private final Presences presences = new Presences();
+ private final Presences presences = new Presences(this);
protected Account account;
protected Avatar avatar;
@@ -231,7 +242,7 @@ public class Contact implements ListItem, Blockable {
for (final String tag : getSystemTags(true)) {
tags.add(new Tag(tag));
}
- Presence.Status status = getShownStatus();
+ final var status = getShownStatus();
if (!showInRoster() && getSystemAccount() != null) {
tags.add(new Tag("Android"));
}
@@ -308,6 +319,7 @@ public class Contact implements ListItem, Blockable {
public void updatePresence(final String resource, final Presence presence) {
this.presences.updatePresence(resource, presence);
+ refreshCaps();
}
public void removePresence(final String resource) {
@@ -321,7 +333,7 @@ public class Contact implements ListItem, Blockable {
refreshCaps();
}
- public Presence.Status getShownStatus() {
+ public im.conversations.android.xmpp.model.stanza.Presence.Availability getShownStatus() {
return this.presences.getShownStatus();
}
@@ -21,11 +21,12 @@ 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 eu.siacs.conversations.xml.Element;
+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;
@@ -52,7 +53,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;
@@ -121,15 +122,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;
@@ -151,11 +161,11 @@ public class MucOptions {
}
private Data getRoomInfoForm() {
- final List<Data> forms =
- serviceDiscoveryResult == null
- ? Collections.emptyList()
- : serviceDiscoveryResult.forms;
- return forms.isEmpty() ? new Data() : forms.get(0);
+ final var serviceDiscoveryResult = getServiceDiscoveryResult();
+ return serviceDiscoveryResult == null
+ ? null
+ : serviceDiscoveryResult.getServiceDiscoveryExtension(
+ "http://jabber.org/protocol/muc#roominfo");
}
public String getAvatar() {
@@ -163,8 +173,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() {
@@ -222,9 +233,10 @@ public class MucOptions {
return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false);
}
- public List<String> getFeatures() {
- return this.serviceDiscoveryResult != null
- ? this.serviceDiscoveryResult.features
+ public Collection<String> getFeatures() {
+ final var serviceDiscoveryResult = getServiceDiscoveryResult();
+ return serviceDiscoveryResult != null
+ ? serviceDiscoveryResult.getFeatureStrings()
: Collections.emptyList();
}
@@ -1,101 +0,0 @@
-package eu.siacs.conversations.entities;
-
-import androidx.annotation.NonNull;
-
-import java.util.Locale;
-
-import eu.siacs.conversations.xml.Element;
-
-public class Presence implements Comparable<Presence> {
-
- public enum Status {
- CHAT, ONLINE, AWAY, XA, DND, OFFLINE;
-
- public String toShowString() {
- switch(this) {
- case CHAT: return "chat";
- case AWAY: return "away";
- case XA: return "xa";
- case DND: return "dnd";
- }
- return null;
- }
-
- public static Status fromShowString(String show) {
- if (show == null) {
- return ONLINE;
- } else {
- switch (show.toLowerCase(Locale.US)) {
- case "away":
- return AWAY;
- case "xa":
- return XA;
- case "dnd":
- return DND;
- case "chat":
- return CHAT;
- default:
- return ONLINE;
- }
- }
- }
- }
-
- private final Status status;
- private ServiceDiscoveryResult disco;
- private final String ver;
- private final String hash;
- private final String node;
- private final String message;
-
- public Presence(Status status, String ver, String hash, String node, String message) {
- this.status = status;
- this.ver = ver;
- this.hash = hash;
- this.node = node;
- this.message = message;
- }
-
- public static Presence parse(String show, Element caps, String message) {
- final String hash = caps == null ? null : caps.getAttribute("hash");
- final String ver = caps == null ? null : caps.getAttribute("ver");
- final String node = caps == null ? null : caps.getAttribute("node");
- return new Presence(Status.fromShowString(show), ver, hash, node, message);
- }
-
- public int compareTo(@NonNull Presence other) {
- return this.status.compareTo(other.status);
- }
-
- public Status getStatus() {
- return this.status;
- }
-
- public boolean hasCaps() {
- return ver != null && hash != null;
- }
-
- public String getVer() {
- return this.ver;
- }
-
- public String getNode() {
- return this.node;
- }
-
- public String getHash() {
- return this.hash;
- }
-
- public String getMessage() {
- return this.message;
- }
-
- public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) {
- this.disco = disco;
- }
-
- public ServiceDiscoveryResult getServiceDiscoveryResult() {
- return disco;
- }
-}
@@ -2,6 +2,7 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Objects;
public class PresenceTemplate extends AbstractEntity {
@@ -13,9 +14,9 @@ public class PresenceTemplate extends AbstractEntity {
private long lastUsed = 0;
private String statusMessage;
- private Presence.Status status = Presence.Status.ONLINE;
+ private Presence.Availability status = Presence.Availability.ONLINE;
- public PresenceTemplate(Presence.Status status, String statusMessage) {
+ public PresenceTemplate(Presence.Availability status, String statusMessage) {
this.status = status;
this.statusMessage = statusMessage;
this.lastUsed = System.currentTimeMillis();
@@ -41,11 +42,11 @@ public class PresenceTemplate extends AbstractEntity {
template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
template.status =
- Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
+ Presence.Availability.valueOfShown(cursor.getString(cursor.getColumnIndex(STATUS)));
return template;
}
- public Presence.Status getStatus() {
+ public Presence.Availability getStatus() {
return status;
}
@@ -1,15 +1,26 @@
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.Jid;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.disco.info.Identity;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.Hashtable;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class Presences {
- private final Hashtable<String, Presence> presences = new Hashtable<>();
+ private final HashMap<String, Presence> presences = new HashMap<>();
+ private final Contact contact;
+
+ public Presences(final Contact contact) {
+ this.contact = contact;
+ }
private static String nameWithoutVersion(String name) {
String[] parts = name.split(" ");
@@ -63,18 +74,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 +112,12 @@ public class Presences {
public List<PresenceTemplate> asTemplates() {
synchronized (this.presences) {
ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
- for (Presence p : presences.values()) {
- if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
- templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
+ for (Presence presence : this.presences.values()) {
+ String message = Strings.nullToEmpty(presence.getStatus()).trim();
+ if (Strings.isNullOrEmpty(message)) {
+ continue;
}
+ templates.add(new PresenceTemplate(presence.getAvailability(), message));
}
return templates;
}
@@ -115,24 +129,46 @@ public class Presences {
}
}
- public List<String> getStatusMessages() {
- ArrayList<String> messages = new ArrayList<>();
+ public Set<String> getStatusMessages() {
+ Set<String> messages = new HashSet<>();
synchronized (this.presences) {
for (Presence presence : this.presences.values()) {
- String message = presence.getMessage() == null ? null : presence.getMessage().trim();
- if (message != null && !message.isEmpty() && !messages.contains(message)) {
- messages.add(message);
+ String message = Strings.nullToEmpty(presence.getStatus()).trim();
+ if (Strings.isNullOrEmpty(message)) {
+ continue;
}
+ messages.add(message);
}
}
return messages;
}
+ public Set<Jid> getFullJids() {
+ final Set<Jid> jids = new HashSet<>();
+ synchronized (this.presences) {
+ for (var resource : this.presences.keySet()) {
+ final var jid = Strings.isNullOrEmpty(resource) ? contact.getJid().asBareJid() : contact.getJid().withResource(resource);
+ jids.add(jid);
+ }
+ }
+ return jids;
+ }
+
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;
}
}
@@ -141,45 +177,46 @@ public class Presences {
}
public boolean anySupport(final String namespace) {
- synchronized (this.presences) {
- if (this.presences.size() == 0) {
+ final var connection = this.contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return false;
+ }
+ final var jids = getFullJids();
+ if (jids.size() == 0) {
+ return true;
+ }
+ for (final var jid : jids) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
+ if (disco != null && disco.getFeatures().contains(namespace)) {
return true;
}
- for (Presence presence : this.presences.values()) {
- ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
- if (disco != null && disco.getFeatures().contains(namespace)) {
- return true;
- }
- }
}
return false;
}
public String firstWhichSupport(final String namespace) {
- synchronized (this.presences) {
- for (Map.Entry<String, Presence> entry : this.presences.entrySet()) {
- String resource = entry.getKey();
- Presence presence = entry.getValue();
- ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
- if (disco != null && disco.getFeatures().contains(namespace)) {
- return resource;
- }
+ final var connection = this.contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return null;
+ }
+ for (final var jid : getFullJids()) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
+ if (disco != null && disco.getFeatures().contains(namespace)) {
+ return jid.getResource();
}
}
return null;
}
public boolean anyIdentity(final String category, final String type) {
- synchronized (this.presences) {
- if (this.presences.size() == 0) {
- // https://github.com/iNPUTmice/Conversations/issues/4230
- return false;
- }
- for (Presence presence : this.presences.values()) {
- ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
- if (disco != null && disco.hasIdentity(category, type)) {
- return true;
- }
+ final var connection = this.contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return false;
+ }
+ for (final var jid : getFullJids()) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
+ if (disco != null && disco.hasIdentityWithCategoryAndType(category, type)) {
+ return true;
}
}
return false;
@@ -188,15 +225,25 @@ public class Presences {
public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
Map<String, String> typeMap = new HashMap<>();
Map<String, String> nameMap = new HashMap<>();
+ final var connection = this.contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return new Pair<>(typeMap, nameMap);
+ }
synchronized (this.presences) {
- for (Map.Entry<String, Presence> presenceEntry : this.presences.entrySet()) {
- String resource = presenceEntry.getKey();
- Presence presence = presenceEntry.getValue();
- ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
- if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
- ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
+ for (final String resource : this.presences.keySet()) {
+ final var serviceDiscoveryResult =
+ connection
+ .getManager(DiscoManager.class)
+ .get(
+ Strings.isNullOrEmpty(resource)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(resource));
+ if (serviceDiscoveryResult != null
+ && !serviceDiscoveryResult.getIdentities().isEmpty()) {
+ final Identity identity =
+ Iterables.getFirst(serviceDiscoveryResult.getIdentities(), null);
String type = identity.getType();
- String name = identity.getName();
+ String name = identity.getIdentityName();
if (type != null) {
typeMap.put(resource, type);
}
@@ -1,353 +0,0 @@
-package eu.siacs.conversations.entities;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.util.Base64;
-import com.google.common.base.Strings;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.forms.Data;
-import eu.siacs.conversations.xmpp.forms.Field;
-import im.conversations.android.xmpp.model.stanza.Iq;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-public class ServiceDiscoveryResult {
- public static final String TABLENAME = "discovery_results";
- public static final String HASH = "hash";
- public static final String VER = "ver";
- public static final String RESULT = "result";
- protected final String hash;
- protected final byte[] ver;
- protected final List<String> features;
- protected final List<Data> forms;
- private final List<Identity> identities;
-
- public ServiceDiscoveryResult(final Iq packet) {
- this.identities = new ArrayList<>();
- this.features = new ArrayList<>();
- this.forms = new ArrayList<>();
- this.hash = "sha-1"; // We only support sha-1 for now
-
- final List<Element> elements = packet.query().getChildren();
-
- for (final Element element : elements) {
- if (element.getName().equals("identity")) {
- Identity id = new Identity(element);
- if (id.getType() != null && id.getCategory() != null) {
- identities.add(id);
- }
- } else if (element.getName().equals("feature")) {
- if (element.getAttribute("var") != null) {
- features.add(element.getAttribute("var"));
- }
- } else if (element.getName().equals("x")
- && element.getAttribute("xmlns").equals(Namespace.DATA)) {
- forms.add(Data.parse(element));
- }
- }
- this.ver = this.mkCapHash();
- }
-
- private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
- this.identities = new ArrayList<>();
- this.features = new ArrayList<>();
- this.forms = new ArrayList<>();
- this.hash = hash;
- this.ver = ver;
-
- JSONArray identities = o.optJSONArray("identities");
- if (identities != null) {
- for (int i = 0; i < identities.length(); i++) {
- this.identities.add(new Identity(identities.getJSONObject(i)));
- }
- }
- JSONArray features = o.optJSONArray("features");
- if (features != null) {
- for (int i = 0; i < features.length(); i++) {
- this.features.add(features.getString(i));
- }
- }
- JSONArray forms = o.optJSONArray("forms");
- if (forms != null) {
- for (int i = 0; i < forms.length(); i++) {
- this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
- }
- }
- }
-
- private ServiceDiscoveryResult() {
- this.hash = "sha-1";
- this.features = Collections.emptyList();
- this.identities = Collections.emptyList();
- this.ver = null;
- this.forms = Collections.emptyList();
- }
-
- public static ServiceDiscoveryResult empty() {
- return new ServiceDiscoveryResult();
- }
-
- public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
- this(
- cursor.getString(cursor.getColumnIndexOrThrow(HASH)),
- Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT),
- new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT))));
- }
-
- private static String clean(String s) {
- return s.replace("<", "<");
- }
-
- private static String blankNull(String s) {
- return s == null ? "" : clean(s);
- }
-
- private static Data createFormFromJSONObject(JSONObject o) {
- Data data = new Data();
- JSONArray names = o.names();
- for (int i = 0; i < names.length(); ++i) {
- try {
- String name = names.getString(i);
- JSONArray jsonValues = o.getJSONArray(name);
- ArrayList<String> values = new ArrayList<>(jsonValues.length());
- for (int j = 0; j < jsonValues.length(); ++j) {
- values.add(jsonValues.getString(j));
- }
- data.put(name, values);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- return data;
- }
-
- private static JSONObject createJSONFromForm(Data data) {
- JSONObject object = new JSONObject();
- for (Field field : data.getFields()) {
- try {
- JSONArray jsonValues = new JSONArray();
- for (String value : field.getValues()) {
- jsonValues.put(value);
- }
- object.put(field.getFieldName(), jsonValues);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- try {
- JSONArray jsonValues = new JSONArray();
- jsonValues.put(data.getFormType());
- object.put(Data.FORM_TYPE, jsonValues);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return object;
- }
-
- public Identity getIdentity(String category, String type) {
- for (Identity id : this.getIdentities()) {
- if ((category == null || id.getCategory().equals(category)) &&
- (type == null || id.getType().equals(type))) {
- return id;
- }
- }
-
- return null;
- }
-
- public boolean hasIdentity(String category, String type) {
- return getIdentity(category, type) != null;
- }
-
- public String getVer() {
- return Base64.encodeToString(this.ver, Base64.NO_WRAP);
- }
-
- public List<Identity> getIdentities() {
- return this.identities;
- }
-
- public List<String> getFeatures() {
- return this.features;
- }
-
- public String getExtendedDiscoInformation(final String formType, final String name) {
- for (final Data form : this.forms) {
- if (formType.equals(form.getFormType())) {
- for (final Field field : form.getFields()) {
- if (name.equals(field.getFieldName())) {
- return field.getValue();
- }
- }
- }
- }
- return null;
- }
-
- private byte[] mkCapHash() {
- StringBuilder s = new StringBuilder();
-
- List<Identity> identities = this.getIdentities();
- Collections.sort(identities);
-
- for (Identity id : identities) {
- s.append(blankNull(id.getCategory()))
- .append("/")
- .append(blankNull(id.getType()))
- .append("/")
- .append(blankNull(id.getLang()))
- .append("/")
- .append(blankNull(id.getName()))
- .append("<");
- }
-
- final List<String> features = this.getFeatures();
- Collections.sort(features);
- for (final String feature : features) {
- s.append(clean(feature)).append("<");
- }
-
- Collections.sort(forms, Comparator.comparing(Data::getFormType));
- for (final Data form : forms) {
- s.append(clean(form.getFormType())).append("<");
- final List<Field> fields = form.getFields();
- Collections.sort(
- fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName())));
- for (final Field field : fields) {
- s.append(Strings.nullToEmpty(field.getFieldName())).append("<");
- final List<String> values = field.getValues();
- Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull));
- for (final String value : values) {
- s.append(blankNull(value)).append("<");
- }
- }
- }
-
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- return null;
- }
-
- return md.digest(s.toString().getBytes(StandardCharsets.UTF_8));
- }
-
- private JSONObject toJSON() {
- try {
- JSONObject o = new JSONObject();
-
- JSONArray ids = new JSONArray();
- for (Identity id : this.getIdentities()) {
- ids.put(id.toJSON());
- }
- o.put("identities", ids);
-
- o.put("features", new JSONArray(this.getFeatures()));
-
- JSONArray forms = new JSONArray();
- for (Data data : this.forms) {
- forms.put(createJSONFromForm(data));
- }
- o.put("forms", forms);
-
- return o;
- } catch (JSONException e) {
- return null;
- }
- }
-
- public ContentValues getContentValues() {
- final ContentValues values = new ContentValues();
- values.put(HASH, this.hash);
- values.put(VER, getVer());
- JSONObject jsonObject = toJSON();
- values.put(RESULT, jsonObject == null ? "" : jsonObject.toString());
- return values;
- }
-
- public static class Identity implements Comparable<Identity> {
- protected final String type;
- protected final String lang;
- protected final String name;
- final String category;
-
- Identity(final String category, final String type, final String lang, final String name) {
- this.category = category;
- this.type = type;
- this.lang = lang;
- this.name = name;
- }
-
- Identity(final Element el) {
- this(
- el.getAttribute("category"),
- el.getAttribute("type"),
- el.getAttribute("xml:lang"),
- el.getAttribute("name"));
- }
-
- Identity(final JSONObject o) {
-
- this(
- o.optString("category", null),
- o.optString("type", null),
- o.optString("lang", null),
- o.optString("name", null));
- }
-
- public String getCategory() {
- return this.category;
- }
-
- public String getType() {
- return this.type;
- }
-
- public String getLang() {
- return this.lang;
- }
-
- public String getName() {
- return this.name;
- }
-
- JSONObject toJSON() {
- try {
- JSONObject o = new JSONObject();
- o.put("category", this.getCategory());
- o.put("type", this.getType());
- o.put("lang", this.getLang());
- o.put("name", this.getName());
- return o;
- } catch (JSONException e) {
- return null;
- }
- }
-
- @Override
- public int compareTo(final Identity o) {
- int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
- if (r == 0) {
- r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
- }
- if (r == 0) {
- r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
- }
- if (r == 0) {
- r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
- }
-
- return r;
- }
- }
-}
@@ -1,14 +1,13 @@
package eu.siacs.conversations.generator;
import android.text.TextUtils;
-
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
+import im.conversations.android.xmpp.model.stanza.Presence;
public class PresenceGenerator extends AbstractGenerator {
@@ -16,20 +15,25 @@ public class PresenceGenerator extends AbstractGenerator {
super(service);
}
- private im.conversations.android.xmpp.model.stanza.Presence subscription(String type, Contact contact) {
- im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
+ private im.conversations.android.xmpp.model.stanza.Presence subscription(
+ String type, Contact contact) {
+ im.conversations.android.xmpp.model.stanza.Presence packet =
+ new im.conversations.android.xmpp.model.stanza.Presence();
packet.setAttribute("type", type);
packet.setTo(contact.getJid());
packet.setFrom(contact.getAccount().getJid().asBareJid());
return packet;
}
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) {
+ public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
+ final Contact contact) {
return requestPresenceUpdatesFrom(contact, null);
}
- public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact, final String preAuth) {
- im.conversations.android.xmpp.model.stanza.Presence packet = subscription("subscribe", contact);
+ public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(
+ final Contact contact, final String preAuth) {
+ im.conversations.android.xmpp.model.stanza.Presence packet =
+ subscription("subscribe", contact);
String displayName = contact.getAccount().getDisplayName();
if (!TextUtils.isEmpty(displayName)) {
packet.addChild("nick", Namespace.NICK).setContent(displayName);
@@ -40,33 +44,33 @@ 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, null);
}
- public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Status status, final boolean personal, final String nickname) {
- 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 String nickname) {
+ 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);
}
@@ -77,8 +81,7 @@ public class PresenceGenerator extends AbstractGenerator {
}
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", "https://cheogram.com");
cap.setAttribute("ver", capHash);
@@ -87,15 +90,18 @@ public class PresenceGenerator extends AbstractGenerator {
}
public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) {
- im.conversations.android.xmpp.model.stanza.Presence presence = new im.conversations.android.xmpp.model.stanza.Presence();
+ im.conversations.android.xmpp.model.stanza.Presence presence =
+ new im.conversations.android.xmpp.model.stanza.Presence();
presence.setTo(mucOptions.getSelf().getFullJid());
presence.setFrom(mucOptions.getAccount().getJid());
presence.setAttribute("type", "unavailable");
return presence;
}
- public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(Account account) {
- im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence();
+ public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(
+ Account account) {
+ im.conversations.android.xmpp.model.stanza.Presence packet =
+ new im.conversations.android.xmpp.model.stanza.Presence();
packet.setFrom(account.getJid());
packet.setAttribute("type", "unavailable");
return packet;
@@ -1,7 +1,12 @@
package eu.siacs.conversations.parser;
import android.util.Log;
+import androidx.annotation.NonNull;
import com.google.common.base.Strings;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -10,7 +15,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -18,10 +22,13 @@ import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
import eu.siacs.conversations.xmpp.pep.Avatar;
+import im.conversations.android.xmpp.Entity;
import im.conversations.android.xmpp.model.occupant.OccupantId;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.openintents.openpgp.util.OpenPgpUtils;
@@ -365,13 +372,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);
@@ -418,7 +429,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);
@@ -459,6 +471,24 @@ public class PresenceParser extends AbstractParser
mXmppConnectionService.updateRosterUi(XmppConnectionService.UpdateRosterReason.PRESENCE, contact);
}
+ private static void logDiscoFailure(final Jid from, ListenableFuture<Void> discoFuture) {
+ Futures.addCallback(
+ discoFuture,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {}
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ if (throwable instanceof TimeoutException) {
+ return;
+ }
+ Log.d(Config.LOGTAG, "could not retrieve disco from " + from, throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+ }
+
@Override
public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) {
if (packet.hasChild("x", Namespace.MUC_USER)) {
@@ -61,7 +61,6 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
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;
@@ -71,9 +70,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;
@@ -86,7 +90,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;
@@ -101,11 +104,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
+ "("
@@ -148,22 +151,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
@@ -292,6 +279,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 =
@@ -693,8 +688,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);
@@ -705,6 +699,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
@@ -725,7 +722,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) {
@@ -925,10 +922,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");
@@ -943,10 +936,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);
}
@@ -1284,6 +1273,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) {
@@ -1564,40 +1559,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();
@@ -2028,6 +1989,60 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return message;
}
+ public void insertCapsCache(
+ EntityCapabilities.EntityCapsHash caps,
+ EntityCapabilities2.EntityCaps2Hash caps2,
+ InfoQuery infoQuery) {
+ final var contentValues = new ContentValues();
+ contentValues.put("caps", caps.encoded());
+ contentValues.put("caps2", caps2.encoded());
+ contentValues.put("disco_info", infoQuery.toString());
+ getWritableDatabase()
+ .insertWithOnConflict(
+ "caps_cache", null, contentValues, SQLiteDatabase.CONFLICT_REPLACE);
+ }
+
+ public InfoQuery getInfoQuery(final EntityCapabilities.Hash hash) {
+ final String selection;
+ final String[] args;
+ if (hash instanceof EntityCapabilities.EntityCapsHash) {
+ selection = "caps=?";
+ args = new String[] {hash.encoded()};
+ } else if (hash instanceof EntityCapabilities2.EntityCaps2Hash) {
+ selection = "caps2=?";
+ args = new String[] {hash.encoded()};
+ } else {
+ return null;
+ }
+ try (final Cursor cursor =
+ getReadableDatabase()
+ .query(
+ "caps_cache",
+ new String[] {"disco_info"},
+ selection,
+ args,
+ null,
+ null,
+ null)) {
+ if (cursor.moveToFirst()) {
+ final var cached = cursor.getString(0);
+ try {
+ final var element =
+ XmlElementReader.read(cached.getBytes(StandardCharsets.UTF_8));
+ if (element instanceof InfoQuery infoQuery) {
+ return infoQuery;
+ }
+ } catch (final IOException e) {
+ Log.e(Config.LOGTAG, "could not restore info query from cache", e);
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+
public static class FilePath {
public final UUID uuid;
public final String path;
@@ -19,6 +19,7 @@ import im.conversations.android.xmpp.model.stanza.Iq;
import im.conversations.android.xmpp.model.stanza.Message;
import java.math.BigInteger;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@@ -55,7 +56,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
- private static Version get(List<String> features) {
+ private static Version get(final Collection<String> features) {
final Version[] values = values();
for (int i = values.length - 1; i >= 0; --i) {
for (String feature : features) {
@@ -67,7 +68,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
return MAM_0;
}
- public static boolean has(List<String> features) {
+ public static boolean has(final Collection<String> features) {
for (String feature : features) {
for (Version version : values()) {
if (version.namespace.equals(feature)) {
@@ -139,11 +139,8 @@ import eu.siacs.conversations.entities.DownloadableFile;
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;
@@ -189,6 +186,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.OnGatewayResult;
@@ -205,13 +203,17 @@ 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;
+import im.conversations.android.xmpp.model.stanza.Presence;
import im.conversations.android.xmpp.model.storage.PrivateStorage;
import java.io.File;
import java.security.Security;
@@ -436,8 +438,6 @@ public class XmppConnectionService extends Service {
public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
private final AtomicLong mLastExpiryRun = new AtomicLong(0);
- private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache =
- new LruCache<>(20);
private final OnStatusChanged statusListener =
new OnStatusChanged() {
@@ -1548,13 +1548,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;
}
}
@@ -4286,7 +4286,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,
mucOptions.getSelf().getNick());
@@ -4664,7 +4665,7 @@ public class XmppConnectionService extends Service {
if (options.online()) {
Account account = conversation.getAccount();
final Jid joinJid = options.getSelf().getFullJid();
- final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
+ final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), options.getSelf().getNick());
packet.setTo(joinJid);
sendPresencePacket(account, packet);
}
@@ -4688,7 +4689,7 @@ public class XmppConnectionService extends Service {
@Override
public void onSuccess() {
- final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
+ final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), nick);
packet.setTo(joinJid);
sendPresencePacket(account, packet);
callback.success(conversation);
@@ -4702,7 +4703,9 @@ public class XmppConnectionService extends Service {
final var packet =
mPresenceGenerator.selfPresence(
- account, Presence.Status.ONLINE, options.nonanonymous(), nick);
+ account,
+ im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
+ options.nonanonymous(), nick);
packet.setTo(joinJid);
sendPresencePacket(account, packet);
if (nick.equals(MucOptions.defaultNick(account))
@@ -4759,7 +4762,9 @@ public class XmppConnectionService extends Service {
account.getJid().asBareJid(), joinJid, current));
final var packet =
mPresenceGenerator.selfPresence(
- account, Presence.Status.ONLINE, options.nonanonymous(), proposed);
+ account,
+ im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
+ options.nonanonymous(), proposed);
packet.setTo(joinJid);
sendPresencePacket(account, packet);
}
@@ -4954,10 +4959,10 @@ public class XmppConnectionService extends Service {
final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
sendIqPacket(account, request, (reply) -> {
- final var result = new ServiceDiscoveryResult(reply);
+ final var result = reply.getExtension(InfoQuery.class);
cb.accept(
- result.getFeatures().contains("http://jabber.org/protocol/muc") &&
- result.hasIdentity("conference", null)
+ result.hasFeature("http://jabber.org/protocol/muc") &&
+ result.hasIdentityWithCategory("conference")
);
});
}
@@ -4970,11 +4975,19 @@ public class XmppConnectionService extends Service {
final Conversation conversation, final OnConferenceConfigurationFetched callback) {
final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
final var account = conversation.getAccount();
- sendIqPacket(
- account,
- request,
- response -> {
- if (response.getType() == Iq.Type.RESULT) {
+ final var connection = account.getXmppConnection();
+ if (connection == null) {
+ return;
+ }
+ final var future =
+ connection
+ .getManager(DiscoManager.class)
+ .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
+ Futures.addCallback(
+ future,
+ new FutureCallback<InfoQuery>() {
+ @Override
+ public void onSuccess(InfoQuery result) {
final MucOptions mucOptions = conversation.getMucOptions();
final Bookmark bookmark = conversation.getBookmark();
final boolean sameBefore =
@@ -4983,7 +4996,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()
@@ -5005,7 +5018,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(), mucOptions.getSelf().getNick());
packet.setTo(me);
sendPresencePacket(account, packet);
@@ -5024,18 +5038,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(
@@ -6699,7 +6722,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 {
@@ -6938,20 +6961,6 @@ public class XmppConnectionService extends Service {
});
}
- public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
- ServiceDiscoveryResult result = discoCache.get(key);
- if (result != null) {
- return result;
- } else {
- if (key.first == null || key.second == null) return null;
- result = databaseBackend.findDiscoveryResult(key.first, key.second);
- if (result != null) {
- discoCache.put(key, result);
- }
- return result;
- }
- }
-
public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
request.setTo(jid);
@@ -6970,125 +6979,11 @@ public class XmppConnectionService extends Service {
});
}
- public void fetchCaps(Account account, final Jid jid, final Presence presence) {
- fetchCaps(account, jid, presence, null);
- }
-
- public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
- final Pair<String, String> key = presence == null ? null : new Pair<>(presence.getHash(), presence.getVer());
- final ServiceDiscoveryResult disco = key == null ? null : getCachedServiceDiscoveryResult(key);
-
- if (disco != null) {
- presence.setServiceDiscoveryResult(disco);
- final Contact contact = account.getRoster().getContact(jid);
- if (contact.refreshRtpCapability()) {
- syncRoster(account);
- }
- contact.refreshCaps();
- if (disco.hasIdentity("gateway", "pstn")) {
- contact.registerAsPhoneAccount(this);
- mQuickConversationsService.considerSyncBackground(false);
- }
- updateConversationUi(true);
- } else {
- final Iq request = new Iq(Iq.Type.GET);
- request.setTo(jid);
- final String node = presence == null ? null : presence.getNode();
- final String ver = presence == null ? null : 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 == null ? null : key.second)
- + " to "
- + jid);
- sendIqPacket(
- account,
- request,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final ServiceDiscoveryResult discoveryResult =
- new ServiceDiscoveryResult(response);
- if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
- databaseBackend.insertDiscoveryResult(discoveryResult);
- injectServiceDiscoveryResult(
- account.getRoster(),
- presence == null ? null : presence.getHash(),
- presence == null ? null : presence.getVer(),
- jid.getResource(),
- discoveryResult);
- if (discoveryResult.hasIdentity("gateway", "pstn")) {
- final Contact contact = account.getRoster().getContact(jid);
- contact.registerAsPhoneAccount(this);
- mQuickConversationsService.considerSyncBackground(false);
- }
- updateConversationUi(true);
- if (cb != null) cb.run();
- } 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);
- }
- });
- }
- }
-
public void fetchCommands(Account account, final Jid jid, Consumer<Iq> callback) {
final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
sendIqPacket(account, request, callback);
}
- private void injectServiceDiscoveryResult(
- Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
- boolean rosterNeedsSync = false;
- for (final Contact contact : roster.getContacts()) {
- boolean serviceDiscoverySet = false;
- Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
- if (onePresence != null) {
- onePresence.setServiceDiscoveryResult(disco);
- serviceDiscoverySet = true;
- } else if (resource == null && hash == null && ver == null) {
- Presence p = new Presence(Presence.Status.OFFLINE, null, null, null, "");
- p.setServiceDiscoveryResult(disco);
- contact.updatePresence("", p);
- serviceDiscoverySet = true;
- }
- if (hash != null && ver != null) {
- 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();
- contact.refreshCaps();
- }
- }
- 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);
@@ -42,7 +42,9 @@ import com.cheogram.android.Util;
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 org.openintents.openpgp.util.OpenPgpUtils;
@@ -67,7 +69,6 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
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;
@@ -97,6 +98,8 @@ 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 eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -516,11 +519,11 @@ public class ContactDetailsActivity extends OmemoActivity
binding.detailsSendPresence.setOnCheckedChangeListener(null);
binding.detailsReceivePresence.setOnCheckedChangeListener(null);
- List<String> statusMessages = contact.getPresences().getStatusMessages();
- if (statusMessages.size() == 0) {
+ Collection<String> statusMessages = contact.getPresences().getStatusMessages();
+ if (statusMessages.isEmpty()) {
binding.statusMessage.setVisibility(View.GONE);
} else if (statusMessages.size() == 1) {
- final String message = statusMessages.get(0);
+ final String message = Iterables.getOnlyElement(statusMessages);
binding.statusMessage.setVisibility(View.VISIBLE);
final Spannable span = new SpannableString(message);
if (Emoticons.isOnlyEmoji(message)) {
@@ -532,16 +535,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)) {
@@ -684,7 +678,7 @@ public class ContactDetailsActivity extends OmemoActivity
final List<ListItem.Tag> tagList = contact.getTags(this);
final boolean hasMetaTags =
- contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
+ contact.isBlocked() || contact.getShownStatus() != Presence.Availability.OFFLINE;
if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
binding.tags.setVisibility(View.GONE);
} else {
@@ -720,8 +714,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);
@@ -731,9 +725,10 @@ public class ContactDetailsActivity extends OmemoActivity
binding.tags.addView(tv);
}
}
- if (contact.getJid().isDomainJid()) {
- for (final var p : contact.getPresences().getPresences()) {
- final var disco = p.getServiceDiscoveryResult();
+ final var connection = contact.getAccount().getXmppConnection();
+ if (contact.getJid().isDomainJid() && connection != null) {
+ for (final var jid : contact.getPresences().getFullJids()) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
if (disco == null) continue;
for (final var identity : disco.getIdentities()) {
final var txt = identity.getCategory() + "/" + identity.getType();
@@ -102,6 +102,9 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.io.Files;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.MoreExecutors;
import com.otaliastudios.autocomplete.Autocomplete;
import com.otaliastudios.autocomplete.AutocompleteCallback;
@@ -149,7 +152,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.Presences;
import eu.siacs.conversations.entities.ReadByMarker;
import eu.siacs.conversations.entities.Transferable;
@@ -204,10 +206,24 @@ import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
import eu.siacs.conversations.xmpp.jingle.RtpCapability;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
import im.conversations.android.xmpp.model.stanza.Iq;
import org.jetbrains.annotations.NotNull;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
public class ConversationFragment extends XmppFragment
implements EditMessage.KeyboardListener,
@@ -2342,21 +2358,32 @@ public class ConversationFragment extends XmppFragment
}
private void refreshFeatureDiscovery() {
- Set<Map.Entry<String, Presence>> presences = conversation.getContact().getPresences().getPresencesMap().entrySet();
- if (presences.isEmpty()) {
- presences = new HashSet<>();
- presences.add(new AbstractMap.SimpleEntry("", null));
- }
- for (Map.Entry<String, Presence> entry : presences) {
- Jid jid = conversation.getContact().getJid();
- if (!entry.getKey().equals("")) jid = jid.withResource(entry.getKey());
- activity.xmppConnectionService.fetchCaps(conversation.getAccount(), jid, entry.getValue(), () -> {
- if (activity == null) return;
- activity.runOnUiThread(() -> {
- refresh();
- refreshCommands(true);
- });
- });
+ final var connection = conversation.getContact().getAccount().getXmppConnection();
+ if (connection == null) return;
+
+ var jids = conversation.getContact().getPresences().getFullJids();
+ if (jids.isEmpty()) {
+ jids = new HashSet<>();
+ jids.add(conversation.getContact().getJid());
+ }
+ for (final var jid : jids) {
+ Futures.addCallback(
+ connection.getManager(DiscoManager.class).info(Entity.presence(jid), null, null),
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(InfoQuery disco) {
+ if (activity == null) return;
+ activity.runOnUiThread(() -> {
+ refresh();
+ refreshCommands(true);
+ });
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {}
+ },
+ MoreExecutors.directExecutor()
+ );
}
}
@@ -4100,7 +4127,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;
@@ -4113,17 +4140,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.setIconTint(ColorStateList.valueOf(SendButtonTool.getSendButtonColor(this.binding.textSendButton, status)));
@@ -68,7 +68,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;
@@ -102,6 +101,11 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
+import im.conversations.android.xmpp.model.stanza.Presence;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
import okhttp3.HttpUrl;
import okhttp3.HttpUrl;
import org.openintents.openpgp.util.OpenPgpUtils;
@@ -438,7 +442,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;
@@ -458,15 +462,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;
}
}
@@ -43,14 +43,14 @@ import eu.siacs.conversations.databinding.DialogEnterJidBinding;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
import eu.siacs.conversations.ui.util.DelayedHintHelper;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.OnGatewayResult;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
public class EnterJidDialog extends DialogFragment implements OnBackendConnected, TextWatcher {
@@ -305,7 +305,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
});
};
- Pair<String,Pair<Jid,Presence>> p = gatewayListAdapter.getSelected();
+ final var p = gatewayListAdapter.getSelected();
final String type = gatewayListAdapter.getSelectedType();
// Resolve based on local settings before submission
@@ -320,7 +320,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
} else if (p.first != null) { // Gateway already responsed to jabber:iq:gateway once
final Account acct = ((XmppActivity) getActivity()).xmppConnectionService.findAccountByJid(accountJid);
((XmppActivity) getActivity()).xmppConnectionService.fetchFromGateway(acct, p.second.first, binding.jid.getText().toString().trim(), finish);
- } else if (p.second.first.isDomainJid() && p.second.second.getServiceDiscoveryResult().getFeatures().contains("jid\\20escaping")) {
+ } else if (p.second.first.isDomainJid() && p.second.second.hasFeature("jid\\20escaping")) {
finish.onGatewayResult(Jid.ofLocalAndDomain(binding.jid.getText().toString().trim(), p.second.first.getDomain().toString()).toString(), null);
} else if (p.second.first.isDomainJid()) {
finish.onGatewayResult(Jid.ofLocalAndDomain(binding.jid.getText().toString().trim().replace("@", "%"), p.second.first.getDomain().toString()).toString(), null);
@@ -537,9 +537,13 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
public List<String> getTypes(Contact gateway) {
List<String> types = new ArrayList<>();
- for(Presence p : gateway.getPresences().getPresences()) {
- if(p.getServiceDiscoveryResult() != null) {
- for (ServiceDiscoveryResult.Identity id : p.getServiceDiscoveryResult().getIdentities()) {
+ final var connection = gateway.getAccount().getXmppConnection();
+ if (connection == null) return types;
+
+ for(final var jid : gateway.getPresences().getFullJids()) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
+ if(disco != null) {
+ for (final var id : disco.getIdentities()) {
if ("gateway".equals(id.getCategory())) types.add(id.getType());
}
}
@@ -552,31 +556,25 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected
return getType(selected);
}
- public Pair<String, Pair<Jid,Presence>> getSelected() {
+ public Pair<String, Pair<Jid, InfoQuery>> getSelected() {
if(this.selected == 0) {
return null; // No gateway, just use direct JID entry
}
Pair<Contact,String> gateway = this.gateways.get(this.selected - 1);
-
- Pair<Jid,Presence> presence = null;
- for (Map.Entry<String,Presence> e : gateway.first.getPresences().getPresencesMap().entrySet()) {
- Presence p = e.getValue();
- if (p.getServiceDiscoveryResult() != null) {
- if (p.getServiceDiscoveryResult().getFeatures().contains("jabber:iq:gateway")) {
- if (e.getKey().equals("")) {
- presence = new Pair<>(gateway.first.getJid(), p);
- } else {
- presence = new Pair<>(gateway.first.getJid().withResource(e.getKey()), p);
- }
+ final var connection = gateway.first.getAccount().getXmppConnection();
+ if (connection == null) return null;
+
+ Pair<Jid,InfoQuery> presence = null;
+ for (final var jid : gateway.first.getPresences().getFullJids()) {
+ final var disco = connection.getManager(DiscoManager.class).get(jid);
+ if (disco != null) {
+ if (disco.hasFeature("jabber:iq:gateway")) {
+ presence = new Pair<>(jid, disco);
break;
}
- if (p.getServiceDiscoveryResult().hasIdentity("gateway", null)) {
- if (e.getKey().equals("")) {
- presence = new Pair<>(gateway.first.getJid(), p);
- } else {
- presence = new Pair<>(gateway.first.getJid().withResource(e.getKey()), p);
- }
+ if (disco.hasIdentityWithCategoryAndType("gateway", null)) {
+ presence = new Pair<>(jid, disco);
}
}
}
@@ -75,7 +75,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;
@@ -97,6 +96,7 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.forms.Data;
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -1332,13 +1332,15 @@ public class StartConversationActivity extends XmppActivity
}
boolean foundSopranica = false;
for (final Account account : accounts) {
+ if (!account.isEnabled()) continue;
+
for (Contact contact : account.getRoster().getContacts()) {
- Presence.Status s = contact.getShownStatus();
+ final var 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);
tags.addAll(contact.getTags(this));
}
@@ -18,170 +18,164 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.databinding.DataBindingUtil;
-
import com.google.android.material.color.MaterialColors;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
-
import eu.siacs.conversations.AppSettings;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.databinding.ItemContactBinding;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.ListItem;
-import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.utils.IrregularUnicodeDetector;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XEP0392Helper;
import eu.siacs.conversations.xmpp.Jid;
-
+import im.conversations.android.xmpp.model.stanza.Presence;
import java.util.List;
public class ListItemAdapter extends ArrayAdapter<ListItem> {
- protected XmppActivity activity;
- private boolean showDynamicTags = false;
- private OnTagClickedListener mOnTagClickedListener = null;
- private final View.OnClickListener onTagTvClick = view -> {
- if (view instanceof TextView tv && mOnTagClickedListener != null) {
- final String tag = tv.getText().toString();
- mOnTagClickedListener.onTagClicked(tag);
- }
- };
-
- public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
- super(activity, 0, objects);
- this.activity = activity;
- }
-
-
- public void refreshSettings() {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
- this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, activity.getResources().getBoolean(R.bool.show_dynamic_tags));
- }
-
- @NonNull
- @Override
- public View getView(int position, View view, @NonNull ViewGroup parent) {
- LayoutInflater inflater = activity.getLayoutInflater();
- final ListItem item = getItem(position);
- ViewHolder viewHolder;
- View innerView;
- if (view == null) {
- final ItemContactBinding binding = DataBindingUtil.inflate(inflater,R.layout.item_contact,parent,false);
- viewHolder = ViewHolder.get(binding);
- view = binding.getRoot();
- innerView = binding.inner;
- } else {
- viewHolder = (ViewHolder) view.getTag();
- innerView = viewHolder.inner;
- }
- if (view.isActivated()) {
- Log.d(Config.LOGTAG,"item "+item.getDisplayName()+" is activated");
- }
- if (activity.colorCodeAccounts()) {
- innerView.setBackgroundColor(item.getAccount().getColor(activity.isDark()));
- }
- //view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background));
- final List<ListItem.Tag> tags = item.getTags(activity);
- final boolean hasMetaTags;
- if (item instanceof Contact contact) {
- hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE;
- } else {
- hasMetaTags = false;
- }
- if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
- viewHolder.tags.setVisibility(View.GONE);
- } else {
- viewHolder.tags.setVisibility(View.VISIBLE);
- viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1);
- final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
- for (final ListItem.Tag tag : tags) {
- final String name = tag.getName();
- final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
- tv.setText(name);
- tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(name))));
- tv.setOnClickListener(this.onTagTvClick);
- final int id = ViewCompat.generateViewId();
- tv.setId(id);
- viewIdBuilder.add(id);
- viewHolder.tags.addView(tv);
- }
- if (item instanceof Contact contact) {
- if (contact.isBlocked()) {
- final TextView tv =
- (TextView)
- inflater.inflate(
- R.layout.item_tag, viewHolder.tags, false);
- tv.setText(R.string.blocked);
- tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(),ContextCompat.getColor(tv.getContext(),R.color.gray_800))));
- final int id = ViewCompat.generateViewId();
- tv.setId(id);
- viewIdBuilder.add(id);
- viewHolder.tags.addView(tv);
- } else {
- final Presence.Status status = contact.getShownStatus();
- if (status != Presence.Status.OFFLINE) {
+ protected XmppActivity activity;
+ private boolean showDynamicTags = false;
+ private OnTagClickedListener mOnTagClickedListener = null;
+ private final View.OnClickListener onTagTvClick =
+ view -> {
+ if (view instanceof TextView tv && mOnTagClickedListener != null) {
+ final String tag = tv.getText().toString();
+ mOnTagClickedListener.onTagClicked(tag);
+ }
+ };
+
+ public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
+ super(activity, 0, objects);
+ this.activity = activity;
+ }
+
+ public void refreshSettings() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
+ this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false);
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, View view, @NonNull ViewGroup parent) {
+ LayoutInflater inflater = activity.getLayoutInflater();
+ final ListItem item = getItem(position);
+ ViewHolder viewHolder;
+ if (view == null) {
+ final ItemContactBinding binding =
+ DataBindingUtil.inflate(inflater, R.layout.item_contact, parent, false);
+ viewHolder = ViewHolder.get(binding);
+ view = binding.getRoot();
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+ if (view.isActivated()) {
+ Log.d(Config.LOGTAG, "item " + item.getDisplayName() + " is activated");
+ }
+ // view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background));
+ final List<ListItem.Tag> tags = item.getTags(activity);
+ final boolean hasMetaTags;
+ if (item instanceof Contact contact) {
+ hasMetaTags =
+ contact.isBlocked()
+ || contact.getShownStatus() != Presence.Availability.OFFLINE;
+ } else {
+ hasMetaTags = false;
+ }
+ if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) {
+ viewHolder.tags.setVisibility(View.GONE);
+ } else {
+ viewHolder.tags.setVisibility(View.VISIBLE);
+ viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1);
+ final ImmutableList.Builder<Integer> viewIdBuilder = new ImmutableList.Builder<>();
+ for (final ListItem.Tag tag : tags) {
+ final String name = tag.getName();
+ final TextView tv =
+ (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
+ tv.setText(name);
+ tv.setBackgroundTintList(
+ ColorStateList.valueOf(
+ MaterialColors.harmonizeWithPrimary(
+ getContext(), XEP0392Helper.rgbFromNick(name))));
+ tv.setOnClickListener(this.onTagTvClick);
+ final int id = ViewCompat.generateViewId();
+ tv.setId(id);
+ viewIdBuilder.add(id);
+ viewHolder.tags.addView(tv);
+ }
+ if (item instanceof Contact contact) {
+ if (contact.isBlocked()) {
+ final TextView tv =
+ (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
+ tv.setText(R.string.blocked);
+ tv.setBackgroundTintList(
+ ColorStateList.valueOf(
+ MaterialColors.harmonizeWithPrimary(
+ tv.getContext(),
+ ContextCompat.getColor(
+ tv.getContext(), R.color.gray_800))));
+ final int id = ViewCompat.generateViewId();
+ tv.setId(id);
+ viewIdBuilder.add(id);
+ viewHolder.tags.addView(tv);
+ } else {
+ final Presence.Availability status = contact.getShownStatus();
+ if (status != Presence.Availability.OFFLINE) {
final TextView tv =
(TextView)
- inflater.inflate(
- R.layout.item_tag, viewHolder.tags, false);
- UIHelper.setStatus(tv, status);
- final int id = ViewCompat.generateViewId();
- tv.setId(id);
- viewIdBuilder.add(id);
- viewHolder.tags.addView(tv);
+ inflater.inflate(R.layout.item_tag, viewHolder.tags, false);
+ UIHelper.setStatus(tv, status);
+ final int id = ViewCompat.generateViewId();
+ tv.setId(id);
+ viewIdBuilder.add(id);
+ viewHolder.tags.addView(tv);
}
}
- }
- viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build()));
- }
- final Jid jid = item.getJid();
- if (jid != null) {
- viewHolder.jid.setVisibility(View.VISIBLE);
- viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid));
- } else {
- viewHolder.jid.setVisibility(View.GONE);
- }
- viewHolder.name.setText(item.getDisplayName());
- AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar);
- return view;
- }
-
- public void setOnTagClickedListener(OnTagClickedListener listener) {
- this.mOnTagClickedListener = listener;
- }
-
-
- public interface OnTagClickedListener {
- void onTagClicked(String tag);
- }
-
- private static class ViewHolder {
- private TextView name;
- private TextView jid;
- private ImageView avatar;
- private View inner;
- 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.inner = binding.inner;
- 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;
+ }
+ }
}
@@ -37,19 +37,16 @@ import android.graphics.drawable.Drawable;
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 {
@@ -63,7 +60,7 @@ public class SendButtonTool {
final boolean empty = text.isEmpty();
final boolean conference = c.getMode() == Conversation.MODE_MULTI;
if (c.getCorrectingMessage() != null
- && (empty || (text.equals(c.getCorrectingMessage().getBody()) && (subject.equals(c.getCorrectingMessage().getSubject())) && (c.getThread() == c.getCorrectingMessage().getThread() || (c.getThread() != null && c.getThread().equals(c.getCorrectingMessage().getThread())))))) {
+ && (empty || (text.equals(c.getCorrectingMessage().getBody()) && (subject.equals(c.getCorrectingMessage().getSubject())) && (c.getThread() == c.getCorrectingMessage().getThread() || (c.getThread() != null && c.getThread().equals(c.getCorrectingMessage().getThread())))))) {
return SendButtonAction.CANCEL;
} else if (conference && !c.getAccount().httpUploadAvailable()) {
if (empty && c.getNextCounterpart() != null) {
@@ -72,7 +69,7 @@ public class SendButtonTool {
return SendButtonAction.TEXT;
}
} else {
- if (empty && (c.getThread() == null || subject.length() == 0)) {
+ if (empty && (c.getThread() == null || subject.length() == 0)) {
if (conference && c.getNextCounterpart() != null) {
return SendButtonAction.CANCEL;
} else {
@@ -113,28 +110,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));
};
}
}
@@ -40,7 +40,6 @@ 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.Reaction;
import eu.siacs.conversations.entities.RtpSessionStatus;
import eu.siacs.conversations.entities.Transferable;
@@ -48,6 +47,7 @@ import eu.siacs.conversations.services.XmppConnectionService;
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;
@@ -600,7 +600,7 @@ public class UIHelper {
return LOCATION_QUESTIONS.contains(body);
}
- public static void setStatus(final TextView textView, Presence.Status status) {
+ public static void setStatus(final TextView textView, Presence.Availability status) {
final @StringRes int text;
final @ColorRes int color =
switch (status) {
@@ -1,20 +0,0 @@
-package eu.siacs.conversations.xml;
-
-import com.google.common.io.ByteSource;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class XmlElementReader {
-
- public static Element read(byte[] bytes) throws IOException {
- return read(ByteSource.wrap(bytes).openStream());
- }
-
- public static Element read(InputStream inputStream) throws IOException {
- final XmlReader xmlReader = new XmlReader();
- xmlReader.setInputStream(inputStream);
- return xmlReader.readElement(xmlReader.readTag());
- }
-
-}
@@ -1,5 +1,4 @@
package eu.siacs.conversations.xmpp;
-
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context;
@@ -19,6 +18,9 @@ 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;
@@ -70,7 +72,10 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
+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;
@@ -87,12 +92,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;
@@ -115,6 +120,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;
@@ -124,6 +132,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;
@@ -175,6 +184,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;
@@ -197,7 +207,6 @@ public class XmppConnection implements Runnable {
protected final Account account;
private final Features features = new Features(this);
- private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>();
private final HashMap<String, Jid> commands = new HashMap<>();
private final SparseArray<Stanza> mStanzaQueue = new SparseArray<>();
private final Hashtable<String, Pair<Iq, Pair<Consumer<Iq>, ScheduledFuture>>> packetCallbacks = new Hashtable<>();
@@ -225,7 +234,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);
@@ -250,6 +258,7 @@ public class XmppConnection implements Runnable {
private CountDownLatch mStreamCountDownLatch;
private static ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1);
private boolean dane = false;
+ private final ClassToInstanceMap<AbstractManager> managers;
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
@@ -259,6 +268,12 @@ public class XmppConnection implements Runnable {
this.unregisteredIqListener = new IqParser(service, account);
this.messageListener = new MessageParser(service, account);
this.bindListener = new BindProcessor(service, account);
+ this.managers =
+ new ImmutableClassToInstanceMap.Builder<AbstractManager>()
+ .put(
+ DiscoManager.class,
+ new DiscoManager(service, this))
+ .build();
}
private static void fixResource(final Context context, final Account account) {
@@ -2074,9 +2089,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();
}
@@ -2241,41 +2254,99 @@ public class XmppConnection implements Runnable {
final boolean waitForDisco, final boolean carbonsEnabled) {
features.carbonsEnabled = carbonsEnabled;
features.blockListRequested = false;
- synchronized (this.disco) {
- this.disco.clear();
- }
+ getManager(DiscoManager.class).clear();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery");
- mPendingServiceDiscoveries.set(0);
mWaitForDisco.set(waitForDisco);
this.lastDiscoStarted = SystemClock.elapsedRealtime();
mXmppConnectionService.scheduleWakeUpCall(
Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode());
- final Element caps = streamFeatures.findChild("c");
- final String hash = caps == null ? null : caps.getAttribute("hash");
- final String ver = caps == null ? null : caps.getAttribute("ver");
- ServiceDiscoveryResult discoveryResult = null;
- if (hash != null && ver != null) {
- discoveryResult =
- mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver));
- }
- final boolean requestDiscoItemsFirst =
- !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY);
- if (requestDiscoItemsFirst) {
- sendServiceDiscoveryItems(account.getDomain());
- }
- if (discoveryResult == null) {
- sendServiceDiscoveryInfo(account.getDomain());
- } else {
- Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache");
- disco.put(account.getDomain(), discoveryResult);
- }
+
+ final var nodeHash = streamFeatures.getCapabilities();
+ final var serverInfoFuture =
+ getManager(DiscoManager.class)
+ .infoOrCache(Entity.discoItem(account.getDomain()), nodeHash);
+
final var features = getFeatures();
if (!features.bind2()) {
discoverMamPreferences();
}
- sendServiceDiscoveryInfo(account.getJid().asBareJid());
- if (!requestDiscoItemsFirst) {
- sendServiceDiscoveryItems(account.getDomain());
+
+ final var accountInfoFuture =
+ getManager(DiscoManager.class)
+ .info(Entity.discoItem(account.getJid().asBareJid()), null);
+
+ final var itemsFuture =
+ getManager(DiscoManager.class).itemsWithInfo(Entity.discoItem(account.getDomain()));
+
+ final var catchingServerFuture =
+ Futures.catching(
+ serverInfoFuture,
+ DiscoManager.CapsHashMismatchException.class,
+ input -> {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": error in server caps",
+ input);
+ return null;
+ },
+ MoreExecutors.directExecutor());
+
+ Futures.addCallback(
+ Futures.allAsList(accountInfoFuture, catchingServerFuture),
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(List<Object> result) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": advanced stream future done");
+ enableAdvancedStreamFeatures();
+ }
+
+ @Override
+ public void onFailure(@Nullable Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ "could not fetch disco for advanced stream features",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
+
+ if (mWaitForDisco.get()) {
+ final ListenableFuture<Void> discoComplete =
+ Futures.whenAllComplete(serverInfoFuture, accountInfoFuture, itemsFuture)
+ .call(() -> null, MoreExecutors.directExecutor());
+ Futures.addCallback(
+ discoComplete,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Void result) {
+ if (timeout(serverInfoFuture, accountInfoFuture, itemsFuture)) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": reached timeout while waiting for disco");
+ return;
+ }
+ if (mWaitForDisco.compareAndSet(true, false)) {
+ finalizeBindOrError();
+ } else {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid()
+ + ": disco complete but bind was already"
+ + " finalized");
+ }
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ Log.d(Config.LOGTAG, "error in disco: ", t);
+ }
+ },
+ MoreExecutors.directExecutor());
+ } else {
+ finalizeBind();
}
if (!mWaitForDisco.get()) {
@@ -2284,63 +2355,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() {
@@ -2364,39 +2391,42 @@ public class XmppConnection implements Runnable {
}
private void discoverCommands() {
- final Iq request = new Iq(Iq.Type.GET);
- request.setTo(account.getDomain());
- request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
- sendIqPacket(
- request,
- (response) -> {
- if (response.getType() == Iq.Type.RESULT) {
- final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
- if (query == null) {
- return;
- }
- final HashMap<String, Jid> commands = new HashMap<>();
- for (final Element child : query.getChildren()) {
- if ("item".equals(child.getName())) {
- final String node = child.getAttribute("node");
- final Jid jid = child.getAttributeAsJid("jid");
- if (node != null && jid != null) {
- commands.put(node, jid);
- }
- }
- }
- synchronized (this.commands) {
- this.commands.clear();
- this.commands.putAll(commands);
+ final var future =
+ getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain()));
+ Futures.addCallback(
+ future,
+ new FutureCallback<>() {
+ @Override
+ public void onSuccess(Map<String, Jid> result) {
+ synchronized (XmppConnection.this.commands) {
+ XmppConnection.this.commands.clear();
+ XmppConnection.this.commands.putAll(result);
}
}
- });
+
+ @Override
+ public void onFailure(@NonNull Throwable throwable) {
+ Log.d(
+ Config.LOGTAG,
+ account.getJid().asBareJid() + ": could not fetch commands",
+ throwable);
+ }
+ },
+ MoreExecutors.directExecutor());
}
public boolean isMamPreferenceAlways() {
return isMamPreferenceAlways;
}
+ private void finalizeBindOrError() {
+ try {
+ finalizeBind();
+ } catch (final Exception e) {
+ throw new Error(e);
+ }
+ }
+
private void finalizeBind() {
this.offlineMessagesRetrieved = false;
this.bindListener.run();
@@ -2420,46 +2450,6 @@ public class XmppConnection implements Runnable {
}
}
- private void sendServiceDiscoveryItems(final Jid server) {
- mPendingServiceDiscoveries.incrementAndGet();
- final Iq iq = new Iq(Iq.Type.GET);
- iq.setTo(server.getDomain());
- iq.query("http://jabber.org/protocol/disco#items");
- this.sendIqPacket(
- iq,
- (packet) -> {
- if (packet.getType() == Iq.Type.RESULT) {
- final HashSet<Jid> items = new HashSet<>();
- final List<Element> elements = packet.query().getChildren();
- for (final Element element : elements) {
- if (element.getName().equals("item")) {
- final Jid jid =
- Jid.Invalid.getNullForInvalid(
- element.getAttributeAsJid("jid"));
- if (jid != null && !jid.equals(account.getDomain())) {
- items.add(jid);
- }
- }
- }
- for (Jid jid : items) {
- sendServiceDiscoveryInfo(jid);
- }
- } else {
- Log.d(
- Config.LOGTAG,
- account.getJid().asBareJid()
- + ": could not query disco items of "
- + server);
- }
- if (packet.getType() != Iq.Type.TIMEOUT) {
- if (mPendingServiceDiscoveries.decrementAndGet() == 0
- && mWaitForDisco.compareAndSet(true, false)) {
- finalizeBind();
- }
- }
- });
- }
-
private void sendEnableCarbons() {
final Iq iq = new Iq(Iq.Type.SET);
iq.addChild("enable", Namespace.CARBONS);
@@ -2820,28 +2810,23 @@ public class XmppConnection implements Runnable {
this.boundStreamFeatures = null;
}
- private List<Entry<Jid, ServiceDiscoveryResult>> findDiscoItemsByFeature(final String feature) {
- synchronized (this.disco) {
- final List<Entry<Jid, ServiceDiscoveryResult>> items = new ArrayList<>();
- for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) {
- if (cursor.getValue().getFeatures().contains(feature)) {
- items.add(cursor);
- }
- }
- return items;
- }
+ public <M extends AbstractManager> M getManager(final Class<M> clazz) {
+ return this.managers.getInstance(clazz);
}
- public Entry<Jid, ServiceDiscoveryResult> getServiceDiscoveryResultByFeature(
- final String feature) {
- synchronized (this.disco) {
- for (final var cursor : this.disco.entrySet()) {
- if (cursor.getValue().getFeatures().contains(feature)) {
- return cursor;
- }
+ private List<Entry<Jid, InfoQuery>> findDiscoItemsByFeature(final String feature) {
+ final List<Entry<Jid, InfoQuery>> items = new ArrayList<>();
+ for (final Entry<Jid, InfoQuery> cursor :
+ getManager(DiscoManager.class).getServerItems().entrySet()) {
+ if (cursor.getValue().getFeatureStrings().contains(feature)) {
+ items.add(cursor);
}
- return null;
}
+ return items;
+ }
+
+ public Entry<Jid, InfoQuery> getServiceDiscoveryResultByFeature(final String feature) {
+ return Iterables.getFirst(findDiscoItemsByFeature(feature), null);
}
public Jid findDiscoItemByFeature(final String feature) {
@@ -2869,15 +2854,14 @@ public class XmppConnection implements Runnable {
public List<String> getMucServers() {
List<String> servers = new ArrayList<>();
- synchronized (this.disco) {
- for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) {
- final ServiceDiscoveryResult value = cursor.getValue();
- if (value.getFeatures().contains("http://jabber.org/protocol/muc")
- && value.hasIdentity("conference", "text")
- && !value.getFeatures().contains("jabber:iq:gateway")
- && !value.hasIdentity("conference", "irc")) {
- servers.add(cursor.getKey().toString());
- }
+ for (final Entry<Jid, InfoQuery> entry :
+ getManager(DiscoManager.class).getServerItems().entrySet()) {
+ final var value = entry.getValue();
+ if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc")
+ && value.hasIdentityWithCategoryAndType("conference", "text")
+ && !value.getFeatureStrings().contains("jabber:iq:gateway")
+ && !value.hasIdentityWithCategoryAndType("conference", "irc")) {
+ servers.add(entry.getKey().toString());
}
}
return servers;
@@ -3004,6 +2988,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) {
@@ -3127,6 +3115,29 @@ public class XmppConnection implements Runnable {
}
}
+ public abstract static class Delegate {
+
+ protected final XmppConnectionService context;
+ protected final XmppConnection connection;
+
+ protected Delegate(final XmppConnectionService context, final XmppConnection connection) {
+ this.context = context;
+ this.connection = connection;
+ }
+
+ protected Account getAccount() {
+ return connection.account;
+ }
+
+ protected DatabaseBackend getDatabase() {
+ return DatabaseBackend.getInstance(context);
+ }
+
+ protected <T extends AbstractManager> T getManager(final Class<T> type) {
+ return connection.getManager(type);
+ }
+ }
+
public class Features {
XmppConnection connection;
private boolean carbonsEnabled = false;
@@ -3138,10 +3149,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() {
@@ -3197,19 +3206,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() {
@@ -3244,9 +3250,9 @@ public class XmppConnection implements Runnable {
return MessageArchiveService.Version.has(getAccountFeatures());
}
- public List<String> getAccountFeatures() {
- ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid());
- return result == null ? Collections.emptyList() : result.getFeatures();
+ public Collection<String> getAccountFeatures() {
+ final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid());
+ return infoQuery == null ? Collections.emptyList() : infoQuery.getFeatureStrings();
}
public boolean push() {
@@ -3263,12 +3269,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;
@@ -3289,7 +3295,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;
@@ -3318,7 +3324,7 @@ public class XmppConnection implements Runnable {
try {
return Long.parseLong(
result.getValue()
- .getExtendedDiscoInformation(
+ .getServiceDiscoveryExtension(
Namespace.HTTP_UPLOAD, "max-file-size"));
} catch (final Exception e) {
return -1;
@@ -5,18 +5,16 @@ import androidx.annotation.NonNull;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
import im.conversations.android.xmpp.model.jingle.Jingle;
import im.conversations.android.xmpp.model.stanza.Iq;
import java.util.Arrays;
@@ -331,14 +329,15 @@ public abstract class AbstractJingleConnection {
}
protected boolean remoteHasFeature(final String feature) {
- final Contact contact = id.getContact();
- final Presence presence =
- contact.getPresences().get(Strings.nullToEmpty(id.with.getResource()));
- final ServiceDiscoveryResult serviceDiscoveryResult =
- presence == null ? null : presence.getServiceDiscoveryResult();
- final List<String> features =
- serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures();
- return features != null && features.contains(feature);
+ final var connection = id.account.getXmppConnection();
+ if (connection == null) {
+ return false;
+ }
+ final var infoQuery = connection.getManager(DiscoManager.class).get(id.with);
+ if (infoQuery == null) {
+ return false;
+ }
+ return infoQuery.hasFeature(feature);
}
public static class Id {
@@ -3,38 +3,39 @@ 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;
import eu.siacs.conversations.xmpp.Jid;
public class RtpCapability {
- private static final List<String> BASIC_RTP_REQUIREMENTS = Arrays.asList(
- Namespace.JINGLE,
- Namespace.JINGLE_TRANSPORT_ICE_UDP,
- Namespace.JINGLE_APPS_RTP,
- Namespace.JINGLE_APPS_DTLS
- );
- private static final Collection<String> VIDEO_REQUIREMENTS = Arrays.asList(
- Namespace.JINGLE_FEATURE_AUDIO,
- Namespace.JINGLE_FEATURE_VIDEO
- );
+ private static final List<String> BASIC_RTP_REQUIREMENTS =
+ Arrays.asList(
+ Namespace.JINGLE,
+ Namespace.JINGLE_TRANSPORT_ICE_UDP,
+ Namespace.JINGLE_APPS_RTP,
+ Namespace.JINGLE_APPS_DTLS);
+ private static final Collection<String> VIDEO_REQUIREMENTS =
+ Arrays.asList(Namespace.JINGLE_FEATURE_AUDIO, Namespace.JINGLE_FEATURE_VIDEO);
- public static Capability check(final Presence presence) {
- final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
- final Set<String> features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures());
+ public static Capability check(final InfoQuery infoQuery) {
+ final Set<String> features =
+ infoQuery == null
+ ? Collections.emptySet()
+ : ImmutableSet.copyOf(infoQuery.getFeatureStrings());
if (features.containsAll(BASIC_RTP_REQUIREMENTS)) {
if (features.containsAll(VIDEO_REQUIREMENTS)) {
return Capability.VIDEO;
@@ -47,15 +48,23 @@ public class RtpCapability {
}
public static String[] filterPresences(final Contact contact, Capability required) {
+ final var connection = contact.getAccount().getXmppConnection();
+ if (connection == null) {
+ return new String[0];
+ }
final Presences presences = contact.getPresences();
final ArrayList<String> resources = new ArrayList<>();
- for (final Map.Entry<String, Presence> presence : presences.getPresencesMap().entrySet()) {
- final Capability capability = check(presence.getValue());
+ for (final String resource : presences.getPresencesMap().keySet()) {
+ final var jid =
+ Strings.isNullOrEmpty(resource)
+ ? contact.getJid().asBareJid()
+ : contact.getJid().withResource(resource);
+ final Capability capability = check(connection.getManager(DiscoManager.class).get(jid));
if (capability == Capability.NONE) {
continue;
}
if (required == Capability.AUDIO || capability == required) {
- resources.add(presence.getKey());
+ resources.add(resource);
}
}
return resources.toArray(new String[0]);
@@ -76,9 +85,17 @@ public class RtpCapability {
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) {
@@ -90,18 +107,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)) {
@@ -114,5 +156,4 @@ public class RtpCapability {
}
}
}
-
}
@@ -107,6 +107,7 @@ public class WebRTCWrapper {
.add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte)
.add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte)
.add("GT-I9505") // Samsung Galaxy S4 (jfltexx)
+ .add("Nexus 7") // ASUS Nexus 7
.build();
private final EventCallback eventCallback;
@@ -0,0 +1,12 @@
+package eu.siacs.conversations.xmpp.manager;
+
+import android.content.Context;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.services.XmppConnectionService;
+
+public abstract class AbstractManager extends XmppConnection.Delegate {
+
+ protected AbstractManager(final XmppConnectionService context, final XmppConnection connection) {
+ super(context, connection);
+ }
+}
@@ -0,0 +1,314 @@
+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 eu.siacs.conversations.services.XmppConnectionService;
+import im.conversations.android.xmpp.Entity;
+import im.conversations.android.xmpp.EntityCapabilities;
+import im.conversations.android.xmpp.EntityCapabilities2;
+import im.conversations.android.xmpp.model.Hash;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.disco.items.Item;
+import im.conversations.android.xmpp.model.disco.items.ItemsQuery;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+
+public class DiscoManager extends AbstractManager {
+
+ public static final String CAPABILITY_NODE = "http://conversations.im";
+
+ // this is the runtime cache that stores disco information for all entities seen during a
+ // session
+
+ // a caps cache will be build in the database
+
+ private final Map<Jid, InfoQuery> entityInformation = new HashMap<>();
+ private final Map<Jid, ImmutableSet<Jid>> discoItems = new HashMap<>();
+
+ public DiscoManager(XmppConnectionService context, XmppConnection connection) {
+ super(context, connection);
+ }
+
+ public static EntityCapabilities.Hash buildHashFromNode(final String node) {
+ final var capsPrefix = CAPABILITY_NODE + "#";
+ final var caps2Prefix = Namespace.ENTITY_CAPABILITIES_2 + "#";
+ if (node.startsWith(capsPrefix)) {
+ final String hash = node.substring(capsPrefix.length());
+ if (Strings.isNullOrEmpty(hash)) {
+ return null;
+ }
+ if (BaseEncoding.base64().canDecode(hash)) {
+ return EntityCapabilities.EntityCapsHash.of(hash);
+ }
+ } else if (node.startsWith(caps2Prefix)) {
+ final String caps = node.substring(caps2Prefix.length());
+ if (Strings.isNullOrEmpty(caps)) {
+ return null;
+ }
+ final int separator = caps.lastIndexOf('.');
+ if (separator < 0) {
+ return null;
+ }
+ final Hash.Algorithm algorithm = Hash.Algorithm.tryParse(caps.substring(0, separator));
+ final String hash = caps.substring(separator + 1);
+ if (algorithm == null || Strings.isNullOrEmpty(hash)) {
+ return null;
+ }
+ if (BaseEncoding.base64().canDecode(hash)) {
+ return EntityCapabilities2.EntityCaps2Hash.of(algorithm, hash);
+ }
+ }
+ return null;
+ }
+
+ public ListenableFuture<Void> infoOrCache(
+ final Entity entity,
+ final im.conversations.android.xmpp.model.capabilties.EntityCapabilities.NodeHash
+ nodeHash) {
+ if (nodeHash == null) {
+ return infoOrCache(entity, null, null);
+ }
+ return infoOrCache(entity, nodeHash.node, nodeHash.hash);
+ }
+
+ public ListenableFuture<Void> infoOrCache(
+ final Entity entity, final String node, final EntityCapabilities.Hash hash) {
+ final var cached = getDatabase().getInfoQuery(hash);
+ if (cached != null) {
+ if (node == null || hash != null) {
+ this.put(entity.address, cached);
+ }
+ return Futures.immediateFuture(null);
+ }
+ return Futures.transform(
+ info(entity, node, hash), f -> null, MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<InfoQuery> info(
+ @NonNull final Entity entity, @Nullable final String node) {
+ return info(entity, node, null);
+ }
+
+ public ListenableFuture<InfoQuery> info(
+ final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) {
+ final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node;
+ final var iqRequest = new Iq(Iq.Type.GET);
+ iqRequest.setTo(entity.address);
+ final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery());
+ if (requestNode != null) {
+ infoQueryRequest.setNode(requestNode);
+ }
+ final var future = connection.sendIqPacket(iqRequest);
+ return Futures.transform(
+ future,
+ iqResult -> {
+ final var infoQuery = iqResult.getExtension(InfoQuery.class);
+ if (infoQuery == null) {
+ throw new IllegalStateException("Response did not have query child");
+ }
+ if (!Objects.equals(requestNode, infoQuery.getNode())) {
+ throw new IllegalStateException(
+ "Node in response did not match node in request");
+ }
+
+ if (node == null
+ || (hash != null
+ && hash.capabilityNode(node).equals(infoQuery.getNode()))) {
+ // only storing results w/o nodes
+ this.put(entity.address, infoQuery);
+ }
+
+ final var caps = EntityCapabilities.hash(infoQuery);
+ final var caps2 = EntityCapabilities2.hash(infoQuery);
+ if (hash instanceof EntityCapabilities.EntityCapsHash) {
+ checkMatch(
+ (EntityCapabilities.EntityCapsHash) hash,
+ caps,
+ EntityCapabilities.EntityCapsHash.class);
+ }
+ if (hash instanceof EntityCapabilities2.EntityCaps2Hash) {
+ checkMatch(
+ (EntityCapabilities2.EntityCaps2Hash) hash,
+ caps2,
+ EntityCapabilities2.EntityCaps2Hash.class);
+ }
+ // we want to avoid caching disco info for entities that put variable data (like
+ // number of occupants in a MUC) into it
+ final boolean cache =
+ Objects.nonNull(hash)
+ || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES)
+ || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES_2);
+
+ if (cache) {
+ getDatabase().insertCapsCache(caps, caps2, infoQuery);
+ }
+
+ return infoQuery;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ private <H extends EntityCapabilities.Hash> void checkMatch(
+ final H expected, final H was, final Class<H> clazz) {
+ if (Arrays.equals(expected.hash, was.hash)) {
+ return;
+ }
+ throw new CapsHashMismatchException(
+ String.format(
+ "%s mismatch. Expected %s was %s",
+ clazz.getSimpleName(),
+ BaseEncoding.base64().encode(expected.hash),
+ BaseEncoding.base64().encode(was.hash)));
+ }
+
+ public ListenableFuture<Collection<Item>> items(final Entity.DiscoItem entity) {
+ return items(entity, null);
+ }
+
+ public ListenableFuture<Collection<Item>> items(
+ final Entity.DiscoItem entity, @Nullable final String node) {
+ final var requestNode = Strings.emptyToNull(node);
+ final var iqPacket = new Iq(Iq.Type.GET);
+ iqPacket.setTo(entity.address);
+ final ItemsQuery itemsQueryRequest = iqPacket.addExtension(new ItemsQuery());
+ if (requestNode != null) {
+ itemsQueryRequest.setNode(requestNode);
+ }
+ final var future = connection.sendIqPacket(iqPacket);
+ return Futures.transform(
+ future,
+ iqResult -> {
+ final var itemsQuery = iqResult.getExtension(ItemsQuery.class);
+ if (itemsQuery == null) {
+ throw new IllegalStateException();
+ }
+ if (!Objects.equals(requestNode, itemsQuery.getNode())) {
+ throw new IllegalStateException(
+ "Node in response did not match node in request");
+ }
+ final var items = itemsQuery.getExtensions(Item.class);
+
+ final var validItems =
+ Collections2.filter(items, i -> Objects.nonNull(i.getJid()));
+
+ final var itemsAsAddresses =
+ ImmutableSet.copyOf(Collections2.transform(validItems, Item::getJid));
+ if (node == null) {
+ this.discoItems.put(entity.address, itemsAsAddresses);
+ }
+ return validItems;
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<List<InfoQuery>> itemsWithInfo(final Entity.DiscoItem entity) {
+ final var itemsFutures = items(entity);
+ final var filtered =
+ Futures.transform(
+ itemsFutures,
+ items ->
+ Collections2.filter(
+ items,
+ i ->
+ i.getNode() == null
+ && !entity.address.equals(i.getJid())),
+ MoreExecutors.directExecutor());
+ return Futures.transformAsync(
+ filtered,
+ items -> {
+ Collection<ListenableFuture<InfoQuery>> infoFutures =
+ Collections2.transform(
+ items, i -> info(Entity.discoItem(i.getJid()), i.getNode()));
+ return Futures.allAsList(infoFutures);
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public ListenableFuture<Map<String, Jid>> commands(final Entity.DiscoItem entity) {
+ final var itemsFuture = items(entity, Namespace.COMMANDS);
+ return Futures.transform(
+ itemsFuture,
+ items -> {
+ final var builder = new ImmutableMap.Builder<String, Jid>();
+ for (final var item : items) {
+ final var jid = item.getJid();
+ final var node = item.getNode();
+ if (Jid.Invalid.isValid(jid) && node != null) {
+ builder.put(node, jid);
+ }
+ }
+ return builder.buildKeepingLast();
+ },
+ MoreExecutors.directExecutor());
+ }
+
+ public Map<Jid, InfoQuery> getServerItems() {
+ final var builder = new ImmutableMap.Builder<Jid, InfoQuery>();
+ final var domain = connection.getAccount().getDomain();
+ final var domainInfoQuery = get(domain);
+ if (domainInfoQuery != null) {
+ builder.put(domain, domainInfoQuery);
+ }
+ final var items = this.discoItems.get(domain);
+ if (items == null) {
+ return builder.build();
+ }
+ for (final var item : items) {
+ final var infoQuery = get(item);
+ if (infoQuery == null) {
+ continue;
+ }
+ builder.put(item, infoQuery);
+ }
+ return builder.buildKeepingLast();
+ }
+
+ private void put(final Jid address, final InfoQuery infoQuery) {
+ synchronized (this.entityInformation) {
+ this.entityInformation.put(address, infoQuery);
+ }
+ if (infoQuery.hasIdentityWithCategoryAndType("gateway", "pstn")) {
+ final var contact = getAccount().getRoster().getContact(address);
+ contact.registerAsPhoneAccount(context);
+ contact.refreshCaps();
+ context.getQuickConversationsService().considerSyncBackground(false);
+ }
+ }
+
+ public InfoQuery get(final Jid address) {
+ synchronized (this.entityInformation) {
+ return this.entityInformation.get(address);
+ }
+ }
+
+ public void clear() {
+ synchronized (this.entityInformation) {
+ this.entityInformation.clear();
+ }
+ }
+
+ public static final class CapsHashMismatchException extends IllegalStateException {
+ public CapsHashMismatchException(final String message) {
+ super(message);
+ }
+ }
+}
@@ -0,0 +1,21 @@
+package im.conversations.android.xml;
+
+import com.google.common.io.ByteSource;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.XmlReader;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class XmlElementReader {
+
+ public static Element read(byte[] bytes) throws IOException {
+ return read(ByteSource.wrap(bytes).openStream());
+ }
+
+ public static Element read(final InputStream inputStream) throws IOException {
+ try (final XmlReader xmlReader = new XmlReader()) {
+ xmlReader.setInputStream(inputStream);
+ return xmlReader.readElement(xmlReader.readTag());
+ }
+ }
+}
@@ -1,6 +1,6 @@
package im.conversations.android.xmpp;
-import org.jxmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.Jid;
public abstract class Entity {
@@ -8,7 +8,6 @@ import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Bytes;
-
import eu.siacs.conversations.xml.Namespace;
import im.conversations.android.xmpp.model.Hash;
import im.conversations.android.xmpp.model.data.Data;
@@ -42,16 +41,12 @@ public class EntityCapabilities2 {
}
private static HashFunction toHashFunction(final Hash.Algorithm algorithm) {
- switch (algorithm) {
- case SHA_1:
- return Hashing.sha1();
- case SHA_256:
- return Hashing.sha256();
- case SHA_512:
- return Hashing.sha512();
- default:
- throw new IllegalArgumentException("Unknown hash algorithm");
- }
+ return switch (algorithm) {
+ case SHA_1 -> Hashing.sha1();
+ case SHA_256 -> Hashing.sha256();
+ case SHA_512 -> Hashing.sha512();
+ default -> throw new IllegalArgumentException("Unknown hash algorithm");
+ };
}
private static String asHex(final String message) {
@@ -29,6 +29,10 @@ public class Data extends Extension {
this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName()));
}
+ public Field getFieldByName(final String name) {
+ return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null);
+ }
+
private void addField(final String name, final Object value) {
addField(name, value, null);
}
@@ -1,6 +1,8 @@
package im.conversations.android.xmpp.model.data;
-import eu.siacs.conversations.xml.Element;
+
import com.google.common.collect.Collections2;
+import com.google.common.collect.Iterables;
+import eu.siacs.conversations.xml.Element;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
import java.util.Collection;
@@ -19,6 +21,10 @@ public class Field extends Extension {
return Collections2.transform(getExtensions(Value.class), Element::getContent);
}
+ public String getValue() {
+ return Iterables.getFirst(getValues(), null);
+ }
+
public void setFieldName(String name) {
this.setAttribute("var", name);
}
@@ -1,9 +1,12 @@
package im.conversations.android.xmpp.model.disco.info;
+import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.Extension;
+import im.conversations.android.xmpp.model.data.Data;
import java.util.Collection;
+import java.util.Objects;
@XmlElement(name = "query")
public class InfoQuery extends Extension {
@@ -35,4 +38,39 @@ public class InfoQuery extends Extension {
public boolean hasIdentityWithCategory(final String category) {
return Iterables.any(getIdentities(), i -> category.equals(i.getCategory()));
}
+
+ public boolean hasIdentityWithCategoryAndType(final String category, final String type) {
+ return Iterables.any(
+ getIdentities(), i -> (category == null || category.equals(i.getCategory())) && (type == null || type.equals(i.getType())));
+ }
+
+ public Collection<String> getFeatureStrings() {
+ return Collections2.filter(
+ Collections2.transform(getFeatures(), Feature::getVar), Objects::nonNull);
+ }
+
+ public Collection<Data> getServiceDiscoveryExtensions() {
+ return getExtensions(Data.class);
+ }
+
+ public Data getServiceDiscoveryExtension(final String formType) {
+ return Iterables.find(
+ getServiceDiscoveryExtensions(), e -> formType.equals(e.getFormType()), null);
+ }
+
+ public String getServiceDiscoveryExtension(final String formType, final String fieldName) {
+ final var extension =
+ Iterables.find(
+ getServiceDiscoveryExtensions(),
+ e -> formType.equals(e.getFormType()),
+ null);
+ if (extension == null) {
+ return null;
+ }
+ final var field = extension.getFieldByName(fieldName);
+ if (field == null) {
+ return null;
+ }
+ return field.getValue();
+ }
}
@@ -1,7 +1,10 @@
package im.conversations.android.xmpp.model.stanza;
+import com.google.common.base.Strings;
import im.conversations.android.annotation.XmlElement;
import im.conversations.android.xmpp.model.capabilties.EntityCapabilities;
+import im.conversations.android.xmpp.model.jabber.Show;
+import im.conversations.android.xmpp.model.jabber.Status;
@XmlElement
public class Presence extends Stanza implements EntityCapabilities {
@@ -9,4 +12,63 @@ public class Presence extends Stanza implements EntityCapabilities {
public Presence() {
super(Presence.class);
}
+
+ public Availability getAvailability() {
+ final var show = getExtension(Show.class);
+ if (show == null) {
+ return Availability.ONLINE;
+ }
+ return Availability.valueOfShown(show.getContent());
+ }
+
+ public void setAvailability(final Availability availability) {
+ if (availability == null || availability == Availability.ONLINE) {
+ return;
+ }
+ this.addExtension(new Show()).setContent(availability.toShowString());
+ }
+
+ public void setStatus(final String status) {
+ if (Strings.isNullOrEmpty(status)) {
+ return;
+ }
+ this.addExtension(new Status()).setContent(status);
+ }
+
+ public String getStatus() {
+ final var status = getExtension(Status.class);
+ return status == null ? null : status.getContent();
+ }
+
+ public enum Availability {
+ CHAT,
+ ONLINE,
+ AWAY,
+ XA,
+ DND,
+ OFFLINE;
+
+ public String toShowString() {
+ return switch (this) {
+ case CHAT -> "chat";
+ case AWAY -> "away";
+ case XA -> "xa";
+ case DND -> "dnd";
+ default -> null;
+ };
+ }
+
+ public static Availability valueOfShown(final String content) {
+ if (Strings.isNullOrEmpty(content)) {
+ return Availability.ONLINE;
+ }
+ return switch (content) {
+ case "away" -> Availability.AWAY;
+ case "xa" -> Availability.XA;
+ case "dnd" -> Availability.DND;
+ case "chat" -> Availability.CHAT;
+ default -> Availability.ONLINE;
+ };
+ }
+ }
}
@@ -1108,7 +1108,7 @@
<string name="show_to_contacts_only">Nur für Kontakte anzeigen</string>
<string name="account_status_connection_timeout">Zeitüberschreitung beim Verbinden</string>
<string name="retry_with_p2p">Erneut mit P2P versuchen</string>
- <string name="account_status_channel_binding">Kanalbindung nicht verfügbar</string>
+ <string name="account_status_channel_binding">Keine Kanalbindung</string>
<string name="word_document">Word-Dokument</string>
<string name="restore_omemo_key">OMEMO-Schlüssel wiederherstellen</string>
<string name="non_quicksy_backup">Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen</string>
@@ -1128,7 +1128,7 @@
<string name="show_to_contacts_only">Näita vaid kontaktidele</string>
<string name="account_status_connection_timeout">Ühenduse on aegunud</string>
<string name="retry_with_p2p">Proovi uuesti võrdõigusvõrguga</string>
- <string name="account_status_channel_binding">Edastuskanaliga sidumine pole võimalik</string>
+ <string name="account_status_channel_binding">Edastuskanaliga sidumine puudub</string>
<string name="word_document">Wordi-dokument</string>
<string name="restore_omemo_key">Taasta OMEMO võtmed</string>
<string name="non_quicksy_backup">Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid</string>
@@ -820,12 +820,12 @@
<string name="ebook">e-book</string>
<string name="video_original">Original (non compressé)</string>
<string name="open_with">Ouvrir avec…</string>
- <string name="set_profile_picture">Photo de profil pour Conversations</string>
+ <string name="set_profile_picture">Photo de profil</string>
<string name="choose_account">Choisir un compte</string>
<string name="restore_backup">Restaurer la sauvegarde</string>
<string name="restore">Restaurer</string>
<string name="enter_password_to_restore">Entrez votre mot de passe pour que le compte %s restaure la sauvegarde.</string>
- <string name="restore_warning">N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte de l\'appareil d’origine.</string>
+ <string name="restore_warning">Ne restaurez pas les clés OMEMO pour tenter de cloner (exécuter simultanément) une installation. La restauration de clés OMEMO ne concerne que les migrations ou en cas de perte de l\'appareil d’origine.</string>
<string name="unable_to_restore_backup">Impossible de restaurer la sauvegarde.</string>
<string name="unable_to_decrypt_backup">Impossible de déchiffrer la sauvegarde. Le mot de passe est-il correct ?</string>
<string name="backup_channel_name">Sauvegarde & restauration</string>
@@ -1010,7 +1010,7 @@
<string name="outdated_backup_file_format">Vous essayez d\'importer un format de fichier de sauvegarde obsolète</string>
<string name="audiobook">Livre audio</string>
<string name="unified_push_distributor">Distributeur UnifiedPush</string>
- <string name="restore_warning_continued">Ne tentez pas de restaurer des sauvegardes que vous n\'avez pas créées vous-même !</string>
+ <string name="restore_warning_continued">Ne restaurez que des sauvegardes que vous avez vous-même créées.</string>
<string name="report_spam">Signaler un spam</string>
<string name="privacy_policy">Politique de confidentialité</string>
<string name="quicksy_wants_your_consent">Quicksy vous demande votre consentement pour utiliser vos données</string>
@@ -1097,7 +1097,7 @@
<string name="change_notification_settings">Modifier les paramètres de notification</string>
<string name="call_is_using_earpiece_tap_to_switch_to_speaker">L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur.</string>
<string name="call_is_using_earpiece">L\'appel passe par les écouteurs.</string>
- <string name="server_info_bind2">XEP-0386: Bind 2</string>
+ <string name="server_info_bind2">XEP-0386 : Bind 2</string>
<string name="edit_nick">Éditer le pseudo</string>
<string name="delete_pgp_key">Supprimer la clé OpenPGP</string>
<string name="call_is_using_bluetooth">L\'appel passe par le bluetooth.</string>
@@ -1119,7 +1119,7 @@
<string name="show_to_contacts_only">Montrer aux contacts uniquement</string>
<string name="pref_title_bubbles">Bulles de discussion</string>
<string name="could_not_modify_call">Impossible de modifier l\'appel</string>
- <string name="account_status_channel_binding">Channel binding indisponible</string>
+ <string name="account_status_channel_binding">Pas de Channel binding</string>
<string name="clients_may_not_support_av">Le client XMPP de votre contact peut ne pas prendre en charge les appels audio/vidéo.</string>
<string name="could_not_add_reaction">Impossible d\'ajouter une réaction</string>
<string name="pref_call_integration">Intégration d\'appel</string>
@@ -1131,4 +1131,18 @@
<string name="more_reactions">Plus de réactions</string>
<string name="add_reaction_title">Ajouter une réaction</string>
<string name="pref_show_avatars">Montrer l\'image de profil</string>
+ <string name="copied_phone_number">Numéro de téléphone copié dans le presse-papier</string>
+ <string name="copy_URI">Copier l\'URI</string>
+ <string name="uri_copied_to_clipboard">URI copiée dans le presse-papier</string>
+ <string name="copy_telephone_number">Copier le numéro de téléphone</string>
+ <string name="restore_omemo_key">Restaurer les clés OMEMO</string>
+ <string name="non_quicksy_backup">Quicksy ne peut restaurer des sauvegardes que pour des comptes de quicksy.im</string>
+ <string name="pref_backup_location">Emplacement de sauvegarde</string>
+ <string name="uri">URI</string>
+ <string name="copy_email_address">Copier l\'adresse mail</string>
+ <string name="copied_email_address">Adresse mail copiée dans le presse-papier</string>
+ <string name="account_status_service_outage_scheduled">Interruption de Service Planifiée</string>
+ <string name="sos_scheduled_return">Le retour du service est planifié pour %s</string>
+ <string name="copy_geo_uri">Copier la géolocalisation</string>
+ <string name="account_status_service_outage_known">Service en panne (problème connu)</string>
</resources>
@@ -1108,7 +1108,7 @@
<string name="show_to_contacts_only">Mostrar só aos contactos</string>
<string name="account_status_connection_timeout">Caducidade da conexión</string>
<string name="retry_with_p2p">Reintentar con P2P</string>
- <string name="account_status_channel_binding">Non está dispoñible a vinculación de canles</string>
+ <string name="account_status_channel_binding">Sen vinculación de canles</string>
<string name="word_document">Documento de Word</string>
<string name="restore_omemo_key">Restaurar claves OMEMO</string>
<string name="non_quicksy_backup">Quicksy só pode restaurar copias de apoio de contas quicksy.im</string>
@@ -1122,7 +1122,7 @@
<string name="pref_chat_bubbles">Messaggi di chat</string>
<string name="pref_chat_bubbles_summary">Colore di sfondo, dimensione caratteri, avatar</string>
<string name="pref_title_bubbles">Messaggi di chat</string>
- <string name="account_status_channel_binding">Associazione dei canali non disponibile</string>
+ <string name="account_status_channel_binding">Nessuna associazione dei canali</string>
<string name="word_document">Documento Word</string>
<string name="restore_omemo_key">Ripristina chiavi OMEMO</string>
<string name="non_quicksy_backup">Quicksy può ripristinare backup solo per profili quicksy.im</string>
@@ -353,7 +353,7 @@
<string name="bad_key_for_encryption">מפתח הצפנה שגוי.</string>
<string name="recording_error">שגיאה</string>
<string name="error_security_exception_during_image_copy">האפליקציה שבה השתמשת כדי לבחור תמונה זו לא סיפקה מספיק הרשאות לקרוא את הקובץ.\n\n<small>השתמש במנהל קבצים אחר כדי לבחור תמונה</small>.</string>
- <string name="account_status_channel_binding">עטיפת ערוץ אינה זמינה</string>
+ <string name="account_status_channel_binding">אין קישור ערוצים</string>
<string name="unpublish_pgp">הסר את המפתח הציבורי של OpenPGP</string>
<string name="unpublish_pgp_message">האם אתה בטוח שברצונך להסיר את מפתח OpenPGP הציבורי שלך מהודעת הנוכחות שלך?\nאנשי הקשר שלך לא יוכלו יותר לשלוח לך הודעות מוצפנות OpenPGP.</string>
<string name="openpgp_has_been_published">מפתח ציבורי OpenPGP פורסם.</string>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="action_settings">설정</string>
- <string name="action_accounts">계정 </string>
+ <string name="action_accounts">계정 관리</string>
<string name="action_contact_details">연락처 정보</string>
<string name="action_add_account">계정 추가 </string>
<string name="action_edit_contact">이름 편집 </string>
@@ -387,4 +387,8 @@
<string name="message_copied_to_clipboard">메세지가 클립보드에 복사되었습니다 </string>
<string name="title_activity_show_location">위치 표시 </string>
<string name="rtp_state_declined_or_busy">바쁨</string>
-</resources>
+ <string name="action_account">계정 관리</string>
+ <string name="action_archive_chat">채팅 기록</string>
+ <string name="action_muc_details">그룹 채팅 정보</string>
+ <string name="channel_details">채널 정보</string>
+</resources>
@@ -1141,7 +1141,7 @@
<string name="account_status_connection_timeout">Limit czasu połączenia</string>
<string name="retry_with_p2p">Spróbuj ponownie używając P2P</string>
<string name="word_document">dokument Microsoft Word</string>
- <string name="account_status_channel_binding">Przywiązywanie kanału niedostępne</string>
+ <string name="account_status_channel_binding">Brak przywiązywania kanału</string>
<string name="restore_omemo_key">Przywróć klucze OMEMO</string>
<string name="non_quicksy_backup">Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im</string>
<string name="pref_backup_location">Lokalizacja kopii zapasowej</string>
@@ -1126,7 +1126,7 @@
<string name="delete_avatar_message">Você gostaria de excluir seu avatar? Alguns clientes podem continuar mostrando uma cópia em cache do seu avatar.</string>
<string name="account_status_connection_timeout">Conexão demorou muito</string>
<string name="retry_with_p2p">Tentar novamente com P2P</string>
- <string name="account_status_channel_binding">Vínculo de canal indisponível</string>
+ <string name="account_status_channel_binding">Nenhum vínculo de canal</string>
<string name="word_document">Documento do Word</string>
<string name="restore_omemo_key">Restaurar as chaves OMEMO</string>
<string name="non_quicksy_backup">O Quicksy só pode restaurar backups de contas quicksy.im</string>
@@ -184,7 +184,7 @@
\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения.</string>
<string name="openpgp_has_been_published">Публичный ключ OpenPGP опубликован.</string>
<string name="mgmt_account_enable">Включить аккаунт</string>
- <string name="mgmt_account_delete_confirm_text">Удалить свой аккаунт? Удаление аккаунта также сотрёт все историю бесед.</string>
+ <string name="mgmt_account_delete_confirm_text">Удалить свой аккаунт? Удаление аккаунта также сотрёт всю историю бесед.</string>
<string name="attach_record_voice">Записать голос</string>
<string name="account_settings_jabber_id">XMPP-адрес</string>
<string name="block_jabber_id">Заблокировать XMPP-адрес</string>
@@ -1155,7 +1155,7 @@
<string name="show_to_contacts_only">Показывать только контактам</string>
<string name="account_status_connection_timeout">Истекло время ожидания подключения</string>
<string name="retry_with_p2p">Повторить через P2P</string>
- <string name="account_status_channel_binding">Привязка канала недоступна</string>
+ <string name="account_status_channel_binding">Нет привязки канала</string>
<string name="word_document">Документ Word</string>
<string name="restore_omemo_key">Восстановить ключи OMEMO</string>
<string name="non_quicksy_backup">Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im</string>
@@ -1142,7 +1142,7 @@
<string name="show_to_contacts_only">Приказуј само контактима</string>
<string name="account_status_connection_timeout">Истекла веза</string>
<string name="retry_with_p2p">Покушај поново са P2P</string>
- <string name="account_status_channel_binding">Везивање канала недоступно</string>
+ <string name="account_status_channel_binding">Нема везивања канала</string>
<string name="word_document">Word документ</string>
<string name="non_quicksy_backup">Quicksy може да врати резервне копије само за quicksy.im налоге</string>
<string name="pref_backup_location">Локација резервних копија</string>
@@ -1157,5 +1157,5 @@
<string name="copy_URI">Копирај URI</string>
<string name="account_status_service_outage_scheduled">Планирана недоступност</string>
<string name="account_status_service_outage_known">Сервис недоступан (познат проблем)</string>
- <string name="sos_scheduled_return">Опоравак сервиса предвиђен у %s</string>
+ <string name="sos_scheduled_return">Опоравак сервиса предвиђен за %s</string>
</resources>
@@ -1156,7 +1156,7 @@
<string name="delete_avatar_message">Бажаєте видалити свій аватар? Деякі клієнти можуть продовжувати відображати копію Вашого аватара з кешу.</string>
<string name="account_status_connection_timeout">Час очікування з\'єднання вичерпано</string>
<string name="retry_with_p2p">Повторити спробу з P2P</string>
- <string name="account_status_channel_binding">Прив\'язка каналу недоступна</string>
+ <string name="account_status_channel_binding">Немає прив\'язки каналу</string>
<string name="word_document">документ Word</string>
<string name="restore_omemo_key">Відновити ключі OMEMO</string>
<string name="non_quicksy_backup">Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im</string>
@@ -1103,7 +1103,7 @@
<string name="show_to_contacts_only">仅对联系人显示</string>
<string name="account_status_connection_timeout">连接超时</string>
<string name="retry_with_p2p">使用 P2P 重试</string>
- <string name="account_status_channel_binding">不支持通道绑定</string>
+ <string name="account_status_channel_binding">无通道绑定</string>
<string name="word_document">Word 文档</string>
<string name="restore_omemo_key">恢复 OMEMO 密钥</string>
<string name="non_quicksy_backup">Quicksy 只能恢复 quicksy.im 账号的备份</string>
@@ -1002,7 +1002,7 @@
<string name="account_state_logged_out">已登出</string>
<string name="unverified_devices">您正在使用未驗證的設備。請掃描您其他設備上的 QR 碼進行驗證,以防止主動中間人攻擊。</string>
<string name="audiobook">有聲書</string>
- <string name="restore_warning_continued">請勿嘗試還原非您自己建立的備份!</string>
+ <string name="restore_warning_continued">僅還原由您親自建立的備份。</string>
<string name="report_spam">報告垃圾訊息</string>
<string name="pref_send_crash_reports">發送崩潰報告</string>
<string name="corresponding_chats_closed">相應的會話已存檔。</string>
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources></resources>
@@ -0,0 +1,339 @@
+package im.conversations.android.xmpp;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNull;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.manager.DiscoManager;
+import im.conversations.android.xml.XmlElementReader;
+import im.conversations.android.xmpp.model.disco.info.InfoQuery;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.ConscryptMode;
+
+@RunWith(RobolectricTestRunner.class)
+@ConscryptMode(ConscryptMode.Mode.OFF)
+public class EntityCapabilitiesTest {
+
+ @Test
+ public void entityCaps() throws IOException {
+ final String xml =
+ """
+ <query xmlns='http://jabber.org/protocol/disco#info'
+ node='http://code.google.com/p/exodus#QgayPKawpkPSDYmwT/WM94uAlu0='>
+ <identity category='client' name='Exodus 0.9.1' type='pc'/>
+ <feature var='http://jabber.org/protocol/caps'/>
+ <feature var='http://jabber.org/protocol/disco#items'/>
+ <feature var='http://jabber.org/protocol/disco#info'/>
+ <feature var='http://jabber.org/protocol/muc'/>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", var);
+ }
+
+ @Test
+ public void entityCapsComplexExample() throws IOException {
+ final String xml =
+ """
+ <query xmlns='http://jabber.org/protocol/disco#info'
+ node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
+ <identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
+ <identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
+ <feature var='http://jabber.org/protocol/caps'/>
+ <feature var='http://jabber.org/protocol/disco#info'/>
+ <feature var='http://jabber.org/protocol/disco#items'/>
+ <feature var='http://jabber.org/protocol/muc'/>
+ <x xmlns='jabber:x:data' type='result'>
+ <field var='FORM_TYPE' type='hidden'>
+ <value>urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field var='ip_version' type='text-multi' >
+ <value>ipv4</value>
+ <value>ipv6</value>
+ </field>
+ <field var='os'>
+ <value>Mac</value>
+ </field>
+ <field var='os_version'>
+ <value>10.5.1</value>
+ </field>
+ <field var='software'>
+ <value>Psi</value>
+ </field>
+ <field var='software_version'>
+ <value>0.11</value>
+ </field>
+ </x>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", var);
+ }
+
+ @Test
+ public void entityCapsOpenFire() throws IOException {
+ final String xml =
+ """
+ <iq type="result" xmlns="jabber:client" to="inputmice3@igniterealtime.org/Conversations.cI4W" from="igniterealtime.org" id="L3xl8X8_kzvx">
+ <query node="https://www.igniterealtime.org/projects/openfire/#Cd91QBSG4JGOCEvRsSz64xeJPMk=" xmlns="http://jabber.org/protocol/disco#info">
+ <identity name="Openfire Server" type="im" category="server" xmlns="http://jabber.org/protocol/disco#info"/>
+ <identity type="pep" category="pubsub" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0#embed-presence-directed" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/caps" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-default" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#purge-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscription-options" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0#embed-message " xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#outcast-affiliation" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="msgoffline" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:register" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#config-node" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-create" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#delete-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:mam:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:mam:1" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:mam:2" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:fulltext:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#persistent-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-and-configure" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-affiliations" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:time" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#manage-subscriptions" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:bookmarks-conversion:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#auto-subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/offline" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish-options" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:carbons:2" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/address" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#collections" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retrieve-subscriptions" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="vcard-temp" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#create-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#get-pending" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:blocking" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-subscribe" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#presence-notifications" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:ping" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:archive:manage" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#filtered-notifications" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:push:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#meta-data" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#multi-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#item-ids" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:roster" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#instant-nodes" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#modify-affiliations" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0#embed-presence-sub" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#publisher-affiliation" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#access-open" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:version" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#retract-items" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:extdisco:1" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:privacy" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:extdisco:2" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/commands" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:last" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:raa:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/pubsub#publish" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:serverinfo:0" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="urn:xmpp:archive:auto" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#info" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="jabber:iq:private" xmlns="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/rsm" xmlns="http://jabber.org/protocol/disco#info"/>
+ <x type="result" xmlns="jabber:x:data">
+ <field var="FORM_TYPE" type="hidden" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field var="os" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">Linux</value>
+ </field>
+ <field var="os_version" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">4.14.355-276.618.amzn2.x86_64 amd64 - Java 17.0.14</value>
+ </field>
+ <field var="software" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">Openfire</value>
+ </field>
+ <field var="software_version" type="text-single" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">5.0.0 Alpha</value>
+ </field>
+ </x>
+ <x type="result" xmlns="jabber:x:data">
+ <field var="FORM_TYPE" type="hidden" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">http://jabber.org/network/serverinfo</value>
+ </field>
+ <field var="admin-addresses" type="list-multi" xmlns="jabber:x:data">
+ <value xmlns="jabber:x:data">xmpp:dwd@dave.cridland.net</value>
+ <value xmlns="jabber:x:data">xmpp:akrherz@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:benjamin@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:benjamin@holyarmy.org</value>
+ <value xmlns="jabber:x:data">xmpp:csh@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:dan.caseley@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:dan.caseley@surevine.com</value>
+ <value xmlns="jabber:x:data">xmpp:flow@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:gdt@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:greg.d.thomas@gmail.com</value>
+ <value xmlns="jabber:x:data">xmpp:guus.der.kinderen@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:guus.der.kinderen@gmail.com</value>
+ <value xmlns="jabber:x:data">xmpp:lg@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">xmpp:rcollier@igniterealtime.org</value>
+ <value xmlns="jabber:x:data">mailto:robincollier@hotmail.com</value>
+ </field>
+ </x>
+ </query>
+</iq>
+""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(Iq.class));
+ final var iq = (Iq) element;
+ final InfoQuery info = iq.getExtension(InfoQuery.class);
+ final String var = EntityCapabilities.hash(info).encoded();
+ Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var);
+ }
+
+ @Test
+ public void caps2() throws IOException {
+ final String xml =
+ """
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" name="BombusMod" type="mobile"/>
+ <feature var="http://jabber.org/protocol/si"/>
+ <feature var="http://jabber.org/protocol/bytestreams"/>
+ <feature var="http://jabber.org/protocol/chatstates"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="jabber:iq:time"/>
+ <feature var="jabber:iq:privacy"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="http://jabber.org/protocol/rosterx"/>
+ <feature var="urn:xmpp:time"/>
+ <feature var="jabber:x:oob"/>
+ <feature var="http://jabber.org/protocol/ibb"/>
+ <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
+ <feature var="urn:xmpp:receipts"/>
+ <feature var="jabber:iq:roster"/>
+ <feature var="jabber:iq:last"/>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities2.hash(info).encoded();
+ Assert.assertEquals("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=", var);
+ }
+
+ @Test
+ public void caps2complex() throws IOException {
+ final String xml =
+ """
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" name="Tkabber" type="pc" xml:lang="en"/>
+ <identity category="client" name="Ткаббер" type="pc" xml:lang="ru"/>
+ <feature var="games:board"/>
+ <feature var="http://jabber.org/protocol/activity"/>
+ <feature var="http://jabber.org/protocol/activity+notify"/>
+ <feature var="http://jabber.org/protocol/bytestreams"/>
+ <feature var="http://jabber.org/protocol/chatstates"/>
+ <feature var="http://jabber.org/protocol/commands"/>
+ <feature var="http://jabber.org/protocol/disco#info"/>
+ <feature var="http://jabber.org/protocol/disco#items"/>
+ <feature var="http://jabber.org/protocol/evil"/>
+ <feature var="http://jabber.org/protocol/feature-neg"/>
+ <feature var="http://jabber.org/protocol/geoloc"/>
+ <feature var="http://jabber.org/protocol/geoloc+notify"/>
+ <feature var="http://jabber.org/protocol/ibb"/>
+ <feature var="http://jabber.org/protocol/iqibb"/>
+ <feature var="http://jabber.org/protocol/mood"/>
+ <feature var="http://jabber.org/protocol/mood+notify"/>
+ <feature var="http://jabber.org/protocol/rosterx"/>
+ <feature var="http://jabber.org/protocol/si"/>
+ <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
+ <feature var="http://jabber.org/protocol/tune"/>
+ <feature var="http://www.facebook.com/xmpp/messages"/>
+ <feature var="http://www.xmpp.org/extensions/xep-0084.html#ns-metadata+notify"/>
+ <feature var="jabber:iq:avatar"/>
+ <feature var="jabber:iq:browse"/>
+ <feature var="jabber:iq:dtcp"/>
+ <feature var="jabber:iq:filexfer"/>
+ <feature var="jabber:iq:ibb"/>
+ <feature var="jabber:iq:inband"/>
+ <feature var="jabber:iq:jidlink"/>
+ <feature var="jabber:iq:last"/>
+ <feature var="jabber:iq:oob"/>
+ <feature var="jabber:iq:privacy"/>
+ <feature var="jabber:iq:roster"/>
+ <feature var="jabber:iq:time"/>
+ <feature var="jabber:iq:version"/>
+ <feature var="jabber:x:data"/>
+ <feature var="jabber:x:event"/>
+ <feature var="jabber:x:oob"/>
+ <feature var="urn:xmpp:avatar:metadata+notify"/>
+ <feature var="urn:xmpp:ping"/>
+ <feature var="urn:xmpp:receipts"/>
+ <feature var="urn:xmpp:time"/>
+ <x xmlns="jabber:x:data" type="result">
+ <field type="hidden" var="FORM_TYPE">
+ <value>urn:xmpp:dataforms:softwareinfo</value>
+ </field>
+ <field var="software">
+ <value>Tkabber</value>
+ </field>
+ <field var="software_version">
+ <value>0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2)</value>
+ </field>
+ <field var="os">
+ <value>Windows</value>
+ </field>
+ <field var="os_version">
+ <value>XP</value>
+ </field>
+ </x>
+ </query>""";
+ final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8));
+ assertThat(element, instanceOf(InfoQuery.class));
+ final InfoQuery info = (InfoQuery) element;
+ final String var = EntityCapabilities2.hash(info).encoded();
+ Assert.assertEquals("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=", var);
+ }
+
+ @Test
+ public void parseCaps2Node() {
+ final var caps =
+ DiscoManager.buildHashFromNode(
+ "urn:xmpp:caps#sha-256.u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=");
+ assertThat(caps, instanceOf(EntityCapabilities2.EntityCaps2Hash.class));
+ }
+
+ @Test
+ public void parseCaps2NodeMissingHash() {
+ final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#sha-256.");
+ assertNull(caps);
+ }
+
+ @Test
+ public void parseCaps2NodeInvalid() {
+ final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#-");
+ assertNull(caps);
+ }
+
+ @Test
+ public void parseCaps2NodeUnknownAlgo() {
+ final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#test.test");
+ assertNull(caps);
+ }
+}