diff --git a/build.gradle b/build.gradle index 6d83ddceaafb700f52c193c8529665e94a805774..c7ae50fd14151c22035d98466da5d76875cd8e76 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/fastlane/metadata/android/gl-ES/changelogs/4214204.txt b/fastlane/metadata/android/gl-ES/changelogs/4214204.txt new file mode 100644 index 0000000000000000000000000000000000000000..527ce685ae76bed83856a020febd6ea6a22438d0 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4214204.txt @@ -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 diff --git a/fastlane/metadata/android/pt-BR/changelogs/379.txt b/fastlane/metadata/android/pt-BR/changelogs/379.txt new file mode 100644 index 0000000000000000000000000000000000000000..6429a13141e1f846935cb33888a10a3fb554a84e --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/379.txt @@ -0,0 +1 @@ +Chamadas de Áudio/Vídeo (Requer suporte do servidor na forma de servidores STUN e TURN descobertos via XEP-0215) diff --git a/fastlane/metadata/android/pt-BR/changelogs/381.txt b/fastlane/metadata/android/pt-BR/changelogs/381.txt new file mode 100644 index 0000000000000000000000000000000000000000..049b99e4e62a5f11fde538d455168bca91023d1c --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/381.txt @@ -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 diff --git a/fastlane/metadata/android/pt-BR/changelogs/42062.txt b/fastlane/metadata/android/pt-BR/changelogs/42062.txt new file mode 100644 index 0000000000000000000000000000000000000000..d239746242890bb4b29b0bfc25f7305c388edbd5 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/42062.txt @@ -0,0 +1 @@ +* Desabilitar a abertura de arquivos de backup (.ceb) pelo gerenciador de arquivos diff --git a/fastlane/metadata/android/pt-BR/changelogs/42065.txt b/fastlane/metadata/android/pt-BR/changelogs/42065.txt new file mode 100644 index 0000000000000000000000000000000000000000..50d69c942f7b14c2f193bd3d02a95d4416c2ee7c --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/42065.txt @@ -0,0 +1 @@ +* Introduzir novo formato de arquivo de backup diff --git a/fastlane/metadata/android/pt-BR/changelogs/4211604.txt b/fastlane/metadata/android/pt-BR/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..8df2f636760626284ec2ff2b269ed82f095d58a1 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4211604.txt @@ -0,0 +1 @@ +* Correções de bugs menores diff --git a/fastlane/metadata/android/pt-BR/changelogs/4211804.txt b/fastlane/metadata/android/pt-BR/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..6585028bdf1002ee96de2283564cd0a328a023fe --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4211804.txt @@ -0,0 +1 @@ +* Adicionar tempo limite para a iniciação da chamada diff --git a/fastlane/metadata/android/pt-BR/changelogs/4212104.txt b/fastlane/metadata/android/pt-BR/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae2419d89ac810372e91029ee045ad5cedd69282 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4212104.txt @@ -0,0 +1 @@ +* Suporte a Reações de Mensagens diff --git a/fastlane/metadata/android/pt-BR/changelogs/4212504.txt b/fastlane/metadata/android/pt-BR/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..6aec1d05f42e06866be479401c3b54e7c398c25f --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4212504.txt @@ -0,0 +1 @@ +* Melhorar o manuseio de algumas reações com emojis diff --git a/fastlane/metadata/android/pt-BR/changelogs/4212704.txt b/fastlane/metadata/android/pt-BR/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..91499dcccdbeb984912eb5720675c0df64a40a90 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4212704.txt @@ -0,0 +1 @@ +* Adicionar a capacidade de exibir as bolhas de mensagem alinhadas à esquerda diff --git a/fastlane/metadata/android/uk/changelogs/4211804.txt b/fastlane/metadata/android/uk/changelogs/4211804.txt index c3490793c919058ab1c1a700cb0fde390e090862..ee26cc2398c2408084f544b35c0d4decd23222e3 100644 --- a/fastlane/metadata/android/uk/changelogs/4211804.txt +++ b/fastlane/metadata/android/uk/changelogs/4211804.txt @@ -1 +1 @@ -* Додано таймаут для ініціювання виклику +* Додано тайм-аут для ініціювання виклику diff --git a/src/conversations/res/values-ga/strings.xml b/src/conversations/res/values-ga/strings.xml index ee21d93fd943365566a149cdeee1e54576395699..91c996957b972fcfbb68429eb76993faf1f03c0d 100644 --- a/src/conversations/res/values-ga/strings.xml +++ b/src/conversations/res/values-ga/strings.xml @@ -3,4 +3,5 @@ Roghnaigh do freastalaí XMPP Bain úsáid as conversations.im Oscail cuntas nua + An bhfuil cuntas XMPP agat? diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 8866550a62c5099a78e39a355ae6f5c93ff06c17..28c3b0075b991e88628c1e1977545c468288507a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -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 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())) { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 7520ed2b5ae341bcaf4919a801d579d0c8ac5d7a..4e7d351e61c2a7a009d96305f85f5ecfefc895cb 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -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(); } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 7c432f545f8e240b8faf87eb80b9f2dd11d984a8..390fc10eeab4827ecbd7d378c356dfbeccaed744 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -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 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 getFeatures() { - return this.serviceDiscoveryResult != null - ? this.serviceDiscoveryResult.features + public Collection getFeatures() { + final var serviceDiscoveryResult = getServiceDiscoveryResult(); + return serviceDiscoveryResult != null + ? serviceDiscoveryResult.getFeatureStrings() : Collections.emptyList(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java deleted file mode 100644 index cabc2a4759b92826f465eef802bf740c847ba448..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/entities/Presence.java +++ /dev/null @@ -1,101 +0,0 @@ -package eu.siacs.conversations.entities; - -import androidx.annotation.NonNull; - -import java.util.Locale; - -import eu.siacs.conversations.xml.Element; - -public class Presence implements Comparable { - - public enum Status { - CHAT, ONLINE, AWAY, XA, DND, OFFLINE; - - public String toShowString() { - switch(this) { - case CHAT: return "chat"; - case AWAY: return "away"; - case XA: return "xa"; - case DND: return "dnd"; - } - return null; - } - - public static Status fromShowString(String show) { - if (show == null) { - return ONLINE; - } else { - switch (show.toLowerCase(Locale.US)) { - case "away": - return AWAY; - case "xa": - return XA; - case "dnd": - return DND; - case "chat": - return CHAT; - default: - return ONLINE; - } - } - } - } - - private final Status status; - private ServiceDiscoveryResult disco; - private final String ver; - private final String hash; - private final String node; - private final String message; - - 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; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java index 958891d34d4b8915fdc3309a4827c37b41001347..0c36a42cf994bc84f0ff51559c2c0fb4450b7e95 100644 --- a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java +++ b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Objects; public class PresenceTemplate extends AbstractEntity { @@ -13,9 +14,9 @@ public class PresenceTemplate extends AbstractEntity { private long lastUsed = 0; private String statusMessage; - private Presence.Status status = Presence.Status.ONLINE; + private Presence.Availability status = Presence.Availability.ONLINE; - public PresenceTemplate(Presence.Status status, String statusMessage) { + public PresenceTemplate(Presence.Availability status, String statusMessage) { this.status = status; this.statusMessage = statusMessage; this.lastUsed = System.currentTimeMillis(); @@ -41,11 +42,11 @@ public class PresenceTemplate extends AbstractEntity { template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED)); template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE)); template.status = - Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))); + Presence.Availability.valueOfShown(cursor.getString(cursor.getColumnIndex(STATUS))); return template; } - public Presence.Status getStatus() { + public Presence.Availability getStatus() { return status; } diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index eabc98f78b6963928470dc21661e18cb3f31fc19..c955b71d8b93be06aae55d6c84305995f96f8f1d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -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 presences = new Hashtable<>(); + private final HashMap presences = new HashMap<>(); + private final Contact contact; + + public Presences(final Contact contact) { + this.contact = contact; + } private static String nameWithoutVersion(String name) { String[] parts = name.split(" "); @@ -63,18 +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 asTemplates() { synchronized (this.presences) { ArrayList templates = new ArrayList<>(presences.size()); - for (Presence p : presences.values()) { - if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) { - templates.add(new PresenceTemplate(p.getStatus(), p.getMessage())); + for (Presence presence : this.presences.values()) { + String message = Strings.nullToEmpty(presence.getStatus()).trim(); + if (Strings.isNullOrEmpty(message)) { + continue; } + templates.add(new PresenceTemplate(presence.getAvailability(), message)); } return templates; } @@ -115,24 +129,46 @@ public class Presences { } } - public List getStatusMessages() { - ArrayList messages = new ArrayList<>(); + public Set getStatusMessages() { + Set messages = new HashSet<>(); synchronized (this.presences) { for (Presence presence : this.presences.values()) { - String message = presence.getMessage() == null ? null : presence.getMessage().trim(); - if (message != null && !message.isEmpty() && !messages.contains(message)) { - messages.add(message); + String message = Strings.nullToEmpty(presence.getStatus()).trim(); + if (Strings.isNullOrEmpty(message)) { + continue; } + messages.add(message); } } return messages; } + public Set getFullJids() { + final Set 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 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> toTypeAndNameMap() { Map typeMap = new HashMap<>(); Map nameMap = new HashMap<>(); + final var connection = this.contact.getAccount().getXmppConnection(); + if (connection == null) { + return new Pair<>(typeMap, nameMap); + } synchronized (this.presences) { - for (Map.Entry presenceEntry : this.presences.entrySet()) { - String resource = presenceEntry.getKey(); - Presence presence = presenceEntry.getValue(); - ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult(); - if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) { - ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0); + for (final String resource : this.presences.keySet()) { + final var serviceDiscoveryResult = + connection + .getManager(DiscoManager.class) + .get( + Strings.isNullOrEmpty(resource) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(resource)); + if (serviceDiscoveryResult != null + && !serviceDiscoveryResult.getIdentities().isEmpty()) { + final Identity identity = + Iterables.getFirst(serviceDiscoveryResult.getIdentities(), null); String type = identity.getType(); - String name = identity.getName(); + String name = identity.getIdentityName(); if (type != null) { typeMap.put(resource, type); } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java deleted file mode 100644 index 1c0e6410bf79bb4f431bc4525ee98a256ab210e7..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ /dev/null @@ -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 features; - protected final List forms; - private final List identities; - - public ServiceDiscoveryResult(final Iq packet) { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = "sha-1"; // We only support sha-1 for now - - final List elements = packet.query().getChildren(); - - for (final Element element : elements) { - if (element.getName().equals("identity")) { - Identity id = new Identity(element); - if (id.getType() != null && id.getCategory() != null) { - identities.add(id); - } - } else if (element.getName().equals("feature")) { - if (element.getAttribute("var") != null) { - features.add(element.getAttribute("var")); - } - } else if (element.getName().equals("x") - && element.getAttribute("xmlns").equals(Namespace.DATA)) { - forms.add(Data.parse(element)); - } - } - this.ver = this.mkCapHash(); - } - - private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = hash; - this.ver = ver; - - JSONArray identities = o.optJSONArray("identities"); - if (identities != null) { - for (int i = 0; i < identities.length(); i++) { - this.identities.add(new Identity(identities.getJSONObject(i))); - } - } - JSONArray features = o.optJSONArray("features"); - if (features != null) { - for (int i = 0; i < features.length(); i++) { - this.features.add(features.getString(i)); - } - } - JSONArray forms = o.optJSONArray("forms"); - if (forms != null) { - for (int i = 0; i < forms.length(); i++) { - this.forms.add(createFormFromJSONObject(forms.getJSONObject(i))); - } - } - } - - private ServiceDiscoveryResult() { - this.hash = "sha-1"; - this.features = Collections.emptyList(); - this.identities = Collections.emptyList(); - this.ver = null; - this.forms = Collections.emptyList(); - } - - public static ServiceDiscoveryResult empty() { - return new ServiceDiscoveryResult(); - } - - public ServiceDiscoveryResult(Cursor cursor) throws JSONException { - this( - cursor.getString(cursor.getColumnIndexOrThrow(HASH)), - Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT), - new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT)))); - } - - private static String clean(String s) { - return s.replace("<", "<"); - } - - private static String blankNull(String s) { - return s == null ? "" : clean(s); - } - - private static Data createFormFromJSONObject(JSONObject o) { - Data data = new Data(); - JSONArray names = o.names(); - for (int i = 0; i < names.length(); ++i) { - try { - String name = names.getString(i); - JSONArray jsonValues = o.getJSONArray(name); - ArrayList values = new ArrayList<>(jsonValues.length()); - for (int j = 0; j < jsonValues.length(); ++j) { - values.add(jsonValues.getString(j)); - } - data.put(name, values); - } catch (Exception e) { - e.printStackTrace(); - } - } - return data; - } - - private static JSONObject createJSONFromForm(Data data) { - JSONObject object = new JSONObject(); - for (Field field : data.getFields()) { - try { - JSONArray jsonValues = new JSONArray(); - for (String value : field.getValues()) { - jsonValues.put(value); - } - object.put(field.getFieldName(), jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - } - try { - JSONArray jsonValues = new JSONArray(); - jsonValues.put(data.getFormType()); - object.put(Data.FORM_TYPE, jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - return object; - } - - public 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 getIdentities() { - return this.identities; - } - - public List 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 identities = this.getIdentities(); - Collections.sort(identities); - - for (Identity id : identities) { - s.append(blankNull(id.getCategory())) - .append("/") - .append(blankNull(id.getType())) - .append("/") - .append(blankNull(id.getLang())) - .append("/") - .append(blankNull(id.getName())) - .append("<"); - } - - final List features = this.getFeatures(); - Collections.sort(features); - for (final String feature : features) { - s.append(clean(feature)).append("<"); - } - - Collections.sort(forms, Comparator.comparing(Data::getFormType)); - for (final Data form : forms) { - s.append(clean(form.getFormType())).append("<"); - final List fields = form.getFields(); - Collections.sort( - fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName()))); - for (final Field field : fields) { - s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); - final List values = field.getValues(); - Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull)); - for (final String value : values) { - s.append(blankNull(value)).append("<"); - } - } - } - - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - - return md.digest(s.toString().getBytes(StandardCharsets.UTF_8)); - } - - private JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - - JSONArray ids = new JSONArray(); - for (Identity id : this.getIdentities()) { - ids.put(id.toJSON()); - } - o.put("identities", ids); - - o.put("features", new JSONArray(this.getFeatures())); - - JSONArray forms = new JSONArray(); - for (Data data : this.forms) { - forms.put(createJSONFromForm(data)); - } - o.put("forms", forms); - - return o; - } catch (JSONException e) { - return null; - } - } - - public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(HASH, this.hash); - values.put(VER, getVer()); - JSONObject jsonObject = toJSON(); - values.put(RESULT, jsonObject == null ? "" : jsonObject.toString()); - return values; - } - - public static class Identity implements Comparable { - protected final String type; - protected final String lang; - protected final String name; - final String category; - - Identity(final String category, final String type, final String lang, final String name) { - this.category = category; - this.type = type; - this.lang = lang; - this.name = name; - } - - Identity(final Element el) { - this( - el.getAttribute("category"), - el.getAttribute("type"), - el.getAttribute("xml:lang"), - el.getAttribute("name")); - } - - Identity(final JSONObject o) { - - this( - o.optString("category", null), - o.optString("type", null), - o.optString("lang", null), - o.optString("name", null)); - } - - public String getCategory() { - return this.category; - } - - public String getType() { - return this.type; - } - - public String getLang() { - return this.lang; - } - - public String getName() { - return this.name; - } - - JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - o.put("category", this.getCategory()); - o.put("type", this.getType()); - o.put("lang", this.getLang()); - o.put("name", this.getName()); - return o; - } catch (JSONException e) { - return null; - } - } - - @Override - public int compareTo(final Identity o) { - int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory())); - if (r == 0) { - r = blankNull(this.getType()).compareTo(blankNull(o.getType())); - } - if (r == 0) { - r = blankNull(this.getLang()).compareTo(blankNull(o.getLang())); - } - if (r == 0) { - r = blankNull(this.getName()).compareTo(blankNull(o.getName())); - } - - return r; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index ca166be4bc6f6338a6fb1659f0532b32ea169dd7..f10eb1619a75e39ae5de2d66bade46062c2ddfb1 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -1,14 +1,13 @@ package eu.siacs.conversations.generator; import android.text.TextUtils; - import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.stanza.Presence; public class PresenceGenerator extends AbstractGenerator { @@ -16,20 +15,25 @@ public class PresenceGenerator extends AbstractGenerator { super(service); } - private im.conversations.android.xmpp.model.stanza.Presence subscription(String type, Contact contact) { - im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); + private im.conversations.android.xmpp.model.stanza.Presence subscription( + String type, Contact contact) { + im.conversations.android.xmpp.model.stanza.Presence packet = + new im.conversations.android.xmpp.model.stanza.Presence(); packet.setAttribute("type", type); packet.setTo(contact.getJid()); packet.setFrom(contact.getAccount().getJid().asBareJid()); return packet; } - public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom( + final Contact contact) { return requestPresenceUpdatesFrom(contact, null); } - public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { - im.conversations.android.xmpp.model.stanza.Presence packet = subscription("subscribe", contact); + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom( + final Contact contact, final String preAuth) { + im.conversations.android.xmpp.model.stanza.Presence packet = + subscription("subscribe", contact); String displayName = contact.getAccount().getDisplayName(); if (!TextUtils.isEmpty(displayName)) { packet.addChild("nick", Namespace.NICK).setContent(displayName); @@ -40,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; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index af7e9976049b75f8713ebf5ad24020cfd830f04f..4c93d7b0cd5269f66f9b144c5dc3bb788c9e2a75 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -1,7 +1,12 @@ package eu.siacs.conversations.parser; import android.util.Log; +import androidx.annotation.NonNull; import com.google.common.base.Strings; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -10,7 +15,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.services.XmppConnectionService; @@ -18,10 +22,13 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.occupant.OccupantId; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import org.openintents.openpgp.util.OpenPgpUtils; @@ -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 discoFuture) { + Futures.addCallback( + discoFuture, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) {} + + @Override + public void onFailure(@NonNull Throwable throwable) { + if (throwable instanceof TimeoutException) { + return; + } + Log.d(Config.LOGTAG, "could not retrieve disco from " + from, throwable); + } + }, + MoreExecutors.directExecutor()); + } + @Override public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { if (packet.hasChild("x", Namespace.MUC_USER)) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index d4736e5888d3a273a54f8dc11f2979e1ca883ef4..bf1412faa267f72c40906898c500945864d24dd1 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index dcf388cea3d72656801440995d0508bd34a96c33..db05a7a877e7a746551b6d0d72a09bcfe887e6b6 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -19,6 +19,7 @@ import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.stanza.Message; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -55,7 +56,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - private static Version get(List features) { + private static Version get(final Collection features) { final Version[] values = values(); for (int i = values.length - 1; i >= 0; --i) { for (String feature : features) { @@ -67,7 +68,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return MAM_0; } - public static boolean has(List features) { + public static boolean has(final Collection features) { for (String feature : features) { for (Version version : values()) { if (version.namespace.equals(feature)) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index add704c44e8e9b737d5713918894751c3f83630f..d9a63d1894418bcfcde930b3853e57dd2e5a1536 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -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 FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private final LruCache, ServiceDiscoveryResult> discoCache = - new LruCache<>(20); private final OnStatusChanged statusListener = new OnStatusChanged() { @@ -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() { + @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 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 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 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); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index f63583a83297946e06eeceb6341aedc30325f816..f061897fa5c983abeb241ab5366fda635db6d6a0 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -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 statusMessages = contact.getPresences().getStatusMessages(); - if (statusMessages.size() == 0) { + Collection statusMessages = contact.getPresences().getStatusMessages(); + if (statusMessages.isEmpty()) { binding.statusMessage.setVisibility(View.GONE); } else if (statusMessages.size() == 1) { - final String message = statusMessages.get(0); + final String message = Iterables.getOnlyElement(statusMessages); binding.statusMessage.setVisibility(View.VISIBLE); final Spannable span = new SpannableString(message); if (Emoticons.isOnlyEmoji(message)) { @@ -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 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(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 851068d14fdab6aca46975cb830e582ed254a55f..6f1946a0bb0cd60c76f77b2e38df2ed16bb67766 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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> presences = conversation.getContact().getPresences().getPresencesMap().entrySet(); - if (presences.isEmpty()) { - presences = new HashSet<>(); - presences.add(new AbstractMap.SimpleEntry("", null)); - } - for (Map.Entry 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))); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index c6f829d22d0cac32b6d87909d8e4851e4493d498..c372af83e511cd2dd8b5676a2668c0b28889025c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -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; } } diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index ad16e5dc7ad7e74d74ce9ffb351abcf8e762e90a..7545bc827d1bd229cac5f2672fafc672f06295b8 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -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> 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 getTypes(Contact gateway) { List 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> getSelected() { + public Pair> getSelected() { if(this.selected == 0) { return null; // No gateway, just use direct JID entry } Pair gateway = this.gateways.get(this.selected - 1); - - Pair presence = null; - for (Map.Entry 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 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); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index bcc43159e4d60e2b4565cd575ff8ce6609eeebcc..3b53e7f3a0c17ea16de68493fc1e87da6e0b6230 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -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)); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index efdfa82db3bb4c4a3ee25792d9468e9bd625c672..8f2baac3f97072e346947acc1998cfe5fe9402fa 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -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 { - protected XmppActivity activity; - private boolean showDynamicTags = false; - private OnTagClickedListener mOnTagClickedListener = null; - private final View.OnClickListener onTagTvClick = view -> { - if (view instanceof TextView tv && mOnTagClickedListener != null) { - final String tag = tv.getText().toString(); - mOnTagClickedListener.onTagClicked(tag); - } - }; - - public ListItemAdapter(XmppActivity activity, List objects) { - super(activity, 0, objects); - this.activity = activity; - } - - - public void refreshSettings() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, 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 tags = item.getTags(activity); - final boolean hasMetaTags; - if (item instanceof Contact contact) { - hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE; - } else { - hasMetaTags = false; - } - if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) { - viewHolder.tags.setVisibility(View.GONE); - } else { - viewHolder.tags.setVisibility(View.VISIBLE); - viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1); - final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); - for (final ListItem.Tag tag : tags) { - final String name = tag.getName(); - final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); - tv.setText(name); - tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(name)))); - tv.setOnClickListener(this.onTagTvClick); - final int id = ViewCompat.generateViewId(); - tv.setId(id); - viewIdBuilder.add(id); - viewHolder.tags.addView(tv); - } - if (item instanceof Contact contact) { - if (contact.isBlocked()) { - final TextView tv = - (TextView) - inflater.inflate( - R.layout.item_tag, viewHolder.tags, false); - tv.setText(R.string.blocked); - tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(),ContextCompat.getColor(tv.getContext(),R.color.gray_800)))); - final int id = ViewCompat.generateViewId(); - tv.setId(id); - viewIdBuilder.add(id); - viewHolder.tags.addView(tv); - } else { - final Presence.Status status = contact.getShownStatus(); - if (status != Presence.Status.OFFLINE) { + protected XmppActivity activity; + private boolean showDynamicTags = false; + private OnTagClickedListener mOnTagClickedListener = null; + private final View.OnClickListener onTagTvClick = + view -> { + if (view instanceof TextView tv && mOnTagClickedListener != null) { + final String tag = tv.getText().toString(); + mOnTagClickedListener.onTagClicked(tag); + } + }; + + public ListItemAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + } + + public void refreshSettings() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false); + } + + @NonNull + @Override + public View getView(int position, View view, @NonNull ViewGroup parent) { + LayoutInflater inflater = activity.getLayoutInflater(); + final ListItem item = getItem(position); + ViewHolder viewHolder; + if (view == null) { + final ItemContactBinding binding = + DataBindingUtil.inflate(inflater, R.layout.item_contact, parent, false); + viewHolder = ViewHolder.get(binding); + view = binding.getRoot(); + } else { + viewHolder = (ViewHolder) view.getTag(); + } + if (view.isActivated()) { + Log.d(Config.LOGTAG, "item " + item.getDisplayName() + " is activated"); + } + // view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background)); + final List tags = item.getTags(activity); + final boolean hasMetaTags; + if (item instanceof Contact contact) { + hasMetaTags = + contact.isBlocked() + || contact.getShownStatus() != Presence.Availability.OFFLINE; + } else { + hasMetaTags = false; + } + if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) { + viewHolder.tags.setVisibility(View.GONE); + } else { + viewHolder.tags.setVisibility(View.VISIBLE); + viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1); + final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); + for (final ListItem.Tag tag : tags) { + final String name = tag.getName(); + final TextView tv = + (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); + tv.setText(name); + tv.setBackgroundTintList( + ColorStateList.valueOf( + MaterialColors.harmonizeWithPrimary( + getContext(), XEP0392Helper.rgbFromNick(name)))); + tv.setOnClickListener(this.onTagTvClick); + final int id = ViewCompat.generateViewId(); + tv.setId(id); + viewIdBuilder.add(id); + viewHolder.tags.addView(tv); + } + if (item instanceof Contact contact) { + if (contact.isBlocked()) { + final TextView tv = + (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); + tv.setText(R.string.blocked); + tv.setBackgroundTintList( + ColorStateList.valueOf( + MaterialColors.harmonizeWithPrimary( + tv.getContext(), + ContextCompat.getColor( + tv.getContext(), R.color.gray_800)))); + final int id = ViewCompat.generateViewId(); + tv.setId(id); + viewIdBuilder.add(id); + viewHolder.tags.addView(tv); + } else { + final Presence.Availability status = contact.getShownStatus(); + if (status != Presence.Availability.OFFLINE) { final TextView tv = (TextView) - inflater.inflate( - R.layout.item_tag, viewHolder.tags, false); - UIHelper.setStatus(tv, status); - final int id = ViewCompat.generateViewId(); - tv.setId(id); - viewIdBuilder.add(id); - viewHolder.tags.addView(tv); + inflater.inflate(R.layout.item_tag, viewHolder.tags, false); + UIHelper.setStatus(tv, status); + final int id = ViewCompat.generateViewId(); + tv.setId(id); + viewIdBuilder.add(id); + viewHolder.tags.addView(tv); } } - } - viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build())); - } - final Jid jid = item.getJid(); - if (jid != null) { - viewHolder.jid.setVisibility(View.VISIBLE); - viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid)); - } else { - viewHolder.jid.setVisibility(View.GONE); - } - viewHolder.name.setText(item.getDisplayName()); - AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); - return view; - } - - public void setOnTagClickedListener(OnTagClickedListener listener) { - this.mOnTagClickedListener = listener; - } - - - public interface OnTagClickedListener { - void onTagClicked(String tag); - } - - private static class ViewHolder { - private TextView name; - private TextView jid; - private ImageView avatar; - private 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; + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java index ae7209fa36370ab68ab37785d7a7248db0febc9e..d1e1abfd045aea59526efa342a76a0a57e035a2b 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java +++ b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java @@ -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)); }; } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 56c1a1d426be37863ccf38f2a4b75c08ada71af9..09b4ac2e9039eade6d72102c079c07d74252e1d3 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java b/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java deleted file mode 100644 index cce6fc163c6b88849335d7fe048346c50f3b0ccd..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.siacs.conversations.xml; - -import com.google.common.io.ByteSource; - -import java.io.IOException; -import java.io.InputStream; - -public class XmlElementReader { - - public static Element read(byte[] bytes) throws IOException { - return read(ByteSource.wrap(bytes).openStream()); - } - - public static Element read(InputStream inputStream) throws IOException { - final XmlReader xmlReader = new XmlReader(); - xmlReader.setInputStream(inputStream); - return xmlReader.readElement(xmlReader.readTag()); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d224b02ed2e17cc62ded1c690feed7ef8dadd9a8..b322fb51e89401c21fd5e48cbb8138ad2548c563 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -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 disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); private final SparseArray mStanzaQueue = new SparseArray<>(); private final Hashtable, 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 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() + .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 result) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": advanced stream future done"); + enableAdvancedStreamFeatures(); + } + + @Override + public void onFailure(@Nullable Throwable throwable) { + Log.d( + Config.LOGTAG, + "could not fetch disco for advanced stream features", + throwable); + } + }, + MoreExecutors.directExecutor()); + + if (mWaitForDisco.get()) { + final ListenableFuture discoComplete = + Futures.whenAllComplete(serverInfoFuture, accountInfoFuture, itemsFuture) + .call(() -> null, MoreExecutors.directExecutor()); + Futures.addCallback( + discoComplete, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + if (timeout(serverInfoFuture, accountInfoFuture, itemsFuture)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": reached timeout while waiting for disco"); + return; + } + if (mWaitForDisco.compareAndSet(true, false)) { + finalizeBindOrError(); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": disco complete but bind was already" + + " finalized"); + } + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "error in disco: ", t); + } + }, + MoreExecutors.directExecutor()); + } else { + finalizeBind(); } if (!mWaitForDisco.get()) { @@ -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 commands = new HashMap<>(); - for (final Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - final String node = child.getAttribute("node"); - final Jid jid = child.getAttributeAsJid("jid"); - if (node != null && jid != null) { - commands.put(node, jid); - } - } - } - synchronized (this.commands) { - this.commands.clear(); - this.commands.putAll(commands); + final var future = + getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain())); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Map result) { + synchronized (XmppConnection.this.commands) { + XmppConnection.this.commands.clear(); + XmppConnection.this.commands.putAll(result); } } - }); + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not fetch commands", + throwable); + } + }, + MoreExecutors.directExecutor()); } public boolean isMamPreferenceAlways() { return isMamPreferenceAlways; } + private void finalizeBindOrError() { + try { + finalizeBind(); + } catch (final Exception e) { + throw new Error(e); + } + } + private void finalizeBind() { this.offlineMessagesRetrieved = false; this.bindListener.run(); @@ -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 items = new HashSet<>(); - final List elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = - Jid.Invalid.getNullForInvalid( - element.getAttributeAsJid("jid")); - if (jid != null && !jid.equals(account.getDomain())) { - items.add(jid); - } - } - } - for (Jid jid : items) { - sendServiceDiscoveryInfo(jid); - } - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not query disco items of " - + server); - } - if (packet.getType() != Iq.Type.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); - } - private void sendEnableCarbons() { final Iq iq = new Iq(Iq.Type.SET); iq.addChild("enable", Namespace.CARBONS); @@ -2820,28 +2810,23 @@ public class XmppConnection implements Runnable { this.boundStreamFeatures = null; } - private List> findDiscoItemsByFeature(final String feature) { - synchronized (this.disco) { - final List> items = new ArrayList<>(); - for (final Entry cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - items.add(cursor); - } - } - return items; - } + public M getManager(final Class clazz) { + return this.managers.getInstance(clazz); } - public Entry getServiceDiscoveryResultByFeature( - final String feature) { - synchronized (this.disco) { - for (final var cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - return cursor; - } + private List> findDiscoItemsByFeature(final String feature) { + final List> items = new ArrayList<>(); + for (final Entry cursor : + getManager(DiscoManager.class).getServerItems().entrySet()) { + if (cursor.getValue().getFeatureStrings().contains(feature)) { + items.add(cursor); } - return null; } + return items; + } + + public Entry getServiceDiscoveryResultByFeature(final String feature) { + return Iterables.getFirst(findDiscoItemsByFeature(feature), null); } public Jid findDiscoItemByFeature(final String feature) { @@ -2869,15 +2854,14 @@ public class XmppConnection implements Runnable { public List getMucServers() { List servers = new ArrayList<>(); - synchronized (this.disco) { - for (final Entry cursor : disco.entrySet()) { - final ServiceDiscoveryResult value = cursor.getValue(); - if (value.getFeatures().contains("http://jabber.org/protocol/muc") - && value.hasIdentity("conference", "text") - && !value.getFeatures().contains("jabber:iq:gateway") - && !value.hasIdentity("conference", "irc")) { - servers.add(cursor.getKey().toString()); - } + for (final Entry entry : + getManager(DiscoManager.class).getServerItems().entrySet()) { + final var value = entry.getValue(); + if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc") + && value.hasIdentityWithCategoryAndType("conference", "text") + && !value.getFeatureStrings().contains("jabber:iq:gateway") + && !value.hasIdentityWithCategoryAndType("conference", "irc")) { + servers.add(entry.getKey().toString()); } } return servers; @@ -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 getManager(final Class 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 getAccountFeatures() { - ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid()); - return result == null ? Collections.emptyList() : result.getFeatures(); + public Collection getAccountFeatures() { + final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid()); + return infoQuery == null ? Collections.emptyList() : infoQuery.getFeatureStrings(); } public boolean push() { @@ -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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index de325534e3a5c45f9bc6acc4aa0a3b8a985aed82..9a80ecabec409f7a9ccd053eb81b5f05a2b61a3a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -5,18 +5,16 @@ import androidx.annotation.NonNull; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.jingle.Jingle; import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Arrays; @@ -331,14 +329,15 @@ public abstract class AbstractJingleConnection { } protected boolean remoteHasFeature(final String feature) { - final Contact contact = id.getContact(); - final Presence presence = - contact.getPresences().get(Strings.nullToEmpty(id.with.getResource())); - final ServiceDiscoveryResult serviceDiscoveryResult = - presence == null ? null : presence.getServiceDiscoveryResult(); - final List features = - serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures(); - return features != null && features.contains(feature); + final var connection = id.account.getXmppConnection(); + if (connection == null) { + return false; + } + final var infoQuery = connection.getManager(DiscoManager.class).get(id.with); + if (infoQuery == null) { + return false; + } + return infoQuery.hasFeature(feature); } public static class Id { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java index ede754f6153d129bff69156a2bfa61430eb236b4..7fb2ed088bc24f2fec8fce266ddd29cc28a9f15a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java @@ -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 BASIC_RTP_REQUIREMENTS = Arrays.asList( - Namespace.JINGLE, - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS - ); - private static final Collection VIDEO_REQUIREMENTS = Arrays.asList( - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO - ); + private static final List BASIC_RTP_REQUIREMENTS = + Arrays.asList( + Namespace.JINGLE, + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS); + private static final Collection VIDEO_REQUIREMENTS = + Arrays.asList(Namespace.JINGLE_FEATURE_AUDIO, Namespace.JINGLE_FEATURE_VIDEO); - public static Capability check(final Presence presence) { - final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - final Set features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures()); + public static Capability check(final InfoQuery infoQuery) { + final Set features = + infoQuery == null + ? Collections.emptySet() + : ImmutableSet.copyOf(infoQuery.getFeatureStrings()); if (features.containsAll(BASIC_RTP_REQUIREMENTS)) { if (features.containsAll(VIDEO_REQUIREMENTS)) { return Capability.VIDEO; @@ -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 resources = new ArrayList<>(); - for (final Map.Entry presence : presences.getPresencesMap().entrySet()) { - final Capability capability = check(presence.getValue()); + for (final String resource : presences.getPresencesMap().keySet()) { + final var jid = + Strings.isNullOrEmpty(resource) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(resource); + final Capability capability = check(connection.getManager(DiscoManager.class).get(jid)); if (capability == Capability.NONE) { continue; } if (required == Capability.AUDIO || capability == required) { - resources.add(presence.getKey()); + resources.add(resource); } } return resources.toArray(new String[0]); @@ -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 { } } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 573f969ad9c824d1e33d4aad18833fceddf5d9f6..20609f943694a7da6d864fa4c12caaaf32f20d1b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -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; diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java new file mode 100644 index 0000000000000000000000000000000000000000..ece2d6a1b1fe1f800423567ada18795ee90d406c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java @@ -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); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java new file mode 100644 index 0000000000000000000000000000000000000000..bc8b957f93a29e8c4825cfc9f7b09aba0ee97ae5 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -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 entityInformation = new HashMap<>(); + private final Map> 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 infoOrCache( + final Entity entity, + final im.conversations.android.xmpp.model.capabilties.EntityCapabilities.NodeHash + nodeHash) { + if (nodeHash == null) { + return infoOrCache(entity, null, null); + } + return infoOrCache(entity, nodeHash.node, nodeHash.hash); + } + + public ListenableFuture infoOrCache( + final Entity entity, final String node, final EntityCapabilities.Hash hash) { + final var cached = getDatabase().getInfoQuery(hash); + if (cached != null) { + if (node == null || hash != null) { + this.put(entity.address, cached); + } + return Futures.immediateFuture(null); + } + return Futures.transform( + info(entity, node, hash), f -> null, MoreExecutors.directExecutor()); + } + + public ListenableFuture info( + @NonNull final Entity entity, @Nullable final String node) { + return info(entity, node, null); + } + + public ListenableFuture info( + final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) { + final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node; + final var iqRequest = new Iq(Iq.Type.GET); + iqRequest.setTo(entity.address); + final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery()); + if (requestNode != null) { + infoQueryRequest.setNode(requestNode); + } + final var future = connection.sendIqPacket(iqRequest); + return Futures.transform( + future, + iqResult -> { + final var infoQuery = iqResult.getExtension(InfoQuery.class); + if (infoQuery == null) { + throw new IllegalStateException("Response did not have query child"); + } + if (!Objects.equals(requestNode, infoQuery.getNode())) { + throw new IllegalStateException( + "Node in response did not match node in request"); + } + + if (node == null + || (hash != null + && hash.capabilityNode(node).equals(infoQuery.getNode()))) { + // only storing results w/o nodes + this.put(entity.address, infoQuery); + } + + final var caps = EntityCapabilities.hash(infoQuery); + final var caps2 = EntityCapabilities2.hash(infoQuery); + if (hash instanceof EntityCapabilities.EntityCapsHash) { + checkMatch( + (EntityCapabilities.EntityCapsHash) hash, + caps, + EntityCapabilities.EntityCapsHash.class); + } + if (hash instanceof EntityCapabilities2.EntityCaps2Hash) { + checkMatch( + (EntityCapabilities2.EntityCaps2Hash) hash, + caps2, + EntityCapabilities2.EntityCaps2Hash.class); + } + // we want to avoid caching disco info for entities that put variable data (like + // number of occupants in a MUC) into it + final boolean cache = + Objects.nonNull(hash) + || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES) + || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES_2); + + if (cache) { + getDatabase().insertCapsCache(caps, caps2, infoQuery); + } + + return infoQuery; + }, + MoreExecutors.directExecutor()); + } + + private void checkMatch( + final H expected, final H was, final Class clazz) { + if (Arrays.equals(expected.hash, was.hash)) { + return; + } + throw new CapsHashMismatchException( + String.format( + "%s mismatch. Expected %s was %s", + clazz.getSimpleName(), + BaseEncoding.base64().encode(expected.hash), + BaseEncoding.base64().encode(was.hash))); + } + + public ListenableFuture> items(final Entity.DiscoItem entity) { + return items(entity, null); + } + + public ListenableFuture> items( + final Entity.DiscoItem entity, @Nullable final String node) { + final var requestNode = Strings.emptyToNull(node); + final var iqPacket = new Iq(Iq.Type.GET); + iqPacket.setTo(entity.address); + final ItemsQuery itemsQueryRequest = iqPacket.addExtension(new ItemsQuery()); + if (requestNode != null) { + itemsQueryRequest.setNode(requestNode); + } + final var future = connection.sendIqPacket(iqPacket); + return Futures.transform( + future, + iqResult -> { + final var itemsQuery = iqResult.getExtension(ItemsQuery.class); + if (itemsQuery == null) { + throw new IllegalStateException(); + } + if (!Objects.equals(requestNode, itemsQuery.getNode())) { + throw new IllegalStateException( + "Node in response did not match node in request"); + } + final var items = itemsQuery.getExtensions(Item.class); + + final var validItems = + Collections2.filter(items, i -> Objects.nonNull(i.getJid())); + + final var itemsAsAddresses = + ImmutableSet.copyOf(Collections2.transform(validItems, Item::getJid)); + if (node == null) { + this.discoItems.put(entity.address, itemsAsAddresses); + } + return validItems; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture> itemsWithInfo(final Entity.DiscoItem entity) { + final var itemsFutures = items(entity); + final var filtered = + Futures.transform( + itemsFutures, + items -> + Collections2.filter( + items, + i -> + i.getNode() == null + && !entity.address.equals(i.getJid())), + MoreExecutors.directExecutor()); + return Futures.transformAsync( + filtered, + items -> { + Collection> infoFutures = + Collections2.transform( + items, i -> info(Entity.discoItem(i.getJid()), i.getNode())); + return Futures.allAsList(infoFutures); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture> commands(final Entity.DiscoItem entity) { + final var itemsFuture = items(entity, Namespace.COMMANDS); + return Futures.transform( + itemsFuture, + items -> { + final var builder = new ImmutableMap.Builder(); + for (final var item : items) { + final var jid = item.getJid(); + final var node = item.getNode(); + if (Jid.Invalid.isValid(jid) && node != null) { + builder.put(node, jid); + } + } + return builder.buildKeepingLast(); + }, + MoreExecutors.directExecutor()); + } + + public Map getServerItems() { + final var builder = new ImmutableMap.Builder(); + final var domain = connection.getAccount().getDomain(); + final var domainInfoQuery = get(domain); + if (domainInfoQuery != null) { + builder.put(domain, domainInfoQuery); + } + final var items = this.discoItems.get(domain); + if (items == null) { + return builder.build(); + } + for (final var item : items) { + final var infoQuery = get(item); + if (infoQuery == null) { + continue; + } + builder.put(item, infoQuery); + } + return builder.buildKeepingLast(); + } + + private void put(final Jid address, final InfoQuery infoQuery) { + synchronized (this.entityInformation) { + this.entityInformation.put(address, infoQuery); + } + 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); + } + } +} diff --git a/src/main/java/im/conversations/android/xml/XmlElementReader.java b/src/main/java/im/conversations/android/xml/XmlElementReader.java new file mode 100644 index 0000000000000000000000000000000000000000..d7c0a0424014066e5f1473a2eac1282c06415a74 --- /dev/null +++ b/src/main/java/im/conversations/android/xml/XmlElementReader.java @@ -0,0 +1,21 @@ +package im.conversations.android.xml; + +import com.google.common.io.ByteSource; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.XmlReader; +import java.io.IOException; +import java.io.InputStream; + +public class XmlElementReader { + + public static Element read(byte[] bytes) throws IOException { + return read(ByteSource.wrap(bytes).openStream()); + } + + public static Element read(final InputStream inputStream) throws IOException { + try (final XmlReader xmlReader = new XmlReader()) { + xmlReader.setInputStream(inputStream); + return xmlReader.readElement(xmlReader.readTag()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Entity.java b/src/main/java/im/conversations/android/xmpp/Entity.java index a578d250780e40c8b5a588a74f1ea43496f2c515..799d7a5eadd99032c3690fe318370540de1497cf 100644 --- a/src/main/java/im/conversations/android/xmpp/Entity.java +++ b/src/main/java/im/conversations/android/xmpp/Entity.java @@ -1,6 +1,6 @@ package im.conversations.android.xmpp; -import org.jxmpp.jid.Jid; +import eu.siacs.conversations.xmpp.Jid; public abstract class Entity { diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java index 1d8a35a68d2d5822c2d0ada756b91e473bb698e9..d891870aa41af45d96daaf6ec8ceaf89d0635689 100644 --- a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -8,7 +8,6 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.io.BaseEncoding; import com.google.common.primitives.Bytes; - import eu.siacs.conversations.xml.Namespace; import im.conversations.android.xmpp.model.Hash; import im.conversations.android.xmpp.model.data.Data; @@ -42,16 +41,12 @@ public class EntityCapabilities2 { } private static HashFunction toHashFunction(final Hash.Algorithm algorithm) { - switch (algorithm) { - case SHA_1: - return Hashing.sha1(); - case SHA_256: - return Hashing.sha256(); - case SHA_512: - return Hashing.sha512(); - default: - throw new IllegalArgumentException("Unknown hash algorithm"); - } + return switch (algorithm) { + case SHA_1 -> Hashing.sha1(); + case SHA_256 -> Hashing.sha256(); + case SHA_512 -> Hashing.sha512(); + default -> throw new IllegalArgumentException("Unknown hash algorithm"); + }; } private static String asHex(final String message) { diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index c754ee48de9364084628b26fc596d2645741ead3..7fc03360d3d275ec27c20d7dc0df8b1987c4298a 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -29,6 +29,10 @@ public class Data extends Extension { this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); } + public Field getFieldByName(final String name) { + return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null); + } + private void addField(final String name, final Object value) { addField(name, value, null); } diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index f3f72fab86e7924e107674d02de4707030ebbf13..0c6b96dff7f5c8c37590fe212937560314e99a2d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -1,6 +1,8 @@ package im.conversations.android.xmpp.model.data; -import eu.siacs.conversations.xml.Element; + import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import eu.siacs.conversations.xml.Element; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; import java.util.Collection; @@ -19,6 +21,10 @@ public class Field extends Extension { return Collections2.transform(getExtensions(Value.class), Element::getContent); } + public String getValue() { + return Iterables.getFirst(getValues(), null); + } + public void setFieldName(String name) { this.setAttribute("var", name); } diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java index 55f104e25bb547419e1e74ffa077ec563a354ea3..368837464881ada2e40c3c762415fd037fe19662 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java @@ -1,9 +1,12 @@ package im.conversations.android.xmpp.model.disco.info; +import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; import java.util.Collection; +import java.util.Objects; @XmlElement(name = "query") public class InfoQuery extends Extension { @@ -35,4 +38,39 @@ public class InfoQuery extends Extension { public boolean hasIdentityWithCategory(final String category) { return Iterables.any(getIdentities(), i -> category.equals(i.getCategory())); } + + public boolean hasIdentityWithCategoryAndType(final String category, final String type) { + return Iterables.any( + getIdentities(), i -> (category == null || category.equals(i.getCategory())) && (type == null || type.equals(i.getType()))); + } + + public Collection getFeatureStrings() { + return Collections2.filter( + Collections2.transform(getFeatures(), Feature::getVar), Objects::nonNull); + } + + public Collection getServiceDiscoveryExtensions() { + return getExtensions(Data.class); + } + + public Data getServiceDiscoveryExtension(final String formType) { + return Iterables.find( + getServiceDiscoveryExtensions(), e -> formType.equals(e.getFormType()), null); + } + + public String getServiceDiscoveryExtension(final String formType, final String fieldName) { + final var extension = + Iterables.find( + getServiceDiscoveryExtensions(), + e -> formType.equals(e.getFormType()), + null); + if (extension == null) { + return null; + } + final var field = extension.getFieldByName(fieldName); + if (field == null) { + return null; + } + return field.getValue(); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java index 129660b000cd1565956f10703afe828599eb8bf8..0887fde2c649978fec81669efd7ba2673d519e4f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java @@ -1,7 +1,10 @@ package im.conversations.android.xmpp.model.stanza; +import com.google.common.base.Strings; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; +import im.conversations.android.xmpp.model.jabber.Show; +import im.conversations.android.xmpp.model.jabber.Status; @XmlElement public class Presence extends Stanza implements EntityCapabilities { @@ -9,4 +12,63 @@ public class Presence extends Stanza implements EntityCapabilities { public Presence() { super(Presence.class); } + + public Availability getAvailability() { + final var show = getExtension(Show.class); + if (show == null) { + return Availability.ONLINE; + } + return Availability.valueOfShown(show.getContent()); + } + + public void setAvailability(final Availability availability) { + if (availability == null || availability == Availability.ONLINE) { + return; + } + this.addExtension(new Show()).setContent(availability.toShowString()); + } + + public void setStatus(final String status) { + if (Strings.isNullOrEmpty(status)) { + return; + } + this.addExtension(new Status()).setContent(status); + } + + public String getStatus() { + final var status = getExtension(Status.class); + return status == null ? null : status.getContent(); + } + + public enum Availability { + CHAT, + ONLINE, + AWAY, + XA, + DND, + OFFLINE; + + public String toShowString() { + return switch (this) { + case CHAT -> "chat"; + case AWAY -> "away"; + case XA -> "xa"; + case DND -> "dnd"; + default -> null; + }; + } + + public static Availability valueOfShown(final String content) { + if (Strings.isNullOrEmpty(content)) { + return Availability.ONLINE; + } + return switch (content) { + case "away" -> Availability.AWAY; + case "xa" -> Availability.XA; + case "dnd" -> Availability.DND; + case "chat" -> Availability.CHAT; + default -> Availability.ONLINE; + }; + } + } } diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 3d342f29cf03960c3945c4ec44ba8617170a8dd6..1dc3d97129b17a95cd9a96efdb7ddc92997ffd50 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1108,7 +1108,7 @@ Nur für Kontakte anzeigen Zeitüberschreitung beim Verbinden Erneut mit P2P versuchen - Kanalbindung nicht verfügbar + Keine Kanalbindung Word-Dokument OMEMO-Schlüssel wiederherstellen Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 0d60d916856c1da3cce3e821d7a2ef15097c32d4..ea972ff880cd3fa40d9549b692322af86d68da2a 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -1128,7 +1128,7 @@ Näita vaid kontaktidele Ühenduse on aegunud Proovi uuesti võrdõigusvõrguga - Edastuskanaliga sidumine pole võimalik + Edastuskanaliga sidumine puudub Wordi-dokument Taasta OMEMO võtmed Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 5e747125cedaf9b2cdca7fb40d049016f6679d42..d2faf1e0e1c9d787bb9497cbfd836b7d3e364f1e 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -820,12 +820,12 @@ e-book Original (non compressé) Ouvrir avec… - Photo de profil pour Conversations + Photo de profil Choisir un compte Restaurer la sauvegarde Restaurer Entrez votre mot de passe pour que le compte %s restaure la sauvegarde. - 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. + 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. Impossible de restaurer la sauvegarde. Impossible de déchiffrer la sauvegarde. Le mot de passe est-il correct ? Sauvegarde & restauration @@ -1010,7 +1010,7 @@ Vous essayez d\'importer un format de fichier de sauvegarde obsolète Livre audio Distributeur UnifiedPush - Ne tentez pas de restaurer des sauvegardes que vous n\'avez pas créées vous-même ! + Ne restaurez que des sauvegardes que vous avez vous-même créées. Signaler un spam Politique de confidentialité Quicksy vous demande votre consentement pour utiliser vos données @@ -1097,7 +1097,7 @@ Modifier les paramètres de notification L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur. L\'appel passe par les écouteurs. - XEP-0386: Bind 2 + XEP-0386 : Bind 2 Éditer le pseudo Supprimer la clé OpenPGP L\'appel passe par le bluetooth. @@ -1119,7 +1119,7 @@ Montrer aux contacts uniquement Bulles de discussion Impossible de modifier l\'appel - Channel binding indisponible + Pas de Channel binding Le client XMPP de votre contact peut ne pas prendre en charge les appels audio/vidéo. Impossible d\'ajouter une réaction Intégration d\'appel @@ -1131,4 +1131,18 @@ Plus de réactions Ajouter une réaction Montrer l\'image de profil + Numéro de téléphone copié dans le presse-papier + Copier l\'URI + URI copiée dans le presse-papier + Copier le numéro de téléphone + Restaurer les clés OMEMO + Quicksy ne peut restaurer des sauvegardes que pour des comptes de quicksy.im + Emplacement de sauvegarde + URI + Copier l\'adresse mail + Adresse mail copiée dans le presse-papier + Interruption de Service Planifiée + Le retour du service est planifié pour %s + Copier la géolocalisation + Service en panne (problème connu) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 96a0979679af4d97004b92fee2e9cff0a55787fa..10606beb37e89aa182077184a3544c59851eab5d 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1108,7 +1108,7 @@ Mostrar só aos contactos Caducidade da conexión Reintentar con P2P - Non está dispoñible a vinculación de canles + Sen vinculación de canles Documento de Word Restaurar claves OMEMO Quicksy só pode restaurar copias de apoio de contas quicksy.im diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index c7e38675cdd2e32d6f1c7e34b9afed57840bb3d1..591bdd0c388fbc4d4e8e1ecc5f65497a84410cb4 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -1122,7 +1122,7 @@ Messaggi di chat Colore di sfondo, dimensione caratteri, avatar Messaggi di chat - Associazione dei canali non disponibile + Nessuna associazione dei canali Documento Word Ripristina chiavi OMEMO Quicksy può ripristinare backup solo per profili quicksy.im diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 59bb5d68fd95cf67de384d3434f5eb02b1967276..c19dfc412dce3cf2a55ed8f81f131965f4977276 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -353,7 +353,7 @@ מפתח הצפנה שגוי. שגיאה האפליקציה שבה השתמשת כדי לבחור תמונה זו לא סיפקה מספיק הרשאות לקרוא את הקובץ.\n\nהשתמש במנהל קבצים אחר כדי לבחור תמונה. - עטיפת ערוץ אינה זמינה + אין קישור ערוצים הסר את המפתח הציבורי של OpenPGP האם אתה בטוח שברצונך להסיר את מפתח OpenPGP הציבורי שלך מהודעת הנוכחות שלך?\nאנשי הקשר שלך לא יוכלו יותר לשלוח לך הודעות מוצפנות OpenPGP. מפתח ציבורי OpenPGP פורסם. diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index e16f16507fd662591a1c437ef2df9f4fc8d70d14..0bceedecb5f9dfc6e64e9deb8ed8bed04b278068 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -1,7 +1,7 @@ 설정 - 계정 + 계정 관리 연락처 정보 계정 추가 이름 편집 @@ -387,4 +387,8 @@ 메세지가 클립보드에 복사되었습니다 위치 표시 바쁨 - \ No newline at end of file + 계정 관리 + 채팅 기록 + 그룹 채팅 정보 + 채널 정보 + diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 79189e7af914fc435141f76dab5312a6aa885988..10600e5ec1fa7913366483c326dcea1d9c3c58c2 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1141,7 +1141,7 @@ Limit czasu połączenia Spróbuj ponownie używając P2P dokument Microsoft Word - Przywiązywanie kanału niedostępne + Brak przywiązywania kanału Przywróć klucze OMEMO Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im Lokalizacja kopii zapasowej diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 63568f1796279f329d28eaa18d9a5385d7a6c007..bdc04fb9be5e4334b280aff18c0b63e25a6969c4 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -1126,7 +1126,7 @@ Você gostaria de excluir seu avatar? Alguns clientes podem continuar mostrando uma cópia em cache do seu avatar. Conexão demorou muito Tentar novamente com P2P - Vínculo de canal indisponível + Nenhum vínculo de canal Documento do Word Restaurar as chaves OMEMO O Quicksy só pode restaurar backups de contas quicksy.im diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 82389f706c8a5c04d3a12b176a2042b5eb13e18d..e1419ea86d90ba66fbb0f639057b76aebdbb8128 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -184,7 +184,7 @@ \nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения. Публичный ключ OpenPGP опубликован. Включить аккаунт - Удалить свой аккаунт? Удаление аккаунта также сотрёт все историю бесед. + Удалить свой аккаунт? Удаление аккаунта также сотрёт всю историю бесед. Записать голос XMPP-адрес Заблокировать XMPP-адрес @@ -1155,7 +1155,7 @@ Показывать только контактам Истекло время ожидания подключения Повторить через P2P - Привязка канала недоступна + Нет привязки канала Документ Word Восстановить ключи OMEMO Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index e9d3d5700f438f56a9b6dddb8e5ae9728313bccd..684702d4eba27a84281b74de98a6f18d14be1e23 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -1142,7 +1142,7 @@ Приказуј само контактима Истекла веза Покушај поново са P2P - Везивање канала недоступно + Нема везивања канала Word документ Quicksy може да врати резервне копије само за quicksy.im налоге Локација резервних копија @@ -1157,5 +1157,5 @@ Копирај URI Планирана недоступност Сервис недоступан (познат проблем) - Опоравак сервиса предвиђен у %s + Опоравак сервиса предвиђен за %s diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 32db9dd69e2127efd5abe2fa4ef4854611670417..517cc98b20db83da003df7cb6e93d5dd3a6626a9 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1156,7 +1156,7 @@ Бажаєте видалити свій аватар? Деякі клієнти можуть продовжувати відображати копію Вашого аватара з кешу. Час очікування з\'єднання вичерпано Повторити спробу з P2P - Прив\'язка каналу недоступна + Немає прив\'язки каналу документ Word Відновити ключі OMEMO Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index ea33fe687f79ea500f77aaa39c1593fa5d546a5c..6f375471c9732334b857956bce9d483d49266f72 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1103,7 +1103,7 @@ 仅对联系人显示 连接超时 使用 P2P 重试 - 不支持通道绑定 + 无通道绑定 Word 文档 恢复 OMEMO 密钥 Quicksy 只能恢复 quicksy.im 账号的备份 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index 37335c9865244442f933df9453b782677aea4874..56b1a368f7cfb0448bddf70b80175bdf6dbe0776 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -1002,7 +1002,7 @@ 已登出 您正在使用未驗證的設備。請掃描您其他設備上的 QR 碼進行驗證,以防止主動中間人攻擊。 有聲書 - 請勿嘗試還原非您自己建立的備份! + 僅還原由您親自建立的備份。 報告垃圾訊息 發送崩潰報告 相應的會話已存檔。 diff --git a/src/quicksy/res/values-ga/strings.xml b/src/quicksy/res/values-ga/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/quicksy/res/values-ga/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..021978dfa354e0edb4e3debbda0c89d7978d267e --- /dev/null +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -0,0 +1,339 @@ +package im.conversations.android.xmpp; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNull; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import im.conversations.android.xml.XmlElementReader; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class EntityCapabilitiesTest { + + @Test + public void entityCaps() throws IOException { + final String xml = + """ + + + + + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", var); + } + + @Test + public void entityCapsComplexExample() throws IOException { + final String xml = + """ + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + ipv4 + ipv6 + + + Mac + + + 10.5.1 + + + Psi + + + 0.11 + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", var); + } + + @Test + public void entityCapsOpenFire() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Linux + + + 4.14.355-276.618.amzn2.x86_64 amd64 - Java 17.0.14 + + + Openfire + + + 5.0.0 Alpha + + + + + http://jabber.org/network/serverinfo + + + xmpp:dwd@dave.cridland.net + xmpp:akrherz@igniterealtime.org + xmpp:benjamin@igniterealtime.org + mailto:benjamin@holyarmy.org + xmpp:csh@igniterealtime.org + xmpp:dan.caseley@igniterealtime.org + mailto:dan.caseley@surevine.com + xmpp:flow@igniterealtime.org + xmpp:gdt@igniterealtime.org + mailto:greg.d.thomas@gmail.com + xmpp:guus.der.kinderen@igniterealtime.org + mailto:guus.der.kinderen@gmail.com + xmpp:lg@igniterealtime.org + xmpp:rcollier@igniterealtime.org + mailto:robincollier@hotmail.com + + + + +"""; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(Iq.class)); + final var iq = (Iq) element; + final InfoQuery info = iq.getExtension(InfoQuery.class); + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var); + } + + @Test + public void caps2() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities2.hash(info).encoded(); + Assert.assertEquals("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=", var); + } + + @Test + public void caps2complex() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Tkabber + + + 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2) + + + Windows + + + XP + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities2.hash(info).encoded(); + Assert.assertEquals("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=", var); + } + + @Test + public void parseCaps2Node() { + final var caps = + DiscoManager.buildHashFromNode( + "urn:xmpp:caps#sha-256.u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY="); + assertThat(caps, instanceOf(EntityCapabilities2.EntityCaps2Hash.class)); + } + + @Test + public void parseCaps2NodeMissingHash() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#sha-256."); + assertNull(caps); + } + + @Test + public void parseCaps2NodeInvalid() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#-"); + assertNull(caps); + } + + @Test + public void parseCaps2NodeUnknownAlgo() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#test.test"); + assertNull(caps); + } +}