From 938cd3b9e9562a0560744b2796de424c84de9988 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Apr 2025 10:05:52 +0200 Subject: [PATCH 01/19] logging and code clean up in XmppDomainVerifier --- .../crypto/XmppDomainVerifier.java | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java index a0b36985e3cd3fe6d298ee69335711dc34e0ec12..614bd185c6ae0a32bd3047b42f5e6c991fd80cc2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java @@ -2,6 +2,8 @@ package eu.siacs.conversations.crypto; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; +import com.google.common.base.CharMatcher; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -12,6 +14,7 @@ import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Locale; import javax.net.ssl.SSLPeerUnverifiedException; @@ -36,18 +39,18 @@ public class XmppDomainVerifier { private static final String XMPP_ADDR = "1.3.6.1.5.5.7.8.5"; private static List getCommonNames(final X509Certificate certificate) { - List domains = new ArrayList<>(); + final var domains = new ImmutableList.Builder(); try { - X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject(); - RDN[] rdns = x500name.getRDNs(BCStyle.CN); - for (int i = 0; i < rdns.length; ++i) { + final var x500name = new JcaX509CertificateHolder(certificate).getSubject(); + final RDN[] nameRDNs = x500name.getRDNs(BCStyle.CN); + for (int i = 0; i < nameRDNs.length; ++i) { domains.add( IETFUtils.valueToString( x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue())); } - return domains; - } catch (CertificateEncodingException e) { - return domains; + return domains.build(); + } catch (final CertificateEncodingException e) { + return Collections.emptyList(); } } @@ -75,17 +78,24 @@ public class XmppDomainVerifier { } } - public static boolean matchDomain(final String needle, final List haystack) { - for (final String entry : haystack) { - if (entry.startsWith("*.")) { + public static boolean matchDomain(final String domain, final List certificateDomains) { + for (final String certificateDomain : certificateDomains) { + if (certificateDomain.startsWith("*.")) { // https://www.rfc-editor.org/rfc/rfc6125#section-6.4.3 // wild cards can only be in the left most label and don’t match '.' - final int i = needle.indexOf('.'); - if (i != -1 && needle.substring(i).equalsIgnoreCase(entry.substring(1))) { + final var wildcardEntry = certificateDomain.substring(1); + if (CharMatcher.is('.').countIn(wildcardEntry) < 2) { + Log.w(LOGTAG, "not enough labels in wildcard certificate"); + break; + } + final int position = domain.indexOf('.'); + if (position != -1 && domain.substring(position).equalsIgnoreCase(wildcardEntry)) { + Log.d(LOGTAG, "domain " + domain + " matched " + certificateDomain); return true; } } else { - if (entry.equalsIgnoreCase(needle)) { + if (certificateDomain.equalsIgnoreCase(domain)) { + Log.d(LOGTAG, "domain " + domain + " matched " + certificateDomain); return true; } } @@ -184,6 +194,7 @@ public class XmppDomainVerifier { return all.build(); } + @NonNull @Override public String toString() { return MoreObjects.toStringHelper(this) From ecdfc3a9cf977bb8e97fca453404bdbdc9721d19 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 10 Apr 2025 16:27:41 +0200 Subject: [PATCH 02/19] =?UTF-8?q?the=20holger=20fix=20=F0=9F=A4=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/de/gultsch/common/Patterns.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/gultsch/common/Patterns.java b/src/main/java/de/gultsch/common/Patterns.java index 36245b4d8aa80bb4f50af79a8d2676eac3532859..c55ee010868aeff31e8e92756e89664d46393bbb 100644 --- a/src/main/java/de/gultsch/common/Patterns.java +++ b/src/main/java/de/gultsch/common/Patterns.java @@ -6,7 +6,7 @@ public class Patterns { public static final Pattern URI_GENERIC = Pattern.compile( - "(?<=^|\\p{Z}|\\s|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); + "(?<=^|\\p{Z}|\\s|\\p{P}|<)(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); public static final Pattern URI_TEL = Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); From f76d54eb701f24633a47ee83b6c67f0719bc3770 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 12 Apr 2025 10:09:55 +0200 Subject: [PATCH 03/19] use message id as session id in jingle file transfer --- .../crypto/XmppDomainVerifier.java | 1 - .../xmpp/jingle/AbstractJingleConnection.java | 26 +++++++------------ .../jingle/JingleFileTransferConnection.java | 1 + 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java index 614bd185c6ae0a32bd3047b42f5e6c991fd80cc2..69799b10a84096c2bdcec9334f84a211af5f0e57 100644 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java @@ -26,7 +26,6 @@ import org.bouncycastle.asn1.DERIA5String; import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; 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 1b1a2d0e3ecf5f287467f789f771b7320df82ce2..de325534e3a5c45f9bc6acc4aa0a3b8a985aed82 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -1,16 +1,13 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; - 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; @@ -20,10 +17,8 @@ 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 im.conversations.android.xmpp.model.jingle.Jingle; import im.conversations.android.xmpp.model.stanza.Iq; - import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -187,8 +182,7 @@ public abstract class AbstractJingleConnection { abstract void deliverPacket(Iq jinglePacket); - protected void receiveOutOfOrderAction( - final Iq jinglePacket, final Jingle.Action action) { + protected void receiveOutOfOrderAction(final Iq jinglePacket, final Jingle.Action action) { Log.d( Config.LOGTAG, String.format( @@ -198,7 +192,8 @@ public abstract class AbstractJingleConnection { Log.d( Config.LOGTAG, String.format( - "%s: got a reason to terminate with out-of-order. but already in state %s", + "%s: got a reason to terminate with out-of-order. but already in state" + + " %s", id.account.getJid().asBareJid(), getState())); respondWithOutOfOrder(jinglePacket); } else { @@ -267,10 +262,7 @@ public abstract class AbstractJingleConnection { } private void respondWithJingleError( - final Iq original, - String jingleCondition, - String condition, - String conditionType) { + final Iq original, String jingleCondition, String condition, String conditionType) { jingleConnectionManager.respondWithJingleError( id.account, original, jingleCondition, condition, conditionType); } @@ -371,15 +363,15 @@ public abstract class AbstractJingleConnection { return new Id(account, with, sessionId); } - public static Id of(Account account, Jid with) { + public static Id of(final Account account, final Jid with) { return new Id(account, with, JingleConnectionManager.nextRandomId()); } - public static Id of(Message message) { + public static Id of(final Message message) { return new Id( message.getConversation().getAccount(), message.getCounterpart(), - JingleConnectionManager.nextRandomId()); + message.getUuid()); } public Contact getContact() { @@ -430,8 +422,8 @@ public abstract class AbstractJingleConnection { case DECLINE, BUSY -> State.TERMINATED_DECLINED_OR_BUSY; case CANCEL, TIMEOUT -> State.TERMINATED_CANCEL_OR_TIMEOUT; case SECURITY_ERROR -> State.TERMINATED_SECURITY_ERROR; - case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> State - .TERMINATED_APPLICATION_FAILURE; + case FAILED_APPLICATION, UNSUPPORTED_TRANSPORTS, UNSUPPORTED_APPLICATIONS -> + State.TERMINATED_APPLICATION_FAILURE; default -> State.TERMINATED_CONNECTIVITY_ERROR; }; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index fbaf7ffe3e7a52d5a9148cc780cb0d6135448912..d8dd240c3c2c38d592cee78c3c2477e8aa560eb2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -101,6 +101,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection this.xmppConnectionService.findOrCreateConversation( id.account, id.with.asBareJid(), false, false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); + this.message.setRemoteMsgId(id.sessionId); this.message.setStatus(Message.STATUS_RECEIVED); this.message.setErrorMessage(null); this.message.setTransferable(this); From 1c393a17f56b7205b9921d123be5976fa8082be1 Mon Sep 17 00:00:00 2001 From: random_r Date: Thu, 10 Apr 2025 09:09:13 +0000 Subject: [PATCH 04/19] Translated using Weblate (Italian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 3e021d6993ae39593b69a20fab3a23d8867920a1..88b1dc86cfb5973ae8095f1fa030056e691d1ac4 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -1129,4 +1129,12 @@ Ripristina chiavi OMEMO Quicksy può ripristinare backup solo per profili quicksy.im Percorso backup + URI + Copia posizione geografica + Copia indirizzo email + Indirizzo email copiato negli appunti + Numero di telefono copiato negli appunti + Copia numero di telefono + Copia URI + URI copiato negli appunti From 29c988daa3078482b86772c48ce2d644cf1e78b1 Mon Sep 17 00:00:00 2001 From: DrManhattanMR Date: Fri, 11 Apr 2025 15:47:20 +0000 Subject: [PATCH 05/19] Translated using Weblate (Spanish) Currently translated at 99.2% (1065 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 8315f5b3b9f901dfd4add28d94822f3b20eb8742..633b2b7f4479a82417b51dc19847c006dd7f0194 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1108,12 +1108,12 @@ El cliente XMPP de su contacto puede que no admita llamadas de audio/vídeo. No se pudo modificar la llamada Burbujas de chat - Color, Tamaño de fuente, Imágenes de perfil + Color de fondo, Tamaño de fuente, Imágenes de perfil Burbujas de Chat Integración de llamadas Mostrar avatares Mostrar imágenes de perfil para tus mensajes y conversaciones 1:1, aparte de las conversaciones en grupo. - Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como ser finalizar una llamada cuando recibimos otra. + Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como finalizar una llamada cuando recibimos otra. Mensajes alineados a la izquierda Mostrar todos los mensajes, incluso los propios, sobre el margen izquierdo para una distribución uniforme del chat. Notificaciones personalizadas From a999155f2445fe0f1fdb6df616fc6378ff75be87 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Fri, 11 Apr 2025 07:25:28 +0000 Subject: [PATCH 06/19] Translated using Weblate (Albanian) Currently translated at 100.0% (85 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/sq/ --- fastlane/metadata/android/sq/changelogs/4213104.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/4213104.txt diff --git a/fastlane/metadata/android/sq/changelogs/4213104.txt b/fastlane/metadata/android/sq/changelogs/4213104.txt new file mode 100644 index 0000000000000000000000000000000000000000..e8da9e89e96ae919b8318b806b1cc54aac7a164c --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4213104.txt @@ -0,0 +1,2 @@ +* Shfrytëzim i SASL SCRAM Downgrade Protection (XEP-0474) +* Dërgim reagimesh ndaj MP MUC te JID i saktë From 0d0436de404b9d5d355ade98fc0b591e5eb42036 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Apr 2025 11:34:45 +0200 Subject: [PATCH 07/19] version bump to 2.18.1 --- CHANGELOG.md | 7 ++++++- build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4214004.txt | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4214004.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 739c328d05491164e1e5ed0c3e81bc4c98922d3f..bd242198d888427b4b1dd9180bb036867aa2e396 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # Changelog +### Version 2.18.1 + +* Fix reactions on files received via P2P +* Improve URI matching + ### Version 2.18.0 * Add ability to pick backup location -* More more URIs (tel:, mailto:) clickable +* Make more URIs (tel:, mailto:) clickable ### Version 2.17.12 diff --git a/build.gradle b/build.gradle index 5433c94cf0e2ab35c10b32bf2996132d4f711bd9..f258c0dc956f8f4232e3a971f486075f9eb5d2c7 100644 --- a/build.gradle +++ b/build.gradle @@ -117,8 +117,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42139 - versionName "2.18.0" + versionCode 42140 + versionName "2.18.1" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4214004.txt b/fastlane/metadata/android/en-US/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..03024d49fc63e7d24aabfd86c4d62b28e7dec975 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* Fix reactions on files received via P2P +* Improve URI matching From 97b4a0dc5f80336d9c536a9eceb02d4163a89362 Mon Sep 17 00:00:00 2001 From: DrManhattanMR Date: Sat, 12 Apr 2025 17:37:40 +0000 Subject: [PATCH 08/19] Translated using Weblate (Spanish) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 633b2b7f4479a82417b51dc19847c006dd7f0194..66f7280ff9d996acb037792388f16849dd05842a 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -4,7 +4,7 @@ Gestionar cuentas Gestionar cuenta Detalles del contacto - Detalles de conversación + Detalles de la conversación grupal Detalles del canal Añadir cuenta Editar contacto @@ -119,7 +119,7 @@ Periodo de gracia El tiempo que se silencian las notificaciones tras detectar actividad en otro de tus dispositivos. Avanzado - Al enviar los informes de los fallos, ayudará a un mayor desarrollo + Al enviar los informes, estás ayudando al desarrollo Confirmar mensajes Permitir a sus contactos saber cuando ha recibido y leído sus mensajes Impedir capturas de pantalla @@ -413,7 +413,7 @@ documento PDF aplicación para Android Contacto - Se ha publicado el avatar. + Foto de perfil ha sido publicada! Enviando %s Ofreciendo %s Ocultar desconectados @@ -484,7 +484,7 @@ Renovar certificado ¡Error buscando clave OMEMO! ¡Clave OMEMO con certificado verificada! - Su dispositivo no admite la selección de certificados de cliente. + Tu dispositivo no admite la selección de certificados de cliente! Conexión Conectar via Tor Todas las conexiones se realizan a través de la red TOR. Requiere Orbot @@ -826,7 +826,7 @@ e-book Original (sin comprimir) Abrir con… - Avatar + Foto de Perfil Elige una cuenta Restaurar copia de respaldo Restaurar @@ -1127,4 +1127,12 @@ Ubicación de la copia de seguridad Restaurar claves OMEMO Canal no disponible + URI + Copiar geolocalización + Copiar dirección email + Email copiado al portapapeles + URI copiada al portapapeles + Copiar URI + Número de teléfono copiado al portapapeles + Copiar numero de teléfono From 36026e957973975d184b41c52dbe1ae3c475730e Mon Sep 17 00:00:00 2001 From: SomeTr Date: Mon, 14 Apr 2025 10:10:09 +0000 Subject: [PATCH 09/19] Translated using Weblate (Ukrainian) Currently translated at 100.0% (86 of 86 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4214004.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4214004.txt diff --git a/fastlane/metadata/android/uk/changelogs/4214004.txt b/fastlane/metadata/android/uk/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..b21f4b435d33f10e15114eae42f0b61bf6e223f4 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* Виправлено реакції на файли, отримані через P2P +* Покращено співставлення URI From 12ada002d19c626cbee5c1fdd559bd9b8dc4f249 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Mon, 14 Apr 2025 12:43:01 +0000 Subject: [PATCH 10/19] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (86 of 86 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4214004.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4214004.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4214004.txt b/fastlane/metadata/android/zh-CN/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..f95ae57ca1d855ab03dcb75c8c03056cbb6eb461 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* 修复对通过 P2P 接收的文件的回应 +* 改进 URI 匹配 From aa0b968b8befc6f1579a5c87d0919597f42b6277 Mon Sep 17 00:00:00 2001 From: random_r Date: Mon, 14 Apr 2025 14:49:21 +0000 Subject: [PATCH 11/19] Translated using Weblate (Italian) Currently translated at 100.0% (86 of 86 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/4214004.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/4214004.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4214004.txt b/fastlane/metadata/android/it-IT/changelogs/4214004.txt new file mode 100644 index 0000000000000000000000000000000000000000..f68f27f50b7e97268cab94faa78c1d3737dddf72 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4214004.txt @@ -0,0 +1,2 @@ +* Corrette le reazioni sui file ricevuti via P2P +* Migliorata la corrispondenza degli URI From 568fa4ace2b135b508fc935fa344273ddbb6a604 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Apr 2025 17:20:49 +0200 Subject: [PATCH 12/19] refactor LocalizedContent code --- .../conversations/parser/MessageParser.java | 4 +- .../eu/siacs/conversations/xml/Element.java | 4 - .../conversations/xml/LocalizedContent.java | 25 +- .../eu/siacs/conversations/xml/XmlReader.java | 224 +++++++++--------- .../android/xmpp/model/stanza/Message.java | 38 ++- 5 files changed, 150 insertions(+), 145 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 7e22df067a348bcec31b3d2ba84d9f413b382cf6..168bea369f3c023f26df9943f38cbf465a450249 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -1157,9 +1157,7 @@ public class MessageParser extends AbstractParser && !packet.hasChild("thread")) { // We already know it has no body per above if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); - final LocalizedContent subject = - packet.findInternationalizedChildContentInDefaultNamespace( - "subject"); + final LocalizedContent subject = packet.getSubject(); if (subject != null && conversation.getMucOptions().setSubject(subject.content)) { mXmppConnectionService.updateConversation(conversation); diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 1b91eeaae84da4639502453e7044c3d730ee39ac..392f74531e9f82af013202750bfa00e4a9bd3593 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -68,10 +68,6 @@ public class Element { return element == null ? null : element.getContent(); } - public LocalizedContent findInternationalizedChildContentInDefaultNamespace(String name) { - return LocalizedContent.get(this, name); - } - public Element findChild(String name, String xmlns) { for (Element child : this.children) { if (name.equals(child.getName()) && xmlns.equals(child.getAttribute("xmlns"))) { diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index d730bd34f7b4a4bef5c5fdd08bd63598d00c1e88..bb50e9e757f7f480ff26f35334c00ee18c6045e6 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.xml; import com.google.common.collect.Iterables; - import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -14,29 +13,13 @@ public class LocalizedContent { public final String language; public final int count; - private LocalizedContent(String content, String language, int count) { + private LocalizedContent(final String content, final String language, final int count) { this.content = content; this.language = language; this.count = count; } - public static LocalizedContent get(final Element element, String name) { - final HashMap contents = new HashMap<>(); - final String parentLanguage = element.getAttribute("xml:lang"); - for(Element child : element.children) { - if (name.equals(child.getName())) { - final String namespace = child.getNamespace(); - final String childLanguage = child.getAttribute("xml:lang"); - final String lang = childLanguage == null ? parentLanguage : childLanguage; - final String content = child.getContent(); - if (content != null && (namespace == null || Namespace.JABBER_CLIENT.equals(namespace))) { - if (contents.put(lang, content) != null) { - //anything that has multiple contents for the same language is invalid - return null; - } - } - } - } + public static LocalizedContent get(final Map contents) { if (contents.isEmpty()) { return null; } @@ -45,10 +28,6 @@ public class LocalizedContent { if (localized != null) { return new LocalizedContent(localized, userLanguage, contents.size()); } - final String defaultLanguageContent = contents.get(null); - if (defaultLanguageContent != null) { - return new LocalizedContent(defaultLanguageContent, STREAM_LANGUAGE, contents.size()); - } final String streamLanguageContent = contents.get(STREAM_LANGUAGE); if (streamLanguageContent != null) { return new LocalizedContent(streamLanguageContent, STREAM_LANGUAGE, contents.size()); diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index 090a6e47dfcdfaf64ab9dd14309611bebfa8b7c8..5323bf1e81ea5da023662c71de1d3cb0e6e54a80 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -2,132 +2,134 @@ package eu.siacs.conversations.xml; import android.util.Log; import android.util.Xml; - import eu.siacs.conversations.Config; - import im.conversations.android.xmpp.ExtensionFactory; -import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.StreamElement; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; public class XmlReader implements Closeable { - private final XmlPullParser parser; - private InputStream is; + private final XmlPullParser parser; + private InputStream is; - public XmlReader() { - this.parser = Xml.newPullParser(); - try { - this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - } catch (XmlPullParserException e) { - Log.d(Config.LOGTAG, "error setting namespace feature on parser"); - } - } + public XmlReader() { + this.parser = Xml.newPullParser(); + try { + this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + } catch (XmlPullParserException e) { + Log.d(Config.LOGTAG, "error setting namespace feature on parser"); + } + } - public void setInputStream(InputStream inputStream) throws IOException { - if (inputStream == null) { - throw new IOException(); - } - this.is = inputStream; - try { - parser.setInput(new InputStreamReader(this.is)); - } catch (XmlPullParserException e) { - throw new IOException("error resetting parser"); - } - } + public void setInputStream(InputStream inputStream) throws IOException { + if (inputStream == null) { + throw new IOException(); + } + this.is = inputStream; + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } - public void reset() throws IOException { - if (this.is == null) { - throw new IOException(); - } - try { - parser.setInput(new InputStreamReader(this.is)); - } catch (XmlPullParserException e) { - throw new IOException("error resetting parser"); - } - } + public void reset() throws IOException { + if (this.is == null) { + throw new IOException(); + } + try { + parser.setInput(new InputStreamReader(this.is)); + } catch (XmlPullParserException e) { + throw new IOException("error resetting parser"); + } + } - @Override - public void close() { - this.is = null; - } + @Override + public void close() { + this.is = null; + } - public Tag readTag() throws IOException { - try { - while (this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.getEventType() == XmlPullParser.START_TAG) { - Tag tag = Tag.start(parser.getName()); - final String xmlns = parser.getNamespace(); - for (int i = 0; i < parser.getAttributeCount(); ++i) { - final String prefix = parser.getAttributePrefix(i); - String name; - if (prefix != null && !prefix.isEmpty()) { - name = prefix+":"+parser.getAttributeName(i); - } else { - name = parser.getAttributeName(i); - } - tag.setAttribute(name,parser.getAttributeValue(i)); - } - if (xmlns != null) { - tag.setAttribute("xmlns", xmlns); - } - return tag; - } else if (parser.getEventType() == XmlPullParser.END_TAG) { - return Tag.end(parser.getName()); - } else if (parser.getEventType() == XmlPullParser.TEXT) { - return Tag.no(parser.getText()); - } - } + public Tag readTag() throws IOException { + try { + while (this.is != null && parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.getEventType() == XmlPullParser.START_TAG) { + Tag tag = Tag.start(parser.getName()); + final String xmlns = parser.getNamespace(); + for (int i = 0; i < parser.getAttributeCount(); ++i) { + // TODO we would also look at parser.getAttributeNamespace() + final String prefix = parser.getAttributePrefix(i); + String name; + if (prefix != null && !prefix.isEmpty()) { + name = prefix + ":" + parser.getAttributeName(i); + } else { + name = parser.getAttributeName(i); + } + tag.setAttribute(name, parser.getAttributeValue(i)); + } + if (xmlns != null) { + tag.setAttribute("xmlns", xmlns); + } + return tag; + } else if (parser.getEventType() == XmlPullParser.END_TAG) { + return Tag.end(parser.getName()); + } else if (parser.getEventType() == XmlPullParser.TEXT) { + return Tag.no(parser.getText()); + } + } - } catch (Throwable throwable) { - throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable); - } - return null; - } + } catch (Throwable throwable) { + throw new IOException( + "xml parser mishandled " + + throwable.getClass().getSimpleName() + + "(" + + throwable.getMessage() + + ")", + throwable); + } + return null; + } - public T readElement(final Tag current, final Class clazz) - throws IOException { - final Element element = readElement(current); - if (clazz.isInstance(element)) { - return clazz.cast(element); - } - throw new IOException( - String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName())); - } + public T readElement(final Tag current, final Class clazz) + throws IOException { + final Element element = readElement(current); + if (clazz.isInstance(element)) { + return clazz.cast(element); + } + throw new IOException( + String.format("Read unexpected {%s}%s", element.getNamespace(), element.getName())); + } - public Element readElement(final Tag currentTag) throws IOException { - final var attributes = currentTag.getAttributes(); - final var namespace = attributes.get("xmlns"); - final var name = currentTag.getName(); - final Element element = ExtensionFactory.create(name, namespace); - element.setAttributes(currentTag.getAttributes()); - Tag nextTag = this.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - if (nextTag.isNo()) { - element.setContent(nextTag.getName()); - nextTag = this.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - } - while (!nextTag.isEnd(element.getName())) { - if (!nextTag.isNo()) { - Element child = this.readElement(nextTag); - element.addChild(child); - } - nextTag = this.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - } - return element; - } + public Element readElement(final Tag currentTag) throws IOException { + final var attributes = currentTag.getAttributes(); + final var namespace = attributes.get("xmlns"); + final var name = currentTag.getName(); + final Element element = ExtensionFactory.create(name, namespace); + element.setAttributes(currentTag.getAttributes()); + Tag nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + if (nextTag.isNo()) { + element.setContent(nextTag.getName()); + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + } + while (!nextTag.isEnd(element.getName())) { + if (!nextTag.isNo()) { + Element child = this.readElement(nextTag); + element.addChild(child); + } + nextTag = this.readTag(); + if (nextTag == null) { + throw new IOException("interrupted mid tag"); + } + } + return element; + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java index a1c981b9d994fee3705efb7a6b4c6bcb86ecea02..dd5f7458e94937a81f4f8479decc7a91ab670915 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java @@ -1,11 +1,13 @@ package im.conversations.android.xmpp.model.stanza; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; - import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; import im.conversations.android.xmpp.model.jabber.Body; - +import im.conversations.android.xmpp.model.jabber.Subject; import java.util.Locale; @XmlElement @@ -21,9 +23,37 @@ public class Message extends Stanza { } public LocalizedContent getBody() { - return findInternationalizedChildContentInDefaultNamespace("body"); + return getLocalizedContent(Body.class); + } + + public LocalizedContent getSubject() { + return getLocalizedContent(Subject.class); + } + + private LocalizedContent getLocalizedContent(final Class clazz) { + final var builder = new ImmutableMap.Builder(); + final var messageLanguage = this.getAttribute("xml:lang"); + final var parentLanguage = + Strings.isNullOrEmpty(messageLanguage) + ? LocalizedContent.STREAM_LANGUAGE + : messageLanguage; + for (final var element : this.getExtensions(clazz)) { + final var elementLanguage = element.getAttribute("xml:lang"); + final var language = + Strings.isNullOrEmpty(elementLanguage) ? parentLanguage : elementLanguage; + final var content = element.getContent(); + if (content == null) { + continue; + } + builder.put(language, content); + } + try { + return LocalizedContent.get(builder.buildOrThrow()); + } catch (final IllegalArgumentException e) { + return null; + } } - + public Type getType() { final var value = this.getAttribute("type"); if (value == null) { From adf227bf1fc846fc734bef980e74029687751cd5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 14 Apr 2025 17:39:09 +0200 Subject: [PATCH 13/19] use new api for accessing pgp encrypted and oob --- .../eu/siacs/conversations/parser/MessageParser.java | 10 +++++++--- .../eu/siacs/conversations/xml/LocalizedContent.java | 1 - 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 168bea369f3c023f26df9943f38cbf465a450249..3907222c66b778d22092fc50de9b16350f462261 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -44,6 +44,7 @@ import im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.forward.Forwarded; import im.conversations.android.xmpp.model.markers.Displayed; import im.conversations.android.xmpp.model.occupant.OccupantId; +import im.conversations.android.xmpp.model.oob.OutOfBandData; import im.conversations.android.xmpp.model.reactions.Reactions; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -547,10 +548,13 @@ public class MessageParser extends AbstractParser final boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT; - final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); + final var encrypted = + packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class); + final String pgpEncrypted = encrypted == null ? null : encrypted.getContent(); + ; - final Element oob = packet.findChild("x", Namespace.OOB); - final String oobUrl = oob != null ? oob.findChildContent("url") : null; + final var oob = packet.getExtension(OutOfBandData.class); + final String oobUrl = oob != null ? oob.getURL() : null; final var replace = packet.getExtension(Replace.class); final var replacementId = replace == null ? null : replace.getId(); final var axolotlEncrypted = packet.getOnlyExtension(Encrypted.class); diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index bb50e9e757f7f480ff26f35334c00ee18c6045e6..fc1c1ca1b7e6d56294d0156d87d76208ec21735f 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.xml; import com.google.common.collect.Iterables; -import java.util.HashMap; import java.util.Locale; import java.util.Map; From 539b55d83571016c2f8faa3d2fb4b8c557f10030 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 15 Apr 2025 09:21:38 +0200 Subject: [PATCH 14/19] extract occupant-id and stanza-id only when they are unique --- .../generator/MessageGenerator.java | 14 ++--- .../conversations/parser/MessageParser.java | 58 +++++++------------ .../conversations/parser/PresenceParser.java | 2 +- .../android/xmpp/model/receipts/Received.java | 5 ++ .../android/xmpp/model/unique/StanzaId.java | 21 +++++++ 5 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 6ff2337cc2bc3175877ee032f0e3b0e55f138b18..d8f71a0b4f1836f6bdbfe5fbd3385e43d99fe366 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -17,11 +17,12 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import im.conversations.android.xmpp.model.correction.Replace; +import im.conversations.android.xmpp.model.hints.Store; import im.conversations.android.xmpp.model.reactions.Reaction; import im.conversations.android.xmpp.model.reactions.Reactions; +import im.conversations.android.xmpp.model.receipts.Received; import im.conversations.android.xmpp.model.unique.OriginId; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Locale; @@ -258,19 +259,14 @@ public class MessageGenerator extends AbstractGenerator { } public im.conversations.android.xmpp.model.stanza.Message received( - Account account, final Jid from, final String id, - ArrayList namespaces, - im.conversations.android.xmpp.model.stanza.Message.Type type) { + final im.conversations.android.xmpp.model.stanza.Message.Type type) { final var receivedPacket = new im.conversations.android.xmpp.model.stanza.Message(); receivedPacket.setType(type); receivedPacket.setTo(from); - receivedPacket.setFrom(account.getJid()); - for (final String namespace : namespaces) { - receivedPacket.addChild("received", namespace).setAttribute("id", id); - } - receivedPacket.addChild("store", "urn:xmpp:hints"); + receivedPacket.addExtension(new Received(id)); + receivedPacket.addExtension(new Store()); return receivedPacket; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 3907222c66b778d22092fc50de9b16350f462261..b6194ccd1be5f90ac229a85b32e3143fa8356aba 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -45,9 +45,11 @@ import im.conversations.android.xmpp.model.forward.Forwarded; import im.conversations.android.xmpp.model.markers.Displayed; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.oob.OutOfBandData; +import im.conversations.android.xmpp.model.pubsub.event.Event; import im.conversations.android.xmpp.model.reactions.Reactions; +import im.conversations.android.xmpp.model.receipts.Request; +import im.conversations.android.xmpp.model.unique.StanzaId; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -72,7 +74,9 @@ public class MessageParser extends AbstractParser } private static String extractStanzaId( - Element packet, boolean isTypeGroupChat, Conversation conversation) { + final im.conversations.android.xmpp.model.stanza.Message packet, + final boolean isTypeGroupChat, + final Conversation conversation) { final Jid by; final boolean safeToExtract; if (isTypeGroupChat) { @@ -83,23 +87,14 @@ public class MessageParser extends AbstractParser by = account.getJid().asBareJid(); safeToExtract = account.getXmppConnection().getFeatures().stanzaIds(); } - return safeToExtract ? extractStanzaId(packet, by) : null; + return safeToExtract ? StanzaId.get(packet, by) : null; } - private static String extractStanzaId(Account account, Element packet) { + private static String extractStanzaId( + final Account account, + final im.conversations.android.xmpp.model.stanza.Message packet) { final boolean safeToExtract = account.getXmppConnection().getFeatures().stanzaIds(); - return safeToExtract ? extractStanzaId(packet, account.getJid().asBareJid()) : null; - } - - private static String extractStanzaId(Element packet, Jid by) { - for (Element child : packet.getChildren()) { - if (child.getName().equals("stanza-id") - && Namespace.STANZA_IDS.equals(child.getNamespace()) - && by.equals(Jid.Invalid.getNullForInvalid(child.getAttributeAsJid("by")))) { - return child.getAttribute("id"); - } - } - return null; + return safeToExtract ? StanzaId.get(packet, account.getJid().asBareJid()) : null; } private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) { @@ -249,7 +244,7 @@ public class MessageParser extends AbstractParser return null; } - private void parseEvent(final Element event, final Jid from, final Account account) { + private void parseEvent(final Event event, final Jid from, final Account account) { final Element items = event.findChild("items"); final String node = items == null ? null : items.getAttribute("node"); if ("urn:xmpp:avatar:metadata".equals(node)) { @@ -551,7 +546,6 @@ public class MessageParser extends AbstractParser final var encrypted = packet.getOnlyExtension(im.conversations.android.xmpp.model.pgp.Encrypted.class); final String pgpEncrypted = encrypted == null ? null : encrypted.getContent(); - ; final var oob = packet.getExtension(OutOfBandData.class); final String oobUrl = oob != null ? oob.getURL() : null; @@ -592,7 +586,8 @@ public class MessageParser extends AbstractParser final Jid mucTrueCounterPartByPresence; if (conversation != null) { final var mucOptions = conversation.getMucOptions(); - occupant = mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null; + occupant = + mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null; final var user = occupant == null ? null : mucOptions.findUserByOccupantId(occupant.getId()); mucTrueCounterPartByPresence = user == null ? null : user.getRealJid(); @@ -611,7 +606,8 @@ public class MessageParser extends AbstractParser mXmppConnectionService.find(account, from.asBareJid()); if (conversation != null) { final var mucOptions = conversation.getMucOptions(); - occupant = mucOptions.occupantId() ? packet.getExtension(OccupantId.class) : null; + occupant = + mucOptions.occupantId() ? packet.getOnlyExtension(OccupantId.class) : null; } else { occupant = null; } @@ -1396,8 +1392,7 @@ public class MessageParser extends AbstractParser // end no body } - final Element event = - original.findChild("event", "http://jabber.org/protocol/pubsub#event"); + final var event = original.getExtension(Event.class); if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) { if (event.hasChild("items")) { parseEvent(event, original.getFrom(), account); @@ -1696,27 +1691,14 @@ public class MessageParser extends AbstractParser final Account account, final im.conversations.android.xmpp.model.stanza.Message packet, final String remoteMsgId, - MessageArchiveService.Query query) { - final boolean markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); - final boolean request = packet.hasChild("request", "urn:xmpp:receipts"); + final MessageArchiveService.Query query) { + final var request = packet.hasExtension(Request.class); if (query == null) { - final ArrayList receiptsNamespaces = new ArrayList<>(); - if (markable) { - receiptsNamespaces.add("urn:xmpp:chat-markers:0"); - } if (request) { - receiptsNamespaces.add("urn:xmpp:receipts"); - } - if (receiptsNamespaces.size() > 0) { final var receipt = mXmppConnectionService .getMessageGenerator() - .received( - account, - packet.getFrom(), - remoteMsgId, - receiptsNamespaces, - packet.getType()); + .received(packet.getFrom(), remoteMsgId, packet.getType()); mXmppConnectionService.sendMessagePacket(account, receipt); } } else if (query.isCatchup()) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 038537dfe28d8051b0fd5aa8021553b06178180a..244da8e40807e568c07f9427c16e4e8a2708dd91 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -77,7 +77,7 @@ public class PresenceParser extends AbstractParser if (item != null && !from.isBareJid()) { mucOptions.setError(MucOptions.Error.NONE); final MucOptions.User user = parseItem(conversation, item, from); - final var occupant = packet.getExtension(OccupantId.class); + final var occupant = packet.getOnlyExtension(OccupantId.class); final String occupantId = mucOptions.occupantId() && occupant != null ? occupant.getId() diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java index 71fe922c158adeb678b30e6181047329e5dc0784..c3c987a5c4c3f4bbcde5868397a676640d60a968 100644 --- a/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java @@ -10,6 +10,11 @@ public class Received extends DeliveryReceipt { super(Received.class); } + public Received(final String id) { + super(Received.class); + this.setId(id); + } + public void setId(String id) { this.setAttribute("id", id); } diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java index 23b0fdcac823de630719bf14f8c47a559a2fbcfd..0078bcbae2be3558658908c98a48e71d6636ac42 100644 --- a/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java +++ b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java @@ -1,8 +1,10 @@ package im.conversations.android.xmpp.model.unique; +import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Map; @XmlElement public class StanzaId extends Extension { @@ -18,4 +20,23 @@ public class StanzaId extends Extension { public String getId() { return this.getAttribute("id"); } + + public static String get( + final im.conversations.android.xmpp.model.stanza.Message packet, final Jid by) { + final var builder = new ImmutableMap.Builder(); + for (final var stanzaId : packet.getExtensions(StanzaId.class)) { + final var id = stanzaId.getId(); + final var byAttribute = Jid.Invalid.getNullForInvalid(stanzaId.getBy()); + if (byAttribute != null && id != null) { + builder.put(byAttribute, id); + } + } + final Map byToId; + try { + byToId = builder.buildOrThrow(); + } catch (final IllegalArgumentException e) { + return null; + } + return byToId.get(by); + } } From 7cf406f944fc62a9fcdf508ac2254bfd30a3003f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 15 Apr 2025 19:20:28 +0200 Subject: [PATCH 15/19] refactor pep event parsing to new api --- .../crypto/axolotl/AxolotlService.java | 1709 +++++++++++------ .../conversations/entities/Bookmark.java | 40 +- .../conversations/parser/MessageParser.java | 133 +- .../services/XmppConnectionService.java | 127 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../siacs/conversations/xmpp/pep/Avatar.java | 163 +- .../xmpp/model/bookmark/Conference.java | 22 - .../android/xmpp/model/bookmark/Storage.java | 12 + .../xmpp/model/bookmark/package-info.java | 4 +- .../xmpp/model/bookmark2/Conference.java | 32 + .../{bookmark => bookmark2}/Extensions.java | 2 +- .../model/{bookmark => bookmark2}/Nick.java | 2 +- .../xmpp/model/bookmark2/package-info.java | 5 + .../android/xmpp/model/mds/Displayed.java | 5 + .../android/xmpp/model/pubsub/Items.java | 5 + .../xmpp/model/pubsub/event/Action.java | 14 + .../xmpp/model/pubsub/event/Delete.java | 11 + .../xmpp/model/pubsub/event/Event.java | 14 +- .../xmpp/model/pubsub/event/Purge.java | 3 +- .../xmpp/model/pubsub/event/Retract.java | 3 +- .../xmpp/model/storage/PrivateStorage.java | 13 + 21 files changed, 1492 insertions(+), 828 deletions(-) create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java rename src/main/java/im/conversations/android/xmpp/model/{bookmark => bookmark2}/Extensions.java (81%) rename src/main/java/im/conversations/android/xmpp/model/{bookmark => bookmark2}/Nick.java (79%) create mode 100644 src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java create mode 100644 src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 938dc794119539f7ff25dc6d6e23b25cc72bb2ee..53757f939072473856f5f8ea8cfbf39dabb66c0d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -6,10 +6,8 @@ import android.os.Bundle; import android.security.KeyChain; import android.util.Log; import android.util.Pair; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -17,38 +15,6 @@ 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 org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.SessionBuilder; -import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.UntrustedIdentityException; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.state.PreKeyBundle; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; - -import java.security.PrivateKey; -import java.security.Security; -import java.security.Signature; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -71,6 +37,35 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportIn import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.model.stanza.Iq; +import java.security.PrivateKey; +import java.security.Security; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.SessionBuilder; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.UntrustedIdentityException; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.state.PreKeyBundle; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.util.KeyHelper; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @@ -102,8 +97,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private int numPublishTriesOnEmptyPep = 0; private boolean pepBroken = false; private int lastDeviceListNotificationHash = 0; - private final Set postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment - private final Set postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup + private final Set postponedSessions = + new HashSet<>(); // sessions stored here will receive after mam catchup treatment + private final Set postponedHealing = + new HashSet<>(); // addresses stored here will need a healing notification after mam + // catchup private final AtomicBoolean changeAccessMode = new AtomicBoolean(false); public AxolotlService(Account account, XmppConnectionService connectionService) { @@ -156,7 +154,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { for (Jid jid : jids) { if (deviceIds.get(jid) != null) { for (Integer foreignId : this.deviceIds.get(jid)) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.toString(), foreignId); if (fetchStatusMap.getAll(address.getName()).containsValue(FetchStatus.ERROR)) { return true; } @@ -167,11 +166,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void preVerifyFingerprint(Contact contact, String fingerprint) { - axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint); + axolotlStore.preVerifyFingerprint( + contact.getAccount(), contact.getJid().asBareJid().toString(), fingerprint); } public void preVerifyFingerprint(Account account, String fingerprint) { - axolotlStore.preVerifyFingerprint(account, account.getJid().asBareJid().toString(), fingerprint); + axolotlStore.preVerifyFingerprint( + account, account.getJid().asBareJid().toString(), fingerprint); } public boolean hasVerifiedKeys(String name) { @@ -184,11 +185,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public String getOwnFingerprint() { - return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); + return CryptoHelper.bytesToHex( + axolotlStore.getIdentityKeyPair().getPublicKey().serialize()); } public Set getKeysWithTrust(FingerprintStatus status) { - return axolotlStore.getContactKeysWithTrust(account.getJid().asBareJid().toString(), status); + return axolotlStore.getContactKeysWithTrust( + account.getJid().asBareJid().toString(), status); } public Set getKeysWithTrust(FingerprintStatus status, Jid jid) { @@ -226,21 +229,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Collection findOwnSessions() { SignalProtocolAddress ownAddress = getAddressForJid(account.getJid().asBareJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); + ArrayList s = + new ArrayList<>(this.sessions.getAll(ownAddress.getName()).values()); Collections.sort(s); return s; } public Collection findSessionsForContact(Contact contact) { SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid()); - ArrayList s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); + ArrayList s = + new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values()); Collections.sort(s); return s; } private Set findSessionsForConversation(Conversation conversation) { if (conversation.getContact().isSelf()) { - //will be added in findOwnSessions() + // will be added in findOwnSessions() return Collections.emptySet(); } HashSet sessions = new HashSet<>(); @@ -280,7 +285,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void destroy() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": destroying old axolotl service. no longer in use"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": destroying old axolotl service. no longer in use"); mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); } @@ -306,7 +314,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final boolean me = jid.asBareJid().equals(account.getJid().asBareJid()); if (me) { if (hash != 0 && hash == this.lastDeviceListNotificationHash) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring duplicate own device id list"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": ignoring duplicate own device id list"); return; } this.lastDeviceListNotificationHash = hash; @@ -315,10 +325,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (me) { deviceIds.remove(getOwnDeviceId()); } - final Set expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); + final Set expiredDevices = + new HashSet<>(axolotlStore.getSubDeviceSessions(jid.asBareJid().toString())); expiredDevices.removeAll(deviceIds); for (Integer deviceId : expiredDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); XmppAxolotlSession session = sessions.get(address); if (session != null && session.getFingerprint() != null) { if (session.getTrust().isActive()) { @@ -328,11 +340,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } final Set newDevices = ImmutableSet.copyOf(deviceIds); for (final Integer deviceId : newDevices) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); XmppAxolotlSession session = sessions.get(address); if (session != null && session.getFingerprint() != null) { if (!session.getTrust().isActive()) { - Log.d(Config.LOGTAG, "reactivating device with fingerprint " + session.getFingerprint()); + Log.d( + Config.LOGTAG, + "reactivating device with fingerprint " + session.getFingerprint()); session.setTrust(session.getTrust().toActive()); } } @@ -343,7 +358,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } needsPublishing |= this.changeAccessMode.get(); for (final Integer deviceId : deviceIds) { - SignalProtocolAddress ownDeviceAddress = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + SignalProtocolAddress ownDeviceAddress = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); if (sessions.get(ownDeviceAddress) == null) { FetchStatus status = fetchStatusMap.get(ownDeviceAddress); if (status == null || status == FetchStatus.TIMEOUT) { @@ -363,7 +379,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final boolean changed = oldSet == null || oldSet.hashCode() != hash; this.deviceIds.put(jid, deviceIds); if (changed) { - mXmppConnectionService.updateConversationUi(); //update the lock icon + mXmppConnectionService.updateConversationUi(); // update the lock icon mXmppConnectionService.keyStatusUpdated(null); if (me) { mXmppConnectionService.updateAccountUi(); @@ -375,7 +391,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void wipeOtherPepDevices() { if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "wipeOtherPepDevices called, but PEP is broken. Ignoring... "); return; } Set deviceIds = new HashSet<>(); @@ -391,22 +410,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void publishOwnDeviceIdIfNeeded() { if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); return; } - Iq packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (response.getType() == Iq.Type.TIMEOUT) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Timeout received while retrieving own Device Ids."); - } else { - //TODO consider calling registerDevices only after item-not-found to account for broken PEPs - final Element item = IqParser.getItem(response); - final Set deviceIds = IqParser.deviceIds(item); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retrieved own device list: " + deviceIds); - registerDevices(account.getJid().asBareJid(), deviceIds); - } - - }); + Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveDeviceIds(account.getJid().asBareJid()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.Type.TIMEOUT) { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Timeout received while retrieving own Device Ids."); + } else { + // TODO consider calling registerDevices only after item-not-found to + // account for broken PEPs + // TODO use new API + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": retrieved own device list: " + + deviceIds); + registerDevices(account.getJid().asBareJid(), deviceIds); + } + }); } private Set getExpiredDevices() { @@ -415,16 +451,34 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getTrust().isActive()) { long diff = System.currentTimeMillis() - session.getTrust().getLastActivation(); if (diff > Config.OMEMO_AUTO_EXPIRY) { - long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account, session.getFingerprint()); + long lastMessageDiff = + System.currentTimeMillis() + - mXmppConnectionService.databaseBackend + .getLastTimeFingerprintUsed( + account, session.getFingerprint()); long hours = Math.round(lastMessageDiff / (1000 * 60.0 * 60.0)); if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) { devices.add(session.getRemoteAddress().getDeviceId()); session.setTrust(session.getTrust().toInactive()); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added own device " + session.getFingerprint() + " to list of expired devices. Last message received " + hours + " hours ago"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": added own device " + + session.getFingerprint() + + " to list of expired devices. Last message received " + + hours + + " hours ago"); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": own device " + session.getFingerprint() + " was active " + hours + " hours ago"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": own device " + + session.getFingerprint() + + " was active " + + hours + + " hours ago"); } - } //TODO print last activation diff + } // TODO print last activation diff } } return devices; @@ -435,12 +489,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids"); if (deviceIdsCopy.isEmpty()) { if (numPublishTriesOnEmptyPep >= publishTriesThreshold) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting..."); + Log.w( + Config.LOGTAG, + getLogprefix(account) + + "Own device publish attempt threshold exceeded, aborting..."); pepBroken = true; return; } else { numPublishTriesOnEmptyPep++; - Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")"); + Log.w( + Config.LOGTAG, + getLogprefix(account) + + "Own device list empty, attempting to publish (try " + + numPublishTriesOnEmptyPep + + ")"); } } else { numPublishTriesOnEmptyPep = 0; @@ -453,74 +515,136 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { publishDeviceIdsAndRefineAccessModel(ids, true); } - private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - final var publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); - mXmppConnectionService.sendIqPacket(account, publish, response -> { - final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; - final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response); - if (firstAttempt && preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for device list. pushing node configuration"); - mXmppConnectionService.pushNodeConfiguration(account, AxolotlService.PEP_DEVICE_LIST, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceIdsAndRefineAccessModel(ids, false); - } - - @Override - public void onPushFailed() { - publishDeviceIdsAndRefineAccessModel(ids, false); + private void publishDeviceIdsAndRefineAccessModel( + final Set ids, final boolean firstAttempt) { + final Bundle publishOptions = + account.getXmppConnection().getFeatures().pepPublishOptions() + ? PublishOptions.openAccess() + : null; + final var publish = + mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); + mXmppConnectionService.sendIqPacket( + account, + publish, + response -> { + final Element error = + response.getType() == Iq.Type.ERROR + ? response.findChild("error") + : null; + final boolean preConditionNotMet = PublishOptions.preconditionNotMet(response); + if (firstAttempt && preConditionNotMet) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": precondition wasn't met for device list. pushing node" + + " configuration"); + mXmppConnectionService.pushNodeConfiguration( + account, + AxolotlService.PEP_DEVICE_LIST, + publishOptions, + new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + publishDeviceIdsAndRefineAccessModel(ids, false); + } + + @Override + public void onPushFailed() { + publishDeviceIdsAndRefineAccessModel(ids, false); + } + }); + } else { + if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": done changing access mode"); + account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (response.getType() == Iq.Type.ERROR) { + if (preConditionNotMet) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": device list pre condition still not met on" + + " second attempt"); + } else if (error != null) { + pepBroken = true; + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Error received while publishing own device id" + + response.findChild("error")); + } + } } }); - } else { - if (AxolotlService.this.changeAccessMode.compareAndSet(true, false)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": done changing access mode"); - account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, false); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (response.getType() == Iq.Type.ERROR) { - if (preConditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": device list pre condition still not met on second attempt"); - } else if (error != null) { - pepBroken = true; - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + response.findChild("error")); - } - - } - } - }); } - public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { + public void publishDeviceVerificationAndBundle( + final SignedPreKeyRecord signedPreKeyRecord, + final Set preKeyRecords, + final boolean announceAfter, + final boolean wipe) { try { IdentityKey axolotlPublicKey = axolotlStore.getIdentityKeyPair().getPublicKey(); - PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); - X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); + PrivateKey x509PrivateKey = + KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); + X509Certificate[] chain = + KeyChain.getCertificateChain( + mXmppConnectionService, account.getPrivateKeyAlias()); Signature verifier = Signature.getInstance("sha256WithRSA"); verifier.initSign(x509PrivateKey, SECURE_RANDOM); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); - final Iq packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": publish verification for device " + getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, PublishOptions.openAccess(), new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, getLogprefix(account) + "configured verification node to be world readable"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, getLogprefix(account) + "unable to set access model on verification node"); - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe); - } - }); - }); + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .publishVerification(signature, chain, getOwnDeviceId()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + ": publish verification for device " + + getOwnDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + String node = AxolotlService.PEP_VERIFICATION + ":" + getOwnDeviceId(); + mXmppConnectionService.pushNodeConfiguration( + account, + node, + PublishOptions.openAccess(), + new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "configured verification node to be world" + + " readable"); + publishDeviceBundle( + signedPreKeyRecord, + preKeyRecords, + announceAfter, + wipe); + } + + @Override + public void onPushFailed() { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "unable to set access model on" + + " verification node"); + publishDeviceBundle( + signedPreKeyRecord, + preKeyRecords, + announceAfter, + wipe); + } + }); + }); } catch (Exception e) { e.printStackTrace(); } @@ -528,175 +652,310 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void publishBundlesIfNeeded(final boolean announce, final boolean wipe) { if (pepBroken) { - Log.d(Config.LOGTAG, getLogprefix(account) + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "publishBundlesIfNeeded called, but PEP is broken. Ignoring... "); return; } if (account.getXmppConnection().getFeatures().pepPublishOptions()) { - this.changeAccessMode.set(account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE)); + this.changeAccessMode.set( + account.isOptionSet(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE)); } else { if (account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server doesn’t support publish-options. setting for later access mode change"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server doesn’t support publish-options. setting for later" + + " access mode change"); mXmppConnectionService.databaseBackend.updateAccount(account); } } if (this.changeAccessMode.get()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model"); - } - final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - - if (response.getType() == Iq.Type.TIMEOUT) { - return; //ignore timeout. do nothing - } - - if (response.getType() == Iq.Type.ERROR) { - Element error = response.findChild("error"); - if (error == null || !error.hasChild("item-not-found")) { - pepBroken = true; - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "request for device bundles came back with something other than item-not-found" + response); - return; - } - } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server gained publish-options capabilities. changing access" + + " model"); + } + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.Type.TIMEOUT) { + return; // ignore timeout. do nothing + } - PreKeyBundle bundle = IqParser.bundle(response); - final Map keys = IqParser.preKeyPublics(response); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + response); - bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); - flush = true; - } - if (keys == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + response); - } - try { - boolean changed = false; - // Validate IdentityKey - IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); - if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); - changed = true; - } + if (response.getType() == Iq.Type.ERROR) { + Element error = response.findChild("error"); + if (error == null || !error.hasChild("item-not-found")) { + pepBroken = true; + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "request for device bundles came back with something" + + " other than item-not-found" + + response); + return; + } + } - // Validate signedPreKeyRecord + ID - SignedPreKeyRecord signedPreKeyRecord; - int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); - try { - signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - if (flush - || !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; + PreKeyBundle bundle = IqParser.bundle(response); + final Map keys = IqParser.preKeyPublics(response); + boolean flush = false; + if (bundle == null) { + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received invalid bundle:" + + response); + bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null); + flush = true; } - } catch (InvalidKeyIdException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); - signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - changed = true; - } + if (keys == null) { + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received invalid prekeys:" + + response); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush + || !identityKeyPair + .getPublicKey() + .equals(bundle.getIdentityKey())) { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Adding own IdentityKey " + + identityKeyPair.getPublicKey() + + " to PEP."); + changed = true; + } - // Validate PreKeys - Set preKeyRecords = new HashSet<>(); - if (keys != null) { - for (Integer id : keys.keySet()) { + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; + int numSignedPreKeys = axolotlStore.getSignedPreKeysCount(); try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - preKeyRecords.add(preKeyRecord); + signedPreKeyRecord = + axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if (flush + || !bundle.getSignedPreKey() + .equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals( + bundle.getSignedPreKeySignature(), + signedPreKeyRecord.getSignature())) { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Adding new signedPreKey with ID " + + (numSignedPreKeys + 1) + + " to PEP."); + signedPreKeyRecord = + KeyHelper.generateSignedPreKey( + identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey( + signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; } - } catch (InvalidKeyIdException ignored) { + } catch (InvalidKeyIdException e) { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Adding new signedPreKey with ID " + + (numSignedPreKeys + 1) + + " to PEP."); + signedPreKeyRecord = + KeyHelper.generateSignedPreKey( + identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey( + signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; } - } - } - int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); - if (newKeys > 0) { - List newRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId() + 1, newKeys); - preKeyRecords.addAll(newRecords); - for (PreKeyRecord record : newRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - changed = true; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP."); - } + // Validate PreKeys + Set preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord + .getKeyPair() + .getPublicKey() + .equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List newRecords = + KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId() + 1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + changed = true; + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Adding " + + newKeys + + " new preKeys to PEP."); + } - if (changed || changeAccessMode.get()) { - if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { - mXmppConnectionService.publishDisplayName(account); - publishDeviceVerificationAndBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } else { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announce, wipe); - } - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Bundle " + getOwnDeviceId() + " in PEP was current"); - if (wipe) { - wipeOtherPepDevices(); - } else if (announce) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); + if (changed || changeAccessMode.get()) { + if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { + mXmppConnectionService.publishDisplayName(account); + publishDeviceVerificationAndBundle( + signedPreKeyRecord, preKeyRecords, announce, wipe); + } else { + publishDeviceBundle( + signedPreKeyRecord, preKeyRecords, announce, wipe); + } + } else { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Bundle " + + getOwnDeviceId() + + " in PEP was current"); + if (wipe) { + wipeOtherPepDevices(); + } else if (announce) { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Announcing device " + + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } + } catch (InvalidKeyException e) { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Failed to publish bundle " + + getOwnDeviceId() + + ", reason: " + + e.getMessage()); } - } - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - }); + }); } - private void publishDeviceBundle(SignedPreKeyRecord signedPreKeyRecord, - Set preKeyRecords, - final boolean announceAfter, - final boolean wipe) { + private void publishDeviceBundle( + SignedPreKeyRecord signedPreKeyRecord, + Set preKeyRecords, + final boolean announceAfter, + final boolean wipe) { publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, true); } - private void publishDeviceBundle(final SignedPreKeyRecord signedPreKeyRecord, - final Set preKeyRecords, - final boolean announceAfter, - final boolean wipe, - final boolean firstAttempt) { - final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - final Iq publish = mXmppConnectionService.getIqGenerator().publishBundles( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, getOwnDeviceId(), publishOptions); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing..."); - mXmppConnectionService.sendIqPacket(account, publish, response -> { - final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response); - if (firstAttempt && preconditionNotMet) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": precondition wasn't met for bundle. pushing node configuration"); - final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); - mXmppConnectionService.pushNodeConfiguration(account, node, publishOptions, new XmppConnectionService.OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); - } - - @Override - public void onPushFailed() { - publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); + private void publishDeviceBundle( + final SignedPreKeyRecord signedPreKeyRecord, + final Set preKeyRecords, + final boolean announceAfter, + final boolean wipe, + final boolean firstAttempt) { + final Bundle publishOptions = + account.getXmppConnection().getFeatures().pepPublishOptions() + ? PublishOptions.openAccess() + : null; + final Iq publish = + mXmppConnectionService + .getIqGenerator() + .publishBundles( + signedPreKeyRecord, + axolotlStore.getIdentityKeyPair().getPublicKey(), + preKeyRecords, + getOwnDeviceId(), + publishOptions); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + ": Bundle " + + getOwnDeviceId() + + " in PEP not current. Publishing..."); + mXmppConnectionService.sendIqPacket( + account, + publish, + response -> { + final boolean preconditionNotMet = PublishOptions.preconditionNotMet(response); + if (firstAttempt && preconditionNotMet) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": precondition wasn't met for bundle. pushing node" + + " configuration"); + final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId(); + mXmppConnectionService.pushNodeConfiguration( + account, + node, + publishOptions, + new XmppConnectionService.OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + publishDeviceBundle( + signedPreKeyRecord, + preKeyRecords, + announceAfter, + wipe, + false); + } + + @Override + public void onPushFailed() { + publishDeviceBundle( + signedPreKeyRecord, + preKeyRecords, + announceAfter, + wipe, + false); + } + }); + } else if (response.getType() == Iq.Type.RESULT) { + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Successfully published bundle. "); + if (wipe) { + wipeOtherPepDevices(); + } else if (announceAfter) { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Announcing device " + + getOwnDeviceId()); + publishOwnDeviceIdIfNeeded(); + } + } else if (response.getType() == Iq.Type.ERROR) { + if (preconditionNotMet) { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "bundle precondition still not met after second" + + " attempt"); + } else { + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Error received while publishing bundle: " + + response.toString()); + } + pepBroken = true; } }); - } else if (response.getType() == Iq.Type.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Successfully published bundle. "); - if (wipe) { - wipeOtherPepDevices(); - } else if (announceAfter) { - Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId()); - publishOwnDeviceIdIfNeeded(); - } - } else if (response.getType() == Iq.Type.ERROR) { - if (preconditionNotMet) { - Log.d(Config.LOGTAG, getLogprefix(account) + "bundle precondition still not met after second attempt"); - } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + response.toString()); - } - pepBroken = true; - } - }); } public void deleteOmemoIdentity() { @@ -734,8 +993,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { mXmppConnectionService.updateAccountUi(); } - private ListenableFuture verifySessionWithPEP(final XmppAxolotlSession session) { - Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); + private ListenableFuture verifySessionWithPEP( + final XmppAxolotlSession session) { + Log.d( + Config.LOGTAG, + "trying to verify fresh session (" + + session.getRemoteAddress().getName() + + ") with pep"); final SignalProtocolAddress address = session.getRemoteAddress(); final IdentityKey identityKey = session.getIdentityKey(); final Jid jid; @@ -747,64 +1011,89 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } final SettableFuture future = SettableFuture.create(); - final Iq packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, response -> { - Pair verification = IqParser.verification(response); - if (verification != null) { - try { - Signature verifier = Signature.getInstance("sha256WithRSA"); - verifier.initVerify(verification.first[0]); - verifier.update(identityKey.serialize()); - if (verifier.verify(verification.second)) { + final Iq packet = + mXmppConnectionService + .getIqGenerator() + .retrieveVerificationForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + Pair verification = IqParser.verification(response); + if (verification != null) { try { - mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - String fingerprint = session.getFingerprint(); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: " + fingerprint); - setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true)); - axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); - fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); - Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); - try { - final String cn = information.getString("subject_cn"); - final Jid jid1 = Jid.of(address.getName()); - Log.d(Config.LOGTAG, "setting common name for " + jid1 + " to " + cn); - account.getRoster().getContact(jid1).setCommonName(cn); - } catch (final IllegalArgumentException ignored) { - //ignored + Signature verifier = Signature.getInstance("sha256WithRSA"); + verifier.initVerify(verification.first[0]); + verifier.update(identityKey.serialize()); + if (verifier.verify(verification.second)) { + try { + mXmppConnectionService + .getMemorizingTrustManager() + .getNonInteractive() + .checkClientTrusted(verification.first, "RSA"); + String fingerprint = session.getFingerprint(); + Log.d( + Config.LOGTAG, + "verified session with x.509 signature. fingerprint" + + " was: " + + fingerprint); + setFingerprintTrust( + fingerprint, + FingerprintStatus.createActiveVerified(true)); + axolotlStore.setFingerprintCertificate( + fingerprint, verification.first[0]); + fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = + CryptoHelper.extractCertificateInformation( + verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid1 = Jid.of(address.getName()); + Log.d( + Config.LOGTAG, + "setting common name for " + jid1 + " to " + cn); + account.getRoster().getContact(jid1).setCommonName(cn); + } catch (final IllegalArgumentException ignored) { + // ignored + } + finishBuildingSessionsFromPEP(address); + future.set(session); + return; + } catch (Exception e) { + Log.d(Config.LOGTAG, "could not verify certificate"); + } } - finishBuildingSessionsFromPEP(address); - future.set(session); - return; } catch (Exception e) { - Log.d(Config.LOGTAG, "could not verify certificate"); + Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); } + } else { + Log.d(Config.LOGTAG, "no verification found"); } - } catch (Exception e) { - Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "no verification found"); - } - fetchStatusMap.put(address, FetchStatus.SUCCESS); - finishBuildingSessionsFromPEP(address); - future.set(session); - }); + fetchStatusMap.put(address, FetchStatus.SUCCESS); + finishBuildingSessionsFromPEP(address); + future.set(session); + }); return future; } private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + SignalProtocolAddress ownAddress = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); Map own = fetchStatusMap.getAll(ownAddress.getName()); Map remote = fetchStatusMap.getAll(address.getName()); if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) { FetchStatus report = null; - if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) { + if (own.containsValue(FetchStatus.SUCCESS) + || remote.containsValue(FetchStatus.SUCCESS)) { report = FetchStatus.SUCCESS; - } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { + } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) + || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { report = FetchStatus.SUCCESS_VERIFIED; - } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { + } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) + || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { report = FetchStatus.SUCCESS_TRUSTED; - } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { + } else if (own.containsValue(FetchStatus.ERROR) + || remote.containsValue(FetchStatus.ERROR)) { report = FetchStatus.ERROR; } mXmppConnectionService.keyStatusUpdated(report); @@ -814,9 +1103,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { boolean publish = false; for (Map.Entry entry : own.entrySet()) { int id = entry.getKey(); - if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) { + if (entry.getValue() == FetchStatus.ERROR + && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) + && ownDeviceIds.remove(id)) { publish = true; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error fetching own device with id " + id + ". removing from announcement"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error fetching own device with id " + + id + + ". removing from announcement"); } } if (publish) { @@ -841,7 +1137,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (callback != null) { callbacks.add(callback); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid + " already running. adding callback"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": fetching device ids for " + + jid + + " already running. adding callback"); packet = null; } else { callbacks = new ArrayList<>(); @@ -849,43 +1150,49 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { callbacks.add(callback); } this.fetchDeviceIdsMap.put(jid, callbacks); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching device ids for " + jid); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": fetching device ids for " + jid); packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(jid); } } if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (response.getType() == Iq.Type.RESULT) { - fetchDeviceListStatus.put(jid, true); - final Element item = IqParser.getItem(response); - final Set deviceIds = IqParser.deviceIds(item); - registerDevices(jid, deviceIds); - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, deviceIds); - } - } - } else { - if (response.getType() == Iq.Type.TIMEOUT) { - fetchDeviceListStatus.remove(jid); - } else { - fetchDeviceListStatus.put(jid, false); - } - final List callbacks; - synchronized (fetchDeviceIdsMap) { - callbacks = fetchDeviceIdsMap.remove(jid); - } - if (callbacks != null) { - for (OnDeviceIdsFetched c : callbacks) { - c.fetched(jid, null); + mXmppConnectionService.sendIqPacket( + account, + packet, + response -> { + if (response.getType() == Iq.Type.RESULT) { + fetchDeviceListStatus.put(jid, true); + // TODO use new API + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); + registerDevices(jid, deviceIds); + final List callbacks; + synchronized (fetchDeviceIdsMap) { + callbacks = fetchDeviceIdsMap.remove(jid); + } + if (callbacks != null) { + for (OnDeviceIdsFetched c : callbacks) { + c.fetched(jid, deviceIds); + } + } + } else { + if (response.getType() == Iq.Type.TIMEOUT) { + fetchDeviceListStatus.remove(jid); + } else { + fetchDeviceListStatus.put(jid, false); + } + final List callbacks; + synchronized (fetchDeviceIdsMap) { + callbacks = fetchDeviceIdsMap.remove(jid); + } + if (callbacks != null) { + for (OnDeviceIdsFetched c : callbacks) { + c.fetched(jid, null); + } + } } - } - } - }); + }); } } @@ -893,126 +1200,194 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final ArrayList unfinishedJids = new ArrayList<>(jids); synchronized (unfinishedJids) { for (Jid jid : unfinishedJids) { - fetchDeviceIds(jid, (j, deviceIds) -> { - synchronized (unfinishedJids) { - unfinishedJids.remove(j); - if (unfinishedJids.size() == 0 && callback != null) { - callback.fetched(); - } - } - }); + fetchDeviceIds( + jid, + (j, deviceIds) -> { + synchronized (unfinishedJids) { + unfinishedJids.remove(j); + if (unfinishedJids.size() == 0 && callback != null) { + callback.fetched(); + } + } + }); } } } - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address) { + private ListenableFuture buildSessionFromPEP( + final SignalProtocolAddress address) { return buildSessionFromPEP(address, null); } - private ListenableFuture buildSessionFromPEP(final SignalProtocolAddress address, OnSessionBuildFromPep callback) { + private ListenableFuture buildSessionFromPEP( + final SignalProtocolAddress address, OnSessionBuildFromPep callback) { final SettableFuture sessionSettableFuture = SettableFuture.create(); - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString()); + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Building new session for " + + address.toString()); if (address.equals(getOwnAxolotlAddress())) { - throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!"); + throw new AssertionError( + "We should NEVER build a session with ourselves. What happened here?!"); } final Jid jid = Jid.of(address.getName()); final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); - final Iq bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, packet -> { - if (packet.getType() == Iq.Type.TIMEOUT) { - fetchStatusMap.put(address, FetchStatus.TIMEOUT); - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. Timeout")); - } else if (packet.getType() == Iq.Type.RESULT) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final List preKeyBundleList = IqParser.preKeys(packet); - final PreKeyBundle bundle = IqParser.bundle(packet); - if (preKeyBundleList.isEmpty() || bundle == null) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Invalid")); - return; - } - Random random = new Random(); - final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (preKey == null) { - //should never happen - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. No suitable PreKey found")); - return; - } + final Iq bundlesPacket = + mXmppConnectionService + .getIqGenerator() + .retrieveBundlesForDevice(jid, address.getDeviceId()); + mXmppConnectionService.sendIqPacket( + account, + bundlesPacket, + packet -> { + if (packet.getType() == Iq.Type.TIMEOUT) { + fetchStatusMap.put(address, FetchStatus.TIMEOUT); + sessionSettableFuture.setException( + new CryptoFailedException("Unable to build session. Timeout")); + } else if (packet.getType() == Iq.Type.RESULT) { + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received preKey IQ packet, processing..."); + final List preKeyBundleList = IqParser.preKeys(packet); + final PreKeyBundle bundle = IqParser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "preKey IQ packet invalid: " + + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildFailed(); + } + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. IQ Packet Invalid")); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = + preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + // should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildFailed(); + } + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. No suitable PreKey found")); + return; + } - final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), - preKey.getPreKeyId(), preKey.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + final PreKeyBundle preKeyBundle = + new PreKeyBundle( + 0, + address.getDeviceId(), + preKey.getPreKeyId(), + preKey.getPreKey(), + bundle.getSignedPreKeyId(), + bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), + bundle.getIdentityKey()); - try { - SessionBuilder builder = new SessionBuilder(axolotlStore, address); - builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); - sessions.put(address, session); - if (Config.X509_VERIFICATION) { - sessionSettableFuture.setFuture(verifySessionWithPEP(session)); //TODO; maybe inject callback in here too - } else { - FingerprintStatus status = getFingerprintTrust(CryptoHelper.bytesToHex(bundle.getIdentityKey().getPublicKey().serialize())); - FetchStatus fetchStatus; - if (status != null && status.isVerified()) { - fetchStatus = FetchStatus.SUCCESS_VERIFIED; - } else if (status != null && status.isTrusted()) { - fetchStatus = FetchStatus.SUCCESS_TRUSTED; - } else { - fetchStatus = FetchStatus.SUCCESS; + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = + new XmppAxolotlSession( + account, + axolotlStore, + address, + bundle.getIdentityKey()); + sessions.put(address, session); + if (Config.X509_VERIFICATION) { + sessionSettableFuture.setFuture( + verifySessionWithPEP( + session)); // TODO; maybe inject callback in here + // too + } else { + FingerprintStatus status = + getFingerprintTrust( + CryptoHelper.bytesToHex( + bundle.getIdentityKey() + .getPublicKey() + .serialize())); + FetchStatus fetchStatus; + if (status != null && status.isVerified()) { + fetchStatus = FetchStatus.SUCCESS_VERIFIED; + } else if (status != null && status.isTrusted()) { + fetchStatus = FetchStatus.SUCCESS_TRUSTED; + } else { + fetchStatus = FetchStatus.SUCCESS; + } + fetchStatusMap.put(address, fetchStatus); + finishBuildingSessionsFromPEP(address); + if (callback != null) { + callback.onSessionBuildSuccessful(); + } + sessionSettableFuture.set(session); + } + } catch (UntrustedIdentityException | InvalidKeyException e) { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Error building session for " + + address + + ": " + + e.getClass().getName() + + ", " + + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + finishBuildingSessionsFromPEP(address); + if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { + removeFromDeviceAnnouncement(address.getDeviceId()); + } + if (callback != null) { + callback.onSessionBuildFailed(); + } + sessionSettableFuture.setException(new CryptoFailedException(e)); } - fetchStatusMap.put(address, fetchStatus); + } else { + fetchStatusMap.put(address, FetchStatus.ERROR); + Element error = packet.findChild("error"); + boolean itemNotFound = error != null && error.hasChild("item-not-found"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "Error received while building session:" + + packet.findChild("error")); finishBuildingSessionsFromPEP(address); + if (oneOfOurs + && itemNotFound + && cleanedOwnDeviceIds.add(address.getDeviceId())) { + removeFromDeviceAnnouncement(address.getDeviceId()); + } if (callback != null) { - callback.onSessionBuildSuccessful(); + callback.onSessionBuildFailed(); } - sessionSettableFuture.set(session); + sessionSettableFuture.setException( + new CryptoFailedException( + "Unable to build session. IQ Packet Error")); } - } catch (UntrustedIdentityException | InvalidKeyException e) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": " - + e.getClass().getName() + ", " + e.getMessage()); - fetchStatusMap.put(address, FetchStatus.ERROR); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException(e)); - } - } else { - fetchStatusMap.put(address, FetchStatus.ERROR); - Element error = packet.findChild("error"); - boolean itemNotFound = error != null && error.hasChild("item-not-found"); - Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while building session:" + packet.findChild("error")); - finishBuildingSessionsFromPEP(address); - if (oneOfOurs && itemNotFound && cleanedOwnDeviceIds.add(address.getDeviceId())) { - removeFromDeviceAnnouncement(address.getDeviceId()); - } - if (callback != null) { - callback.onSessionBuildFailed(); - } - sessionSettableFuture.setException(new CryptoFailedException("Unable to build session. IQ Packet Error")); - } - }); + }); return sessionSettableFuture; } private void removeFromDeviceAnnouncement(Integer id) { HashSet temp = new HashSet<>(getOwnDeviceIds()); if (temp.remove(id)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " remove own device id " + id + " from announcement. devices left:" + temp); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + " remove own device id " + + id + + " from announcement. devices left:" + + temp); publishOwnDeviceId(temp); } } @@ -1020,47 +1395,95 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public Set findDevicesWithoutSession(final Conversation conversation) { Set addresses = new HashSet<>(); for (Jid jid : getCryptoTargets(conversation)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Finding devices without session for " + + jid); final Set ids = deviceIds.get(jid); if (ids != null && !ids.isEmpty()) { for (Integer foreignId : ids) { - SignalProtocolAddress address = new SignalProtocolAddress(jid.toString(), foreignId); + SignalProtocolAddress address = + new SignalProtocolAddress(jid.toString(), foreignId); if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + axolotlStore + .loadSession(address) + .getSessionState() + .getRemoteIdentityKey(); if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already have session for " + + address.toString() + + ", adding to cache..."); + XmppAxolotlSession session = + new XmppAxolotlSession( + account, axolotlStore, address, identityKey); sessions.put(address, session); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Found device " + + jid + + ":" + + foreignId); if (fetchStatusMap.get(address) != FetchStatus.ERROR) { addresses.add(address); } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "skipping over " + + address + + " because it's broken"); } } } } } else { mXmppConnectionService.keyStatusUpdated(FetchStatus.ERROR); - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } } Set ownIds = this.deviceIds.get(account.getJid().asBareJid()); for (Integer ownId : (ownIds != null ? ownIds : new HashSet())) { - SignalProtocolAddress address = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); + SignalProtocolAddress address = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownId); if (sessions.get(address) == null) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); if (identityKey != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already have session for " + + address.toString() + + ", adding to cache..."); + XmppAxolotlSession session = + new XmppAxolotlSession(account, axolotlStore, address, identityKey); sessions.put(address, session); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().asBareJid() + ":" + ownId); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Found device " + + account.getJid().asBareJid() + + ":" + + ownId); if (fetchStatusMap.get(address) != FetchStatus.ERROR) { addresses.add(address); } else { - Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken"); + Log.d( + Config.LOGTAG, + getLogprefix(account) + + "skipping over " + + address + + " because it's broken"); } } } @@ -1077,9 +1500,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { iterator.remove(); } } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": createSessionsIfNeeded() - jids with empty device list: " + jidsWithEmptyDeviceList); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": createSessionsIfNeeded() - jids with empty device list: " + + jidsWithEmptyDeviceList); if (jidsWithEmptyDeviceList.size() > 0) { - fetchDeviceIds(jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); + fetchDeviceIds( + jidsWithEmptyDeviceList, () -> createSessionsIfNeededActual(conversation)); return true; } else { return createSessionsIfNeededActual(conversation); @@ -1087,11 +1515,17 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private boolean createSessionsIfNeededActual(final Conversation conversation) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); boolean newSessions = false; Set addresses = findDevicesWithoutSession(conversation); for (SignalProtocolAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Processing device: " + + address.toString()); FetchStatus status = fetchStatusMap.get(address); if (status == null || status == FetchStatus.TIMEOUT) { fetchStatusMap.put(address, FetchStatus.PENDING); @@ -1100,7 +1534,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } else if (status == FetchStatus.PENDING) { newSessions = true; } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Already fetching bundle for " + + address.toString()); } } @@ -1125,14 +1563,19 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public boolean hasPendingKeyFetches(List jids) { - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); + SignalProtocolAddress ownAddress = + new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0); if (fetchStatusMap.getAll(ownAddress.getName()).containsValue(FetchStatus.PENDING)) { return true; } synchronized (this.fetchDeviceIdsMap) { for (Jid jid : jids) { - SignalProtocolAddress foreignAddress = new SignalProtocolAddress(jid.asBareJid().toString(), 0); - if (fetchStatusMap.getAll(foreignAddress.getName()).containsValue(FetchStatus.PENDING) || this.fetchDeviceIdsMap.containsKey(jid)) { + SignalProtocolAddress foreignAddress = + new SignalProtocolAddress(jid.asBareJid().toString(), 0); + if (fetchStatusMap + .getAll(foreignAddress.getName()) + .containsValue(FetchStatus.PENDING) + || this.fetchDeviceIdsMap.containsKey(jid)) { return true; } } @@ -1143,7 +1586,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @Nullable private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation c) { Set remoteSessions = findSessionsForConversation(c); - final boolean acceptEmpty = (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) || c.getContact().isSelf(); + final boolean acceptEmpty = + (c.getMode() == Conversation.MODE_MULTI && c.getMucOptions().getUserCount() == 0) + || c.getContact().isSelf(); Collection ownSessions = findOwnSessions(); if (remoteSessions.isEmpty() && !acceptEmpty) { return false; @@ -1158,7 +1603,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return true; } - //this is being used for private muc messages only + // this is being used for private muc messages only private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Jid jid) { if (jid == null) { return false; @@ -1177,7 +1622,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @Nullable public XmppAxolotlMessage encrypt(Message message) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final String content; if (message.hasFileOnRemoteHost()) { content = message.getFileParams().url; @@ -1187,7 +1633,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { try { axolotlMessage.encrypt(content); } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); + Log.w( + Config.LOGTAG, + getLogprefix(account) + "Failed to encrypt message: " + e.getMessage()); return null; } @@ -1201,31 +1649,42 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void preparePayloadMessage(final Message message, final boolean delay) { - executor.execute(new Runnable() { - @Override - public void run() { - XmppAxolotlMessage axolotlMessage = encrypt(message); - if (axolotlMessage == null) { - mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); - //mXmppConnectionService.updateConversationUi(); - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid()); - messageCache.put(message.getUuid(), axolotlMessage); - mXmppConnectionService.resendMessage(message, delay); - } - } - }); + executor.execute( + new Runnable() { + @Override + public void run() { + XmppAxolotlMessage axolotlMessage = encrypt(message); + if (axolotlMessage == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + // mXmppConnectionService.updateConversationUi(); + } else { + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Generated message, caching: " + + message.getUuid()); + messageCache.put(message.getUuid(), axolotlMessage); + mXmppConnectionService.resendMessage(message, delay); + } + } + }); } - private OmemoVerifiedIceUdpTransportInfo encrypt(final IceUdpTransportInfo element, final XmppAxolotlSession session) throws CryptoFailedException { - final OmemoVerifiedIceUdpTransportInfo transportInfo = new OmemoVerifiedIceUdpTransportInfo(); + private OmemoVerifiedIceUdpTransportInfo encrypt( + final IceUdpTransportInfo element, final XmppAxolotlSession session) + throws CryptoFailedException { + final OmemoVerifiedIceUdpTransportInfo transportInfo = + new OmemoVerifiedIceUdpTransportInfo(); transportInfo.setAttributes(element.getAttributes()); for (final Element child : element.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { - final Element fingerprint = new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); + if ("fingerprint".equals(child.getName()) + && Namespace.JINGLE_APPS_DTLS.equals(child.getNamespace())) { + final Element fingerprint = + new Element("fingerprint", Namespace.OMEMO_DTLS_SRTP_VERIFICATION); fingerprint.setAttribute("setup", child.getAttribute("setup")); fingerprint.setAttribute("hash", child.getAttribute("hash")); - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); final String content = child.getContent(); axolotlMessage.encrypt(content); axolotlMessage.addDevice(session, true); @@ -1238,25 +1697,29 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return transportInfo; } - - public ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { + public ListenableFuture> encrypt( + final RtpContentMap rtpContentMap, final Jid jid, final int deviceId) { return Futures.transformAsync( getSession(jid, deviceId), session -> encrypt(rtpContentMap, session), - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } - private ListenableFuture> encrypt(final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { + private ListenableFuture> encrypt( + final RtpContentMap rtpContentMap, final XmppAxolotlSession session) { if (Config.REQUIRE_RTP_VERIFICATION) { requireVerification(session); } - final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>(); + final ImmutableMap.Builder< + String, DescriptionTransport> + descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); omemoVerification.setDeviceId(session.getRemoteAddress().getDeviceId()); omemoVerification.setSessionFingerprint(session.getFingerprint()); - for (final Map.Entry> content : rtpContentMap.contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + for (final Map.Entry> + content : rtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = + content.getValue(); final OmemoVerifiedIceUdpTransportInfo encryptedTransportInfo; try { encryptedTransportInfo = encrypt(descriptionTransport.transport, session); @@ -1265,18 +1728,21 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } descriptionTransportBuilder.put( content.getKey(), - new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, encryptedTransportInfo) - ); + new DescriptionTransport<>( + descriptionTransport.senders, + descriptionTransport.description, + encryptedTransportInfo)); } return Futures.immediateFuture( new OmemoVerifiedPayload<>( omemoVerification, - new OmemoVerifiedRtpContentMap(rtpContentMap.group, descriptionTransportBuilder.build()) - )); + new OmemoVerifiedRtpContentMap( + rtpContentMap.group, descriptionTransportBuilder.build()))); } private ListenableFuture getSession(final Jid jid, final int deviceId) { - final SignalProtocolAddress address = new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); + final SignalProtocolAddress address = + new SignalProtocolAddress(jid.asBareJid().toString(), deviceId); final XmppAxolotlSession session = sessions.get(address); if (session == null) { return buildSessionFromPEP(address); @@ -1284,26 +1750,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } - public ListenableFuture> decrypt(OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { - final ImmutableMap.Builder> descriptionTransportBuilder = new ImmutableMap.Builder<>(); + public ListenableFuture> decrypt( + OmemoVerifiedRtpContentMap omemoVerifiedRtpContentMap, final Jid from) { + final ImmutableMap.Builder< + String, DescriptionTransport> + descriptionTransportBuilder = new ImmutableMap.Builder<>(); final OmemoVerification omemoVerification = new OmemoVerification(); - final ImmutableList.Builder> pepVerificationFutures = new ImmutableList.Builder<>(); - for (final Map.Entry> content : omemoVerifiedRtpContentMap.contents.entrySet()) { - final DescriptionTransport descriptionTransport = content.getValue(); + final ImmutableList.Builder> pepVerificationFutures = + new ImmutableList.Builder<>(); + for (final Map.Entry> + content : omemoVerifiedRtpContentMap.contents.entrySet()) { + final DescriptionTransport descriptionTransport = + content.getValue(); final OmemoVerifiedPayload decryptedTransport; try { - decryptedTransport = decrypt((OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, from, pepVerificationFutures); + decryptedTransport = + decrypt( + (OmemoVerifiedIceUdpTransportInfo) descriptionTransport.transport, + from, + pepVerificationFutures); } catch (CryptoFailedException e) { return Futures.immediateFailedFuture(e); } omemoVerification.setOrEnsureEqual(decryptedTransport); descriptionTransportBuilder.put( content.getKey(), - new DescriptionTransport<>(descriptionTransport.senders, descriptionTransport.description, decryptedTransport.payload) - ); + new DescriptionTransport<>( + descriptionTransport.senders, + descriptionTransport.description, + decryptedTransport.payload)); } processPostponed(); - final ImmutableList> sessionFutures = pepVerificationFutures.build(); + final ImmutableList> sessionFutures = + pepVerificationFutures.build(); return Futures.transform( Futures.allAsList(sessionFutures), sessions -> { @@ -1314,27 +1793,35 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } return new OmemoVerifiedPayload<>( omemoVerification, - new RtpContentMap(omemoVerifiedRtpContentMap.group, descriptionTransportBuilder.build()) - ); - + new RtpContentMap( + omemoVerifiedRtpContentMap.group, + descriptionTransportBuilder.build())); }, - MoreExecutors.directExecutor() - ); + MoreExecutors.directExecutor()); } - private OmemoVerifiedPayload decrypt(final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, final Jid from, ImmutableList.Builder> pepVerificationFutures) throws CryptoFailedException { + private OmemoVerifiedPayload decrypt( + final OmemoVerifiedIceUdpTransportInfo verifiedIceUdpTransportInfo, + final Jid from, + ImmutableList.Builder> pepVerificationFutures) + throws CryptoFailedException { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(verifiedIceUdpTransportInfo.getAttributes()); final OmemoVerification omemoVerification = new OmemoVerification(); for (final Element child : verifiedIceUdpTransportInfo.getChildren()) { - if ("fingerprint".equals(child.getName()) && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { + if ("fingerprint".equals(child.getName()) + && Namespace.OMEMO_DTLS_SRTP_VERIFICATION.equals(child.getNamespace())) { final Element fingerprint = new Element("fingerprint", Namespace.JINGLE_APPS_DTLS); fingerprint.setAttribute("setup", child.getAttribute("setup")); fingerprint.setAttribute("hash", child.getAttribute("hash")); - final Element encrypted = child.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); - final XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); + final Element encrypted = + child.findChildEnsureSingle( + XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); + final XmppAxolotlMessage xmppAxolotlMessage = + XmppAxolotlMessage.fromElement(encrypted, from.asBareJid()); final XmppAxolotlSession session = getReceivingSession(xmppAxolotlMessage); - final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); + final XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintext = + xmppAxolotlMessage.decrypt(session, getOwnDeviceId()); final Integer preKeyId = session.getPreKeyIdAndReset(); if (preKeyId != null) { postponedSessions.add(session); @@ -1359,45 +1846,53 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getTrust().isVerified()) { return; } - throw new NotVerifiedException(String.format( - "session with %s was not verified", - session.getFingerprint() - )); + throw new NotVerifiedException( + String.format("session with %s was not verified", session.getFingerprint())); } - public ListenableFuture prepareKeyTransportMessage(final Conversation conversation) { - return Futures.submit(()->{ - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); - if (buildHeader(axolotlMessage, conversation)) { - return axolotlMessage; - } else { - throw new IllegalStateException("No session to decrypt to"); - } - },executor); + public ListenableFuture prepareKeyTransportMessage( + final Conversation conversation) { + return Futures.submit( + () -> { + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + if (buildHeader(axolotlMessage, conversation)) { + return axolotlMessage; + } else { + throw new IllegalStateException("No session to decrypt to"); + } + }, + executor); } public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) { XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid()); if (axolotlMessage != null) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid()); messageCache.remove(message.getUuid()); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid()); } return axolotlMessage; } private XmppAxolotlSession recreateUncachedSession(SignalProtocolAddress address) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); return (identityKey != null) ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) : null; } private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) { - SignalProtocolAddress senderAddress = new SignalProtocolAddress(message.getFrom().toString(), message.getSenderDeviceId()); + SignalProtocolAddress senderAddress = + new SignalProtocolAddress( + message.getFrom().toString(), message.getSenderDeviceId()); return getReceivingSession(senderAddress); - } private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) { @@ -1411,7 +1906,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return session; } - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException { + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage( + XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) + throws NotEncryptedForThisDeviceException, + BrokenSessionException, + OutdatedSenderException { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; XmppAxolotlSession session = getReceivingSession(message); @@ -1423,7 +1922,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { postPreKeyMessageHandling(session, postponePreKeyMessageHandling); } } catch (NotEncryptedForThisDeviceException e) { - if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) && message.getSenderDeviceId() == ownDeviceId) { + if (account.getJid().asBareJid().equals(message.getFrom().asBareJid()) + && message.getSenderDeviceId() == ownDeviceId) { Log.w(Config.LOGTAG, getLogprefix(account) + "Reflected omemo message received"); } else { throw e; @@ -1434,7 +1934,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage()); throw e; } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e); + Log.w( + Config.LOGTAG, + getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), + e); } if (session.isFresh() && plaintextMessage != null) { @@ -1445,7 +1948,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } public void reportBrokenSessionException(BrokenSessionException e, boolean postpone) { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": broken session with " + e.getSignalProtocolAddress().toString() + " detected", e); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": broken session with " + + e.getSignalProtocolAddress().toString() + + " detected", + e); if (postpone) { postponedHealing.add(e.getSignalProtocolAddress()); } else { @@ -1455,32 +1964,51 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void notifyRequiresHealing(final SignalProtocolAddress signalProtocolAddress) { if (healingAttempts.add(signalProtocolAddress)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": attempt to heal " + signalProtocolAddress); - buildSessionFromPEP(signalProtocolAddress, new OnSessionBuildFromPep() { - @Override - public void onSessionBuildSuccessful() { - Log.d(Config.LOGTAG, "successfully build new session from pep after detecting broken session"); - completeSession(getReceivingSession(signalProtocolAddress)); - } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": attempt to heal " + signalProtocolAddress); + buildSessionFromPEP( + signalProtocolAddress, + new OnSessionBuildFromPep() { + @Override + public void onSessionBuildSuccessful() { + Log.d( + Config.LOGTAG, + "successfully build new session from pep after detecting broken" + + " session"); + completeSession(getReceivingSession(signalProtocolAddress)); + } - @Override - public void onSessionBuildFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to build new session from pep after detecting broken session"); - } - }); + @Override + public void onSessionBuildFailed() { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to build new session from pep after" + + " detecting broken session"); + } + }); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not attempt to heal " + signalProtocolAddress + " again"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": do not attempt to heal " + + signalProtocolAddress + + " again"); } } - private void postPreKeyMessageHandling(final XmppAxolotlSession session, final boolean postpone) { + private void postPreKeyMessageHandling( + final XmppAxolotlSession session, final boolean postpone) { if (postpone) { postponedSessions.add(session); } else { if (axolotlStore.flushPreKeys()) { publishBundlesIfNeeded(false, false); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": nothing to flush. Not republishing key"); } if (trustedOrPreviouslyResponded(session) && Config.AUTOMATICALLY_COMPLETE_SESSIONS) { completeSession(session); @@ -1502,7 +2030,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } iterator.remove(); } - final Iterator postponedHealingAttemptsIterator = postponedHealing.iterator(); + final Iterator postponedHealingAttemptsIterator = + postponedHealing.iterator(); while (postponedHealingAttemptsIterator.hasNext()) { notifyRequiresHealing(postponedHealingAttemptsIterator.next()); postponedHealingAttemptsIterator.remove(); @@ -1527,18 +2056,24 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void completeSession(XmppAxolotlSession session) { - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); + final XmppAxolotlMessage axolotlMessage = + new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId()); axolotlMessage.addDevice(session, true); try { final Jid jid = Jid.of(session.getRemoteAddress().getName()); - final var packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage); + final var packet = + mXmppConnectionService + .getMessageGenerator() + .generateKeyTransportMessage(jid, axolotlMessage); mXmppConnectionService.sendMessagePacket(account, packet); } catch (IllegalArgumentException e) { - throw new Error("Remote addresses are created from jid and should convert back to jid", e); + throw new Error( + "Remote addresses are created from jid and should convert back to jid", e); } } - public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { + public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage( + XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) { final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; final XmppAxolotlSession session = getReceivingSession(message); try { @@ -1565,7 +2100,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (session.getIdentityKey() != null) { return verifySessionWithPEP(session); } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": identity key was empty after reloading for x509 verification"); + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": identity key was empty after reloading for x509 verification"); } } return Futures.immediateFuture(session); @@ -1584,7 +2122,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { void fetched(Jid jid, Set deviceIds); } - public interface OnMultipleDeviceIdFetched { void fetched(); } @@ -1644,14 +2181,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public void clear() { map.clear(); } - } private static class SessionMap extends AxolotlAddressMap { private final XmppConnectionService xmppConnectionService; private final Account account; - public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { + public SessionMap( + XmppConnectionService service, SQLiteAxolotlStore store, Account account) { super(); this.xmppConnectionService = service; this.account = account; @@ -1671,30 +2208,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return candidates; } - private void putDevicesForJid(String bareJid, List deviceIds, SQLiteAxolotlStore store) { + private void putDevicesForJid( + String bareJid, List deviceIds, SQLiteAxolotlStore store) { for (Integer deviceId : deviceIds) { SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId); - IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + IdentityKey identityKey = + store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); if (Config.X509_VERIFICATION) { - X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize())); + X509Certificate certificate = + store.getFingerprintCertificate( + CryptoHelper.bytesToHex( + identityKey.getPublicKey().serialize())); if (certificate != null) { - Bundle information = CryptoHelper.extractCertificateInformation(certificate); + Bundle information = + CryptoHelper.extractCertificateInformation(certificate); try { final String cn = information.getString("subject_cn"); final Jid jid = Jid.of(bareJid); Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn); account.getRoster().getContact(jid).setCommonName(cn); } catch (final IllegalArgumentException ignored) { - //ignored + // ignored } } } - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); + this.put( + axolotlAddress, + new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); } } private void fillMap(SQLiteAxolotlStore store) { - List deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString()); + List deviceIds = + store.getSubDeviceSessions(account.getJid().asBareJid().toString()); putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store); for (String address : store.getKnownAddresses()) { deviceIds = store.getSubDeviceSessions(address); @@ -1723,7 +2269,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } for (Map.Entry entry : devices.entrySet()) { if (entry.getValue() == FetchStatus.ERROR) { - Log.d(Config.LOGTAG, "resetting error for " + jid.asBareJid() + "(" + entry.getKey() + ")"); + Log.d( + Config.LOGTAG, + "resetting error for " + + jid.asBareJid() + + "(" + + entry.getKey() + + ")"); entry.setValue(FetchStatus.TIMEOUT); } } @@ -1760,6 +2312,5 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { public NotVerifiedException(String message) { super(message); } - } } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 2a009482806658f8a7da4dec44796f988dd2d65c..41bd8c978a85bd32a32546f55f88a67d26310ae7 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -10,6 +10,9 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.bookmark2.Conference; +import im.conversations.android.xmpp.model.pubsub.PubSub; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashMap; @@ -36,7 +39,8 @@ public class Bookmark extends Element implements ListItem { this.account = account; } - public static Map parseFromStorage(Element storage, Account account) { + public static Map parseFromStorage( + final Storage storage, final Account account) { if (storage == null) { return Collections.emptyMap(); } @@ -57,24 +61,24 @@ public class Bookmark extends Element implements ListItem { return bookmarks; } - public static Map parseFromPubSub(final Element pubSub, final Account account) { + public static Map parseFromPubSub(final PubSub pubSub, final Account account) { if (pubSub == null) { return Collections.emptyMap(); } - final Element items = pubSub.findChild("items"); - if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) { - final Map bookmarks = new HashMap<>(); - for (Element item : items.getChildren()) { - if (item.getName().equals("item")) { - final Bookmark bookmark = Bookmark.parseFromItem(item, account); - if (bookmark != null) { - bookmarks.put(bookmark.jid, bookmark); - } - } + final var items = pubSub.getItems(); + if (items == null || !Namespace.BOOKMARKS2.equals(items.getNode())) { + return Collections.emptyMap(); + } + final Map bookmarks = new HashMap<>(); + for (final var item : items.getItemMap(Conference.class).entrySet()) { + final Bookmark bookmark = + Bookmark.parseFromItem(item.getKey(), item.getValue(), account); + if (bookmark == null) { + continue; } - return bookmarks; + bookmarks.put(bookmark.jid, bookmark); } - return Collections.emptyMap(); + return bookmarks; } public static Bookmark parse(Element element, Account account) { @@ -88,13 +92,13 @@ public class Bookmark extends Element implements ListItem { return bookmark; } - public static Bookmark parseFromItem(Element item, Account account) { - final Element conference = item.findChild("conference", Namespace.BOOKMARKS2); - if (conference == null) { + public static Bookmark parseFromItem( + final String id, final Conference conference, final Account account) { + if (id == null || conference == null) { return null; } final Bookmark bookmark = new Bookmark(account); - bookmark.jid = Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id")); + bookmark.jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(id)); // TODO verify that we only use bare jids and ignore full jids if (bookmark.jid == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index b6194ccd1be5f90ac229a85b32e3143fa8356aba..83e817c2b5c95eaf42ab315d3b3291dc943bb0f4 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; +import androidx.annotation.NonNull; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.AppSettings; @@ -37,15 +38,23 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.avatar.Metadata; +import im.conversations.android.xmpp.model.axolotl.DeviceList; import im.conversations.android.xmpp.model.axolotl.Encrypted; +import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.bookmark2.Conference; import im.conversations.android.xmpp.model.carbons.Received; import im.conversations.android.xmpp.model.carbons.Sent; import im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.forward.Forwarded; import im.conversations.android.xmpp.model.markers.Displayed; +import im.conversations.android.xmpp.model.nick.Nick; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.oob.OutOfBandData; +import im.conversations.android.xmpp.model.pubsub.Items; +import im.conversations.android.xmpp.model.pubsub.event.Delete; import im.conversations.android.xmpp.model.pubsub.event.Event; +import im.conversations.android.xmpp.model.pubsub.event.Purge; import im.conversations.android.xmpp.model.reactions.Reactions; import im.conversations.android.xmpp.model.receipts.Request; import im.conversations.android.xmpp.model.unique.StanzaId; @@ -53,6 +62,7 @@ import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -244,11 +254,13 @@ public class MessageParser extends AbstractParser return null; } - private void parseEvent(final Event event, final Jid from, final Account account) { - final Element items = event.findChild("items"); - final String node = items == null ? null : items.getAttribute("node"); + private void parseEvent(final Items items, final Jid from, final Account account) { + final String node = items.getNode(); if ("urn:xmpp:avatar:metadata".equals(node)) { - Avatar avatar = Avatar.parseMetadata(items); + // TODO support retract + final var entry = items.getFirstItemWithId(Metadata.class); + final var avatar = + entry == null ? null : Avatar.parseMetadata(entry.getKey(), entry.getValue()); if (avatar != null) { avatar.owner = from.asBareJid(); if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { @@ -274,24 +286,27 @@ public class MessageParser extends AbstractParser } } } else if (Namespace.NICK.equals(node)) { - final Element i = items.findChild("item"); - final String nick = i == null ? null : i.findChildContent("nick", Namespace.NICK); + final var nickItem = items.getFirstItem(Nick.class); + final String nick = nickItem == null ? null : nickItem.getContent(); if (nick != null) { setNick(account, from, nick); } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Element item = items.findChild("item"); - final Set deviceIds = IqParser.deviceIds(item); - Log.d( - Config.LOGTAG, - AxolotlService.getLogprefix(account) - + "Received PEP device list " - + deviceIds - + " update from " - + from - + ", processing... "); - final AxolotlService axolotlService = account.getAxolotlService(); - axolotlService.registerDevices(from, deviceIds); + final var deviceList = items.getFirstItem(DeviceList.class); + if (deviceList != null) { + final Set deviceIds = deviceList.getDeviceIds(); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Received PEP device list " + + deviceIds + + " update from " + + from + + ", processing... "); + final AxolotlService axolotlService = account.getAxolotlService(); + axolotlService.registerDevices(from, new HashSet<>(deviceIds)); + } + } else if (Namespace.BOOKMARKS.equals(node) && account.getJid().asBareJid().equals(from)) { final var connection = account.getXmppConnection(); if (connection.getFeatures().bookmarksConversion()) { @@ -302,9 +317,7 @@ public class MessageParser extends AbstractParser + ": received storage:bookmark notification even though we" + " opted into bookmarks:1"); } - final Element i = items.findChild("item"); - final Element storage = - i == null ? null : i.findChild("storage", Namespace.BOOKMARKS); + final var storage = items.getFirstItem(Storage.class); final Map bookmarks = Bookmark.parseFromStorage(storage, account); mXmppConnectionService.processBookmarksInitial(account, bookmarks, true); Log.d( @@ -318,17 +331,19 @@ public class MessageParser extends AbstractParser + " not detected"); } } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { - final Element item = items.findChild("item"); - final Element retract = items.findChild("retract"); - if (item != null) { - final Bookmark bookmark = Bookmark.parseFromItem(item, account); - if (bookmark != null) { - account.putBookmark(bookmark); - mXmppConnectionService.processModifiedBookmark(bookmark); - mXmppConnectionService.updateConversationUi(); + final var retractions = items.getRetractions(); + ; + for (final var item : items.getItemMap(Conference.class).entrySet()) { + final Bookmark bookmark = + Bookmark.parseFromItem(item.getKey(), item.getValue(), account); + if (bookmark == null) { + continue; } + account.putBookmark(bookmark); + mXmppConnectionService.processModifiedBookmark(bookmark); + mXmppConnectionService.updateConversationUi(); } - if (retract != null) { + for (final var retract : retractions) { final Jid id = Jid.Invalid.getNullForInvalid(retract.getAttributeAsJid("id")); if (id != null) { account.removeBookmark(id); @@ -342,35 +357,37 @@ public class MessageParser extends AbstractParser } else if (Config.MESSAGE_DISPLAYED_SYNCHRONIZATION && Namespace.MDS_DISPLAYED.equals(node) && account.getJid().asBareJid().equals(from)) { - final Element item = items.findChild("item"); - mXmppConnectionService.processMdsItem(account, item); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + " received pubsub notification for node=" - + node); + for (final var item : + items.getItemMap(im.conversations.android.xmpp.model.mds.Displayed.class) + .entrySet()) { + mXmppConnectionService.processMdsItem(account, item); + } } } - private void parseDeleteEvent(final Element event, final Jid from, final Account account) { - final Element delete = event.findChild("delete"); - final String node = delete == null ? null : delete.getAttribute("node"); + private void parseDeleteEvent(final Delete delete, final Jid from, final Account account) { + final String node = delete.getNode(); if (Namespace.NICK.equals(node)) { - Log.d(Config.LOGTAG, "parsing nick delete event from " + from); setNick(account, from, null); } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node"); deleteAllBookmarks(account); - } else if (Namespace.AVATAR_METADATA.equals(node) - && account.getJid().asBareJid().equals(from)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted avatar metadata node"); + } else if (Namespace.AVATAR_METADATA.equals(node)) { + final boolean isAccount = account.getJid().asBareJid().equals(from); + if (isAccount) { + account.setAvatar(null); + mXmppConnectionService.databaseBackend.updateAccount(account); + mXmppConnectionService.getAvatarService().clear(account); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": deleted avatar metadata node"); + } } } - private void parsePurgeEvent(final Element event, final Jid from, final Account account) { - final Element purge = event.findChild("purge"); - final String node = purge == null ? null : purge.getAttribute("node"); + private void parsePurgeEvent( + @NonNull final Purge purge, final Jid from, final Account account) { + final String node = purge.getNode(); if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks"); deleteAllBookmarks(account); @@ -1394,12 +1411,20 @@ public class MessageParser extends AbstractParser final var event = original.getExtension(Event.class); if (event != null && Jid.Invalid.hasValidFrom(original) && original.getFrom().isBareJid()) { - if (event.hasChild("items")) { - parseEvent(event, original.getFrom(), account); - } else if (event.hasChild("delete")) { - parseDeleteEvent(event, original.getFrom(), account); - } else if (event.hasChild("purge")) { - parsePurgeEvent(event, original.getFrom(), account); + final var action = event.getAction(); + final var node = action == null ? null : action.getNode(); + if (node == null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": no node found in PubSub event from " + + original.getFrom()); + } else if (action instanceof Items items) { + parseEvent(items, original.getFrom(), account); + } else if (action instanceof Purge purge) { + parsePurgeEvent(purge, original.getFrom(), account); + } else if (action instanceof Delete delete) { + parseDeleteEvent(delete, from, account); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 32b1ae4645475c275d3e39abf1156635168cb78e..900028152aa061bcc31767fdb2b0e12dfc03abdb 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -137,7 +137,12 @@ import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; +import im.conversations.android.xmpp.model.avatar.Metadata; +import im.conversations.android.xmpp.model.bookmark.Storage; +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.storage.PrivateStorage; import java.io.File; import java.security.Security; import java.security.cert.CertificateException; @@ -2093,14 +2098,17 @@ public class XmppConnectionService extends Service { public void fetchBookmarks(final Account account) { final Iq iqPacket = new Iq(Iq.Type.GET); - final Element query = iqPacket.query("jabber:iq:private"); - query.addChild("storage", Namespace.BOOKMARKS); + iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage()); final Consumer callback = (response) -> { if (response.getType() == Iq.Type.RESULT) { - final Element query1 = response.query(); - final Element storage = query1.findChild("storage", "storage:bookmarks"); - Map bookmarks = Bookmark.parseFromStorage(storage, account); + final var privateStorage = response.getExtension(PrivateStorage.class); + if (privateStorage == null) { + return; + } + final var bookmarkStorage = privateStorage.getExtension(Storage.class); + Map bookmarks = + Bookmark.parseFromStorage(bookmarkStorage, account); processBookmarksInitial(account, bookmarks, false); } else { Log.d( @@ -2118,7 +2126,7 @@ public class XmppConnectionService extends Service { retrieve, (response) -> { if (response.getType() == Iq.Type.RESULT) { - final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); + final var pubsub = response.getExtension(PubSub.class); final Map bookmarks = Bookmark.parseFromPubSub(pubsub, account); processBookmarksInitial(account, bookmarks, true); @@ -2136,30 +2144,34 @@ public class XmppConnectionService extends Service { if (response.getType() != Iq.Type.RESULT) { return; } - final var pubSub = response.findChild("pubsub", Namespace.PUBSUB); - final Element items = pubSub == null ? null : pubSub.findChild("items"); - if (items == null - || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) { + final var pubsub = response.getExtension(PubSub.class); + if (pubsub == null) { return; } - for (final Element child : items.getChildren()) { - if ("item".equals(child.getName())) { - processMdsItem(account, child); + final var items = pubsub.getItems(); + if (items == null) { + return; + } + if (Namespace.MDS_DISPLAYED.equals(items.getNode())) { + for (final var item : + items.getItemMap( + im.conversations.android.xmpp.model.mds.Displayed + .class) + .entrySet()) { + processMdsItem(account, item); } } }); } - public void processMdsItem(final Account account, final Element item) { - final Jid jid = - item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id")); + public void processMdsItem(final Account account, final Map.Entry item) { + final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey())); if (jid == null) { return; } - final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED); - final Element stanzaId = - displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS); - final String id = stanzaId == null ? null : stanzaId.getAttribute("id"); + final var displayed = item.getValue(); + final var stanzaId = displayed.getStanzaId(); + final String id = stanzaId == null ? null : stanzaId.getId(); final Conversation conversation = find(account, jid); if (id != null && conversation != null) { conversation.setDisplayState(id); @@ -4920,16 +4932,20 @@ public class XmppConnectionService extends Service { packet, new Consumer() { - private Avatar parseAvatar(Iq packet) { - Element pubsub = - packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - return Avatar.parseMetadata(items); - } + private Avatar parseAvatar(final Iq packet) { + final var pubsub = packet.getExtension(PubSub.class); + if (pubsub == null) { + return null; + } + final var items = pubsub.getItems(); + if (items == null) { + return null; } - return null; + final var item = items.getFirstItemWithId(Metadata.class); + if (item == null) { + return null; + } + return Avatar.parseMetadata(item.getKey(), item.getValue()); } private boolean errorIsItemNotFound(Iq packet) { @@ -5164,30 +5180,39 @@ public class XmppConnectionService extends Service { account, packet, response -> { - if (response.getType() == Iq.Type.RESULT) { - Element pubsub = - response.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - Avatar avatar = Avatar.parseMetadata(items); - if (avatar != null) { - avatar.owner = account.getJid().asBareJid(); - if (fileBackend.isAvatarCached(avatar)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); - } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); - } - return; - } - } + if (response.getType() != Iq.Type.RESULT) { + callback.error(0, null); + } + final var pubsub = packet.getExtension(PubSub.class); + if (pubsub == null) { + callback.error(0, null); + return; + } + final var items = pubsub.getItems(); + if (items == null) { + callback.error(0, null); + return; + } + final var item = items.getFirstItemWithId(Metadata.class); + if (item == null) { + callback.error(0, null); + return; + } + final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue()); + if (avatar == null) { + callback.error(0, null); + return; + } + avatar.owner = account.getJid().asBareJid(); + if (fileBackend.isAvatarCached(avatar)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); } - callback.error(0, null); }); } diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index cc08154f9b4fa0a510765b13619b8d1c16dce493..9095de54e8cc5e076dbec2843ae0e5334fdaa53f 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -112,4 +112,5 @@ public final class Namespace { public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; + public static final String PRIVATE_XML_STORAGE = "jabber:iq:private"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index a4397e466a93a1ad107976fa79cefcef9425f082..3109b1d0696ce6b30d966dc137bd91fc9947f472 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -1,102 +1,95 @@ package eu.siacs.conversations.xmpp.pep; import android.util.Base64; - import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.avatar.Metadata; public class Avatar { - public enum Origin { PEP, VCARD } + public enum Origin { + PEP, + VCARD + } public String type; - public String sha1sum; - public String image; - public int height; - public int width; - public long size; - public Jid owner; - public Origin origin = Origin.PEP; //default to maintain compat + public String sha1sum; + public String image; + public int height; + public int width; + public long size; + public Jid owner; + public Origin origin = Origin.PEP; // default to maintain compat - public byte[] getImageAsBytes() { - return Base64.decode(image, Base64.DEFAULT); - } + public byte[] getImageAsBytes() { + return Base64.decode(image, Base64.DEFAULT); + } - public String getFilename() { - return sha1sum; - } + public String getFilename() { + return sha1sum; + } - public static Avatar parseMetadata(Element items) { - Element item = items.findChild("item"); - if (item == null) { - return null; - } - Element metadata = item.findChild("metadata"); - if (metadata == null) { - return null; - } - String primaryId = item.getAttribute("id"); - if (primaryId == null) { - return null; - } - for (Element child : metadata.getChildren()) { - if (child.getName().equals("info") - && primaryId.equals(child.getAttribute("id"))) { - Avatar avatar = new Avatar(); - String height = child.getAttribute("height"); - String width = child.getAttribute("width"); - String size = child.getAttribute("bytes"); - try { - if (height != null) { - avatar.height = Integer.parseInt(height); - } - if (width != null) { - avatar.width = Integer.parseInt(width); - } - if (size != null) { - avatar.size = Long.parseLong(size); - } - } catch (NumberFormatException e) { - return null; - } - avatar.type = child.getAttribute("type"); - String hash = child.getAttribute("id"); - if (!isValidSHA1(hash)) { - return null; - } - avatar.sha1sum = hash; - avatar.origin = Origin.PEP; - return avatar; - } - } - return null; - } + public static Avatar parseMetadata(final String primaryId, final Metadata metadata) { + if (primaryId == null || metadata == null) { + return null; + } + for (Element child : metadata.getChildren()) { + if (child.getName().equals("info") && primaryId.equals(child.getAttribute("id"))) { + Avatar avatar = new Avatar(); + String height = child.getAttribute("height"); + String width = child.getAttribute("width"); + String size = child.getAttribute("bytes"); + try { + if (height != null) { + avatar.height = Integer.parseInt(height); + } + if (width != null) { + avatar.width = Integer.parseInt(width); + } + if (size != null) { + avatar.size = Long.parseLong(size); + } + } catch (NumberFormatException e) { + return null; + } + avatar.type = child.getAttribute("type"); + String hash = child.getAttribute("id"); + if (!isValidSHA1(hash)) { + return null; + } + avatar.sha1sum = hash; + avatar.origin = Origin.PEP; + return avatar; + } + } + return null; + } - @Override - public boolean equals(Object object) { - if (object != null && object instanceof Avatar) { - Avatar other = (Avatar) object; - return other.getFilename().equals(this.getFilename()); - } else { - return false; - } - } + @Override + public boolean equals(Object object) { + if (object != null && object instanceof Avatar) { + Avatar other = (Avatar) object; + return other.getFilename().equals(this.getFilename()); + } else { + return false; + } + } - public static Avatar parsePresence(Element x) { - String hash = x == null ? null : x.findChildContent("photo"); - if (hash == null) { - return null; - } - if (!isValidSHA1(hash)) { - return null; - } - Avatar avatar = new Avatar(); - avatar.sha1sum = hash; - avatar.origin = Origin.VCARD; - return avatar; - } + public static Avatar parsePresence(Element x) { + String hash = x == null ? null : x.findChildContent("photo"); + if (hash == null) { + return null; + } + if (!isValidSHA1(hash)) { + return null; + } + Avatar avatar = new Avatar(); + avatar.sha1sum = hash; + avatar.origin = Origin.VCARD; + return avatar; + } - private static boolean isValidSHA1(String s) { - return s != null && s.matches("[a-fA-F0-9]{40}"); - } + private static boolean isValidSHA1(String s) { + return s != null && s.matches("[a-fA-F0-9]{40}"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java index 0f924e8883b40157d0aa96676ab5f6412e971138..17fed121b1145978b81fe3a8ba5ac521fb2d3de3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java @@ -1,32 +1,10 @@ package im.conversations.android.xmpp.model.bookmark; -import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; -@XmlElement public class Conference extends Extension { public Conference() { super(Conference.class); } - - public boolean isAutoJoin() { - return this.getAttributeAsBoolean("autojoin"); - } - - public String getConferenceName() { - return this.getAttribute("name"); - } - - public void setAutoJoin(boolean autoJoin) { - setAttribute("autojoin", autoJoin); - } - - public Nick getNick() { - return this.getExtension(Nick.class); - } - - public Extensions getExtensions() { - return this.getExtension(Extensions.class); - } } diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java new file mode 100644 index 0000000000000000000000000000000000000000..2ecac583dd8686251550bf6f66686ec2ee7f2f00 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Storage.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Storage extends Extension { + + public Storage() { + super(Storage.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java index 1bb963be849a57cdfeae7a2eb1c8930db49c799a..2a50d0f27818cf8cffe9904275e940ad88a298e3 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java @@ -1,5 +1,5 @@ -@XmlPackage(namespace = Namespace.BOOKMARKS2) +@XmlPackage(namespace = Namespace.BOOKMARKS) package im.conversations.android.xmpp.model.bookmark; -import im.conversations.android.annotation.XmlPackage; import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java new file mode 100644 index 0000000000000000000000000000000000000000..265c80e3fd7db0d2e56572eb700f94592af12a5e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Conference.java @@ -0,0 +1,32 @@ +package im.conversations.android.xmpp.model.bookmark2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Conference extends Extension { + + public Conference() { + super(Conference.class); + } + + public boolean isAutoJoin() { + return this.getAttributeAsBoolean("autojoin"); + } + + public String getConferenceName() { + return this.getAttribute("name"); + } + + public void setAutoJoin(boolean autoJoin) { + setAttribute("autojoin", autoJoin); + } + + public Nick getNick() { + return this.getExtension(Nick.class); + } + + public Extensions getExtensions() { + return this.getExtension(Extensions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java similarity index 81% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java rename to src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java index b9385cf5473de88d5d43d771a57fe88995bc2536..f3d3534faaa49f200f4575160af7786ae260b066 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Extensions.java @@ -1,4 +1,4 @@ -package im.conversations.android.xmpp.model.bookmark; +package im.conversations.android.xmpp.model.bookmark2; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java similarity index 79% rename from src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java rename to src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java index ee5efa3864baebb0c848c65769b6b0a8d4a69022..c309e5056b74daa783e7fcc2f68796b1357938ba 100644 --- a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/Nick.java @@ -1,4 +1,4 @@ -package im.conversations.android.xmpp.model.bookmark; +package im.conversations.android.xmpp.model.bookmark2; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d872edde9aaa08dbacb78b603f97512f80860e98 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BOOKMARKS2) +package im.conversations.android.xmpp.model.bookmark2; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java index 9f5275371c3564ee52d1825bc67fda4327d36514..a49216c27dc2f1ea7f5cd9ee1f1538366127ac88 100644 --- a/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java +++ b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java @@ -3,10 +3,15 @@ package im.conversations.android.xmpp.model.mds; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.unique.StanzaId; @XmlElement(namespace = Namespace.MDS_DISPLAYED) public class Displayed extends Extension { public Displayed() { super(Displayed.class); } + + public StanzaId getStanzaId() { + return this.getOnlyExtension(StanzaId.class); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java index ceb1931ca35cbb1048e5c66f14cfe5ea9eff2303..47fc68c08368afccb4fc20703bd2aeae1aa81728 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java @@ -45,6 +45,11 @@ public interface Items { return Iterables.getFirst(map.values(), null); } + default Map.Entry getFirstItemWithId(final Class clazz) { + final var entries = getItemMap(clazz).entrySet(); + return Iterables.getFirst(entries, null); + } + default T getOnlyItem(final Class clazz) { final var map = getItemMap(clazz); return Iterables.getOnlyElement(map.values()); diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java new file mode 100644 index 0000000000000000000000000000000000000000..c41a9fbdb2d4038835b711978a1920e37e772189 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Action.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class Action extends Extension { + + public Action(Class clazz) { + super(clazz); + } + + public String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java new file mode 100644 index 0000000000000000000000000000000000000000..ed5f6295eca366dff2895757402cc618042547c4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Delete.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Delete extends Action { + + public Delete() { + super(Delete.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java index 1e180c460053ee0bb74cbefa753c86b63f175e2f..d224060d452378cc0f81bb60b6ad5061b4dc2a5d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java @@ -12,25 +12,17 @@ public class Event extends Extension { super(Event.class); } - public Items getItems() { - return this.getExtension(ItemsWrapper.class); - } - - public Purge getPurge() { - return this.getExtension(Purge.class); + public Action getAction() { + return this.getOnlyExtension(Action.class); } @XmlElement(name = "items") - public static class ItemsWrapper extends Extension implements Items { + public static class ItemsWrapper extends Action implements Items { public ItemsWrapper() { super(ItemsWrapper.class); } - public String getNode() { - return this.getAttribute("node"); - } - public Collection getItems() { return this.getExtensions(Item.class); } diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java index 64550e0b77f1ca33d57e2b501204995545bfcac6..66329a2aff54b05ca2de2c79abb952f911d18aa1 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java @@ -1,10 +1,9 @@ package im.conversations.android.xmpp.model.pubsub.event; import im.conversations.android.annotation.XmlElement; -import im.conversations.android.xmpp.model.Extension; @XmlElement -public class Purge extends Extension { +public class Purge extends Action { public Purge() { super(Purge.class); diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java index 139a49522c48cb74aa9b4469768baf43305fb446..bbdf2fc497e441ba6ba2a191b885c88027591477 100644 --- a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java @@ -1,10 +1,9 @@ package im.conversations.android.xmpp.model.pubsub.event; import im.conversations.android.annotation.XmlElement; -import im.conversations.android.xmpp.model.Extension; @XmlElement -public class Retract extends Extension { +public class Retract extends Action { public Retract() { super(Retract.class); diff --git a/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java b/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..dff10104a81fe596a0ff827ec8c7dc40b5af7b8e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/storage/PrivateStorage.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.storage; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "query", namespace = Namespace.PRIVATE_XML_STORAGE) +public class PrivateStorage extends Extension { + + public PrivateStorage() { + super(PrivateStorage.class); + } +} From 2690a19467ca7aacf271396f532ca7ce0a12af3d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 16 Apr 2025 12:28:20 +0200 Subject: [PATCH 16/19] implement Service Outage Status and show message in account screen --- conversations.doap | 7 + .../siacs/conversations/entities/Account.java | 128 ++-- .../entities/ServiceDiscoveryResult.java | 662 +++++++++--------- .../http/ServiceOutageStatus.java | 162 +++++ .../services/XmppConnectionService.java | 37 + .../conversations/ui/EditAccountActivity.java | 41 +- .../ui/adapter/AccountAdapter.java | 20 +- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../conversations/xmpp/XmppConnection.java | 14 + .../xmpp/pep/PublishOptions.java | 2 + .../android/xmpp/processor/BindProcessor.java | 24 +- src/main/res/layout/activity_edit_account.xml | 49 ++ src/main/res/values/strings.xml | 3 + 13 files changed, 728 insertions(+), 422 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/http/ServiceOutageStatus.java diff --git a/conversations.doap b/conversations.doap index 89132d021139da15ac127e11e0864754d730c742..e402ff875d9811b2e758457e9aceaa4b5a441ef4 100644 --- a/conversations.doap +++ b/conversations.doap @@ -489,6 +489,13 @@ complete 0.1.0 + + + + + complete + 0.3.0 + diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 075ee301aa08380e072f67cf3b94dfdb02b5cf07..6673f7dced1706ca795e1d19f51d1f6bc724fb6d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -18,6 +18,7 @@ import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.HashedTokenSha256; import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.http.ServiceOutageStatus; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.Resolver; @@ -73,6 +74,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_ID = "pgp_id"; private static final String KEY_PINNED_MECHANISM = "pinned_mechanism"; + public static final String KEY_SOS_URL = "sos_url"; public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration"; protected final JSONObject keys; @@ -105,6 +107,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private String pinnedChannelBinding; private String fastMechanism; private String fastToken; + private ServiceOutageStatus serviceOutageStatus; public Account(final Jid jid, final String password) { this( @@ -783,6 +786,22 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable throw new IllegalStateException("This method should not be called"); } + public void setServiceOutageStatus(final ServiceOutageStatus sos) { + this.serviceOutageStatus = sos; + } + + public ServiceOutageStatus getServiceOutageStatus() { + return this.serviceOutageStatus; + } + + public boolean isServiceOutage() { + final var sos = this.serviceOutageStatus; + if (sos != null && ServiceOutageStatus.isPossibleOutage(this.status)) { + return sos.isNow(); + } + return false; + } + public enum State { DISABLED(false, false), LOGGED_OUT(false, false), @@ -844,78 +863,43 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public int getReadableId() { - switch (this) { - case DISABLED: - return R.string.account_status_disabled; - case LOGGED_OUT: - return R.string.account_state_logged_out; - case ONLINE: - return R.string.account_status_online; - case CONNECTING: - return R.string.account_status_connecting; - case OFFLINE: - return R.string.account_status_offline; - case UNAUTHORIZED: - return R.string.account_status_unauthorized; - case SERVER_NOT_FOUND: - return R.string.account_status_not_found; - case NO_INTERNET: - return R.string.account_status_no_internet; - case CONNECTION_TIMEOUT: - return R.string.account_status_connection_timeout; - case REGISTRATION_FAILED: - return R.string.account_status_regis_fail; - case REGISTRATION_WEB: - return R.string.account_status_regis_web; - case REGISTRATION_CONFLICT: - return R.string.account_status_regis_conflict; - case REGISTRATION_SUCCESSFUL: - return R.string.account_status_regis_success; - case REGISTRATION_NOT_SUPPORTED: - return R.string.account_status_regis_not_sup; - case REGISTRATION_INVALID_TOKEN: - return R.string.account_status_regis_invalid_token; - case TLS_ERROR: - return R.string.account_status_tls_error; - case TLS_ERROR_DOMAIN: - return R.string.account_status_tls_error_domain; - case INCOMPATIBLE_SERVER: - return R.string.account_status_incompatible_server; - case INCOMPATIBLE_CLIENT: - return R.string.account_status_incompatible_client; - case CHANNEL_BINDING: - return R.string.account_status_channel_binding; - case TOR_NOT_AVAILABLE: - return R.string.account_status_tor_unavailable; - case BIND_FAILURE: - return R.string.account_status_bind_failure; - case SESSION_FAILURE: - return R.string.session_failure; - case DOWNGRADE_ATTACK: - return R.string.sasl_downgrade; - case HOST_UNKNOWN: - return R.string.account_status_host_unknown; - case POLICY_VIOLATION: - return R.string.account_status_policy_violation; - case REGISTRATION_PLEASE_WAIT: - return R.string.registration_please_wait; - case REGISTRATION_PASSWORD_TOO_WEAK: - return R.string.registration_password_too_weak; - case STREAM_ERROR: - return R.string.account_status_stream_error; - case STREAM_OPENING_ERROR: - return R.string.account_status_stream_opening_error; - case PAYMENT_REQUIRED: - return R.string.payment_required; - case SEE_OTHER_HOST: - return R.string.reconnect_on_other_host; - case MISSING_INTERNET_PERMISSION: - return R.string.missing_internet_permission; - case TEMPORARY_AUTH_FAILURE: - return R.string.account_status_temporary_auth_failure; - default: - return R.string.account_status_unknown; - } + return switch (this) { + case DISABLED -> R.string.account_status_disabled; + case LOGGED_OUT -> R.string.account_state_logged_out; + case ONLINE -> R.string.account_status_online; + case CONNECTING -> R.string.account_status_connecting; + case OFFLINE -> R.string.account_status_offline; + case UNAUTHORIZED -> R.string.account_status_unauthorized; + case SERVER_NOT_FOUND -> R.string.account_status_not_found; + case NO_INTERNET -> R.string.account_status_no_internet; + case CONNECTION_TIMEOUT -> R.string.account_status_connection_timeout; + case REGISTRATION_FAILED -> R.string.account_status_regis_fail; + case REGISTRATION_WEB -> R.string.account_status_regis_web; + case REGISTRATION_CONFLICT -> R.string.account_status_regis_conflict; + case REGISTRATION_SUCCESSFUL -> R.string.account_status_regis_success; + case REGISTRATION_NOT_SUPPORTED -> R.string.account_status_regis_not_sup; + case REGISTRATION_INVALID_TOKEN -> R.string.account_status_regis_invalid_token; + case TLS_ERROR -> R.string.account_status_tls_error; + case TLS_ERROR_DOMAIN -> R.string.account_status_tls_error_domain; + case INCOMPATIBLE_SERVER -> R.string.account_status_incompatible_server; + case INCOMPATIBLE_CLIENT -> R.string.account_status_incompatible_client; + case CHANNEL_BINDING -> R.string.account_status_channel_binding; + case TOR_NOT_AVAILABLE -> R.string.account_status_tor_unavailable; + case BIND_FAILURE -> R.string.account_status_bind_failure; + case SESSION_FAILURE -> R.string.session_failure; + case DOWNGRADE_ATTACK -> R.string.sasl_downgrade; + case HOST_UNKNOWN -> R.string.account_status_host_unknown; + case POLICY_VIOLATION -> R.string.account_status_policy_violation; + case REGISTRATION_PLEASE_WAIT -> R.string.registration_please_wait; + case REGISTRATION_PASSWORD_TOO_WEAK -> R.string.registration_password_too_weak; + case STREAM_ERROR -> R.string.account_status_stream_error; + case STREAM_OPENING_ERROR -> R.string.account_status_stream_opening_error; + case PAYMENT_REQUIRED -> R.string.payment_required; + case SEE_OTHER_HOST -> R.string.reconnect_on_other_host; + case MISSING_INTERNET_PERMISSION -> R.string.missing_internet_permission; + case TEMPORARY_AUTH_FAILURE -> R.string.account_status_temporary_auth_failure; + default -> R.string.account_status_unknown; + }; } } } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index 3f2d2a5adf7f907a58c5866a29ead6d9e7a71585..3a99f6ca28e5b26872119b6ba188c31e1231232e 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -3,15 +3,12 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; import android.util.Base64; - -import androidx.annotation.NonNull; - import com.google.common.base.Strings; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - +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; @@ -19,337 +16,334 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; - -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 org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; public class ServiceDiscoveryResult { - public static final String TABLENAME = "discovery_results"; - public static final String HASH = "hash"; - public static final String VER = "ver"; - public static final String RESULT = "result"; - protected final String hash; - protected final byte[] ver; - protected final List features; - protected final List forms; - private final List identities; - public ServiceDiscoveryResult(final Iq packet) { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = "sha-1"; // We only support sha-1 for now - - final List elements = packet.query().getChildren(); - - for (final Element element : elements) { - if (element.getName().equals("identity")) { - Identity id = new Identity(element); - if (id.getType() != null && id.getCategory() != null) { - identities.add(id); - } - } else if (element.getName().equals("feature")) { - if (element.getAttribute("var") != null) { - features.add(element.getAttribute("var")); - } - } else if (element.getName().equals("x") && element.getAttribute("xmlns").equals(Namespace.DATA)) { - forms.add(Data.parse(element)); - } - } - this.ver = this.mkCapHash(); - } - private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = hash; - this.ver = ver; - - JSONArray identities = o.optJSONArray("identities"); - if (identities != null) { - for (int i = 0; i < identities.length(); i++) { - this.identities.add(new Identity(identities.getJSONObject(i))); - } - } - JSONArray features = o.optJSONArray("features"); - if (features != null) { - for (int i = 0; i < features.length(); i++) { - this.features.add(features.getString(i)); - } - } - JSONArray forms = o.optJSONArray("forms"); - if (forms != null) { - for (int i = 0; i < forms.length(); i++) { - this.forms.add(createFormFromJSONObject(forms.getJSONObject(i))); - } - } - } - - private ServiceDiscoveryResult() { - this.hash = "sha-1"; - this.features = Collections.emptyList(); - this.identities = Collections.emptyList(); - this.ver = null; - this.forms = Collections.emptyList(); - } - - public static ServiceDiscoveryResult empty() { - return new ServiceDiscoveryResult(); - } - - public ServiceDiscoveryResult(Cursor cursor) throws JSONException { - this( - cursor.getString(cursor.getColumnIndexOrThrow(HASH)), - Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT), - new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT))) - ); - } - - private static String clean(String s) { - return s.replace("<","<"); - } - - private static String blankNull(String s) { - return s == null ? "" : clean(s); - } - - private static Data createFormFromJSONObject(JSONObject o) { - Data data = new Data(); - JSONArray names = o.names(); - for (int i = 0; i < names.length(); ++i) { - try { - String name = names.getString(i); - JSONArray jsonValues = o.getJSONArray(name); - ArrayList values = new ArrayList<>(jsonValues.length()); - for (int j = 0; j < jsonValues.length(); ++j) { - values.add(jsonValues.getString(j)); - } - data.put(name, values); - } catch (Exception e) { - e.printStackTrace(); - } - } - return data; - } - - private static JSONObject createJSONFromForm(Data data) { - JSONObject object = new JSONObject(); - for (Field field : data.getFields()) { - try { - JSONArray jsonValues = new JSONArray(); - for (String value : field.getValues()) { - jsonValues.put(value); - } - object.put(field.getFieldName(), jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - } - try { - JSONArray jsonValues = new JSONArray(); - jsonValues.put(data.getFormType()); - object.put(Data.FORM_TYPE, jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - return object; - } - - public String getVer() { - return Base64.encodeToString(this.ver, Base64.NO_WRAP); - } - - public List getIdentities() { - return this.identities; - } - - public List getFeatures() { - return this.features; - } - - public boolean hasIdentity(String category, String type) { - for (Identity id : this.getIdentities()) { - if ((category == null || id.getCategory().equals(category)) && - (type == null || id.getType().equals(type))) { - return true; - } - } - - return false; - } - - public String getExtendedDiscoInformation(String formType, String name) { - for (Data form : this.forms) { - if (formType.equals(form.getFormType())) { - for (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( + public static final String TABLENAME = "discovery_results"; + public static final String HASH = "hash"; + public static final String VER = "ver"; + public static final String RESULT = "result"; + protected final String hash; + protected final byte[] ver; + protected final List features; + protected final List forms; + private final List identities; + + public ServiceDiscoveryResult(final Iq packet) { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.forms = new ArrayList<>(); + this.hash = "sha-1"; // We only support sha-1 for now + + final List elements = packet.query().getChildren(); + + for (final Element element : elements) { + if (element.getName().equals("identity")) { + Identity id = new Identity(element); + if (id.getType() != null && id.getCategory() != null) { + identities.add(id); + } + } else if (element.getName().equals("feature")) { + if (element.getAttribute("var") != null) { + features.add(element.getAttribute("var")); + } + } else if (element.getName().equals("x") + && element.getAttribute("xmlns").equals(Namespace.DATA)) { + forms.add(Data.parse(element)); + } + } + this.ver = this.mkCapHash(); + } + + private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.forms = new ArrayList<>(); + this.hash = hash; + this.ver = ver; + + JSONArray identities = o.optJSONArray("identities"); + if (identities != null) { + for (int i = 0; i < identities.length(); i++) { + this.identities.add(new Identity(identities.getJSONObject(i))); + } + } + JSONArray features = o.optJSONArray("features"); + if (features != null) { + for (int i = 0; i < features.length(); i++) { + this.features.add(features.getString(i)); + } + } + JSONArray forms = o.optJSONArray("forms"); + if (forms != null) { + for (int i = 0; i < forms.length(); i++) { + this.forms.add(createFormFromJSONObject(forms.getJSONObject(i))); + } + } + } + + private ServiceDiscoveryResult() { + this.hash = "sha-1"; + this.features = Collections.emptyList(); + this.identities = Collections.emptyList(); + this.ver = null; + this.forms = Collections.emptyList(); + } + + public static ServiceDiscoveryResult empty() { + return new ServiceDiscoveryResult(); + } + + public ServiceDiscoveryResult(Cursor cursor) throws JSONException { + this( + cursor.getString(cursor.getColumnIndexOrThrow(HASH)), + Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT), + new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT)))); + } + + private static String clean(String s) { + return s.replace("<", "<"); + } + + private static String blankNull(String s) { + return s == null ? "" : clean(s); + } + + private static Data createFormFromJSONObject(JSONObject o) { + Data data = new Data(); + JSONArray names = o.names(); + for (int i = 0; i < names.length(); ++i) { + try { + String name = names.getString(i); + JSONArray jsonValues = o.getJSONArray(name); + ArrayList values = new ArrayList<>(jsonValues.length()); + for (int j = 0; j < jsonValues.length(); ++j) { + values.add(jsonValues.getString(j)); + } + data.put(name, values); + } catch (Exception e) { + e.printStackTrace(); + } + } + return data; + } + + private static JSONObject createJSONFromForm(Data data) { + JSONObject object = new JSONObject(); + for (Field field : data.getFields()) { + try { + JSONArray jsonValues = new JSONArray(); + for (String value : field.getValues()) { + jsonValues.put(value); + } + object.put(field.getFieldName(), jsonValues); + } catch (Exception e) { + e.printStackTrace(); + } + } + try { + JSONArray jsonValues = new JSONArray(); + jsonValues.put(data.getFormType()); + object.put(Data.FORM_TYPE, jsonValues); + } catch (Exception e) { + e.printStackTrace(); + } + return object; + } + + public String getVer() { + return Base64.encodeToString(this.ver, Base64.NO_WRAP); + } + + public List getIdentities() { + return this.identities; + } + + public List getFeatures() { + return this.features; + } + + public boolean hasIdentity(String category, String type) { + for (Identity id : this.getIdentities()) { + if ((category == null || id.getCategory().equals(category)) + && (type == null || id.getType().equals(type))) { + return true; + } + } + + return false; + } + + public String getExtendedDiscoInformation(final String formType, final String name) { + for (final Data form : this.forms) { + if (formType.equals(form.getFormType())) { + for (final Field field : form.getFields()) { + if (name.equals(field.getFieldName())) { + return field.getValue(); + } + } + } + } + return null; + } + + private byte[] mkCapHash() { + StringBuilder s = new StringBuilder(); + + List identities = this.getIdentities(); + Collections.sort(identities); + + for (Identity id : identities) { + s.append(blankNull(id.getCategory())) + .append("/") + .append(blankNull(id.getType())) + .append("/") + .append(blankNull(id.getLang())) + .append("/") + .append(blankNull(id.getName())) + .append("<"); + } + + final List features = this.getFeatures(); + Collections.sort(features); + for (final String feature : features) { + s.append(clean(feature)).append("<"); + } + + Collections.sort(forms, Comparator.comparing(Data::getFormType)); + for (final Data form : forms) { + s.append(clean(form.getFormType())).append("<"); + final List fields = form.getFields(); + Collections.sort( fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName()))); - for (final Field field : fields) { - s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); - final List values = field.getValues(); - Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull)); - for (final String value : values) { - s.append(blankNull(value)).append("<"); - } - } - } - - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } + 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; - } - } + 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/http/ServiceOutageStatus.java b/src/main/java/eu/siacs/conversations/http/ServiceOutageStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..f74a9e4d7ff760d7c09edfd95ef2ed2e104e5fd8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/http/ServiceOutageStatus.java @@ -0,0 +1,162 @@ +package eu.siacs.conversations.http; + +import android.content.Context; +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; +import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.entities.Account; +import java.io.IOException; +import java.lang.reflect.Type; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class ServiceOutageStatus { + + private static final Collection SERVICE_OUTAGE_STATE = + Arrays.asList( + Account.State.CONNECTION_TIMEOUT, + Account.State.SERVER_NOT_FOUND, + Account.State.STREAM_OPENING_ERROR); + + private final boolean planned; + private final Instant beginning; + + @SerializedName("expected_end") + private final Instant expectedEnd; + + private final Map message; + + public ServiceOutageStatus( + final boolean planned, + final Instant beginning, + final Instant expectedEnd, + final Map message) { + this.planned = planned; + this.beginning = beginning; + this.expectedEnd = expectedEnd; + this.message = message; + } + + public boolean isNow() { + final var now = Instant.now(); + final var hasDefault = this.message != null && this.message.containsKey("default"); + return hasDefault + && this.beginning != null + && this.expectedEnd != null + && this.beginning.isBefore(now) + && this.expectedEnd.isAfter(now); + } + + public static ListenableFuture fetch( + final Context context, final HttpUrl url) { + final var appSettings = new AppSettings(context); + final var builder = HttpConnectionManager.okHttpClient(context).newBuilder(); + if (appSettings.isUseTor()) { + builder.proxy(HttpConnectionManager.getProxy()); + } + + var client = builder.build(); + + final SettableFuture future = SettableFuture.create(); + + var request = new Request.Builder().url(url).build(); + + client.newCall(request) + .enqueue( + new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + future.setException(e); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + try (final ResponseBody body = response.body()) { + if (!response.isSuccessful() || body == null) { + future.setException( + new IOException( + "unexpected server response (" + + response.code() + + ")")); + return; + } + var gson = + new GsonBuilder() + .registerTypeAdapter( + Instant.class, + new InstantDeserializer()) + .create(); + future.set( + gson.fromJson( + body.string(), ServiceOutageStatus.class)); + } catch (final IOException | JsonSyntaxException e) { + future.setException(e); + } + } + }); + + return future; + } + + public static boolean isPossibleOutage(final Account.State state) { + return SERVICE_OUTAGE_STATE.contains(state); + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("planned", planned) + .add("beginning", beginning) + .add("expectedEnd", expectedEnd) + .add("message", message) + .toString(); + } + + public boolean isPlanned() { + return this.planned; + } + + public long getExpectedEnd() { + if (this.expectedEnd == null) { + return 0L; + } + return this.expectedEnd.toEpochMilli(); + } + + public String getMessage() { + final var translated = this.message.get(Locale.getDefault().getLanguage()); + if (Strings.isNullOrEmpty(translated)) { + return this.message.get("default"); + } + return translated; + } + + private static class InstantDeserializer implements JsonDeserializer { + @Override + public Instant deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return Instant.parse(json.getAsString()); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 900028152aa061bcc31767fdb2b0e12dfc03abdb..849831940de0fa7cb906a6f72acf9d00cc7fdead 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -58,6 +58,9 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -87,6 +90,7 @@ import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.http.ServiceOutageStatus; import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.persistance.DatabaseBackend; @@ -170,6 +174,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import me.leolin.shortcutbadger.ShortcutBadger; +import okhttp3.HttpUrl; import org.conscrypt.Conscrypt; import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep; import org.openintents.openpgp.IOpenPgpService2; @@ -358,6 +363,10 @@ public class XmppConnectionService extends Service { @Override public void onStatusChanged(final Account account) { + final var status = account.getStatus(); + if (ServiceOutageStatus.isPossibleOutage(status)) { + fetchServiceOutageStatus(account); + } XmppConnection connection = account.getXmppConnection(); updateAccountUi(); @@ -489,6 +498,7 @@ public class XmppConnectionService extends Service { getNotificationService().updateErrorNotification(); } }; + private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; private WakeLock wakeLock; @@ -1133,6 +1143,33 @@ public class XmppConnectionService extends Service { } } + private void fetchServiceOutageStatus(final Account account) { + final var sosUrl = account.getKey(Account.KEY_SOS_URL); + if (Strings.isNullOrEmpty(sosUrl)) { + return; + } + final var url = HttpUrl.parse(sosUrl); + if (url == null) { + return; + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching service outage " + url); + Futures.addCallback( + ServiceOutageStatus.fetch(getApplicationContext(), url), + new FutureCallback<>() { + @Override + public void onSuccess(final ServiceOutageStatus sos) { + Log.d(Config.LOGTAG, "fetched " + sos); + account.setServiceOutageStatus(sos); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d(Config.LOGTAG, "error fetching sos", throwable); + } + }, + MoreExecutors.directExecutor()); + } + public boolean processUnifiedPushMessage( final Account account, final Jid transport, final Element push) { return unifiedPushBroker.processPushMessage(account, transport, push); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 728af3cd4a752eb19d04340dfc42661b147dc8e9..2cb112170ca6e65f9f49d29deb6beb4625709fef 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -18,6 +18,7 @@ import android.security.KeyChainAliasCallback; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.format.DateUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -503,7 +504,7 @@ public class EditAccountActivity extends OmemoActivity final List accounts = xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); - if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { + if (accounts != null && accounts.isEmpty() && Config.MAGIC_CREATE_DOMAIN != null) { Intent intent = SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); StartConversationActivity.addInviteUri(intent, getIntent()); @@ -905,9 +906,9 @@ public class EditAccountActivity extends OmemoActivity } @Override - public void onNewIntent(final Intent intent) { + public void onNewIntent(@NonNull final Intent intent) { super.onNewIntent(intent); - if (intent != null && intent.getData() != null) { + if (intent.getData() != null) { final XmppUri uri = new XmppUri(intent.getData()); if (xmppConnectionServiceBound) { processFingerprintVerification(uri, false); @@ -1400,6 +1401,7 @@ public class EditAccountActivity extends OmemoActivity } else { this.binding.otherDeviceKeysCard.setVisibility(View.GONE); } + this.binding.serviceOutage.setVisibility(View.GONE); } else { final TextInputLayout errorLayout; final var status = this.mAccount.getStatus(); @@ -1428,6 +1430,39 @@ public class EditAccountActivity extends OmemoActivity removeErrorsOnAllBut(errorLayout); this.binding.stats.setVisibility(View.GONE); this.binding.otherDeviceKeysCard.setVisibility(View.GONE); + final var sos = mAccount.getServiceOutageStatus(); + if (mAccount.isServiceOutage() && sos != null) { + this.binding.serviceOutage.setVisibility(View.VISIBLE); + if (sos.isPlanned()) { + this.binding.sosTitle.setText(R.string.account_status_service_outage_scheduled); + } else { + this.binding.sosTitle.setText(R.string.account_status_service_outage_known); + } + final var sosMessage = sos.getMessage(); + if (Strings.isNullOrEmpty(sosMessage)) { + this.binding.sosMessage.setVisibility(View.GONE); + } else { + this.binding.sosMessage.setText(sosMessage); + this.binding.sosMessage.setVisibility(View.VISIBLE); + } + final var expectedEnd = sos.getExpectedEnd(); + if (expectedEnd <= 0) { + this.binding.sosScheduledEnd.setVisibility(View.GONE); + } else { + this.binding.sosScheduledEnd.setVisibility(View.VISIBLE); + this.binding.sosScheduledEnd.setText( + getString( + R.string.sos_scheduled_return, + DateUtils.formatDateTime( + this, + expectedEnd, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_ABBREV_ALL + | DateUtils.FORMAT_SHOW_DATE))); + } + } else { + this.binding.serviceOutage.setVisibility(View.GONE); + } } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 96b1470c37a0fa01bc0eee516dae7cbe2d55045b..470dd69475db6e8ca1626273d02f6eef2fba8887 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -49,11 +49,25 @@ public class AccountAdapter extends ArrayAdapter { } else { viewHolder = (ViewHolder) view.getTag(); } + if (account == null) { + return view; + } viewHolder.binding.accountJid.setText(account.getJid().asBareJid().toString()); AvatarWorkerTask.loadAvatar(account, viewHolder.binding.accountImage, R.dimen.avatar); - viewHolder.binding.accountStatus.setText( - getContext().getString(account.getStatus().getReadableId())); - switch (account.getStatus()) { + final var status = account.getStatus(); + if (account.isServiceOutage()) { + final var sos = account.getServiceOutageStatus(); + if (sos != null && sos.isPlanned()) { + viewHolder.binding.accountStatus.setText( + R.string.account_status_service_outage_scheduled); + } else { + viewHolder.binding.accountStatus.setText( + R.string.account_status_service_outage_known); + } + } else { + viewHolder.binding.accountStatus.setText(status.getReadableId()); + } + switch (status) { case ONLINE: viewHolder.binding.accountStatus.setTextColor( MaterialColors.getColor( diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 9095de54e8cc5e076dbec2843ae0e5334fdaa53f..15d88f75aaa6a0800774a945a78b1101754602f6 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -113,4 +113,5 @@ public final class Namespace { public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; public static final String PRIVATE_XML_STORAGE = "jabber:iq:private"; + public static final String SERVICE_OUTAGE_STATUS = "urn:xmpp:sos:0"; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 953e6e1cc176725eb53f6dce0b75e318a03fdd1f..11c6df3bbb0995af4311d2c09c9cb98d8444b9f1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -3138,6 +3138,20 @@ public class XmppConnection implements Runnable { this.blockListRequested = value; } + public HttpUrl getServiceOutageStatus() { + final var disco = connection.disco.get(account.getDomain()); + if (disco == null) { + return null; + } + final var address = + disco.getExtendedDiscoInformation( + Namespace.SERVICE_OUTAGE_STATUS, "external-status-addresses"); + if (Strings.isNullOrEmpty(address)) { + return null; + } + return HttpUrl.parse(address); + } + public boolean httpUpload(long filesize) { if (Config.DISABLE_HTTP_UPLOAD) { return false; diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java index c9b764752d3bbea3225ffa4b67bb1a3a75b31730..2dcaf9bac7b07c6cc89d7d7a3b6db18789f59fc7 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -12,12 +12,14 @@ public class PublishOptions { public static Bundle openAccess() { final Bundle options = new Bundle(); options.putString("pubsub#access_model", "open"); + options.putString("pubsub#notify_delete", "true"); return options; } public static Bundle presenceAccess() { final Bundle options = new Bundle(); options.putString("pubsub#access_model", "presence"); + options.putString("pubsub#notify_delete", "true"); return options; } diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 1423df7f50239d8511b511e3cb25c94c5a9fdd2f..3230185b648a92acc122d474f13a4f265572b1cf 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -2,13 +2,11 @@ package im.conversations.android.xmpp.processor; import android.text.TextUtils; import android.util.Log; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; - import im.conversations.android.xmpp.model.stanza.Iq; public class BindProcessor implements Runnable { @@ -24,14 +22,21 @@ public class BindProcessor implements Runnable { @Override public void run() { final XmppConnection connection = account.getXmppConnection(); + final var features = connection.getFeatures(); service.cancelAvatarFetches(account); final boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true); + final boolean sosModified; + final var sos = features.getServiceOutageStatus(); + if (sos != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " server has SOS on " + sos); + sosModified = account.setKey(Account.KEY_SOS_URL, sos.toString()); + } else { + sosModified = false; + } final boolean gainedFeature = - account.setOption( - Account.OPTION_HTTP_UPLOAD_AVAILABLE, - connection.getFeatures().httpUpload(0)); - if (loggedInSuccessfully || gainedFeature) { + account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, features.httpUpload(0)); + if (loggedInSuccessfully || gainedFeature || sosModified) { service.databaseBackend.updateAccount(account); } @@ -57,18 +62,17 @@ public class BindProcessor implements Runnable { connection.fetchRoster(); - if (connection.getFeatures().bookmarks2()) { + if (features.bookmarks2()) { service.fetchBookmarks2(account); - } else if (!connection.getFeatures().bookmarksConversion()) { + } else if (!features.bookmarksConversion()) { service.fetchBookmarks(account); } - if (connection.getFeatures().mds()) { + if (features.mds()) { service.fetchMessageDisplayedSynchronization(account); } else { Log.d(Config.LOGTAG, account.getJid() + ": server has no support for mds"); } - final var features = connection.getFeatures(); final boolean bind2 = features.bind2(); final boolean flexible = features.flexibleOfflineMessageRetrieval(); final boolean catchup = service.getMessageArchiveService().inCatchup(account); diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 45d8710d03673fb3208b6a006993cd28d0c72c5f..57db26b861b3883d97286452b5cfc6612770683c 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -146,6 +146,55 @@ + + + + + + + + + + + + + + Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar. Show to contacts only Backup location + Planned Downtime + Service Down (Known Issue) + The service is scheduled to return at %s From 4f3e3c0295d7675a57fe03f301fefe1534de3360 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 16 Apr 2025 18:16:37 +0200 Subject: [PATCH 17/19] show links in SOS message --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/de/gultsch/common/Linkify.java | 4 +-- .../siacs/conversations/entities/Account.java | 4 ++- .../services/XmppConnectionService.java | 1 + .../siacs/conversations/ui/AboutActivity.java | 15 ++++++++--- .../ui/ConferenceDetailsActivity.java | 5 ++-- .../conversations/ui/EditAccountActivity.java | 13 ++++++++-- .../conversations/ui/text/FixedURLSpan.java | 14 +++++------ .../conversations/utils/StylingHelper.java | 8 +++--- src/main/res/layout/activity_about.xml | 7 +++--- src/main/res/layout/activity_edit_account.xml | 25 ++++++++++--------- src/main/res/layout/activity_muc_details.xml | 1 - src/main/res/layout/item_message_content.xml | 1 - 14 files changed, 60 insertions(+), 42 deletions(-) diff --git a/build.gradle b/build.gradle index f258c0dc956f8f4232e3a971f486075f9eb5d2c7..eedd3851471b3a27cda9d78c828abf23c4acf593 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.5.2' + classpath 'com.android.tools.build:gradle:8.9.1' classpath "com.diffplug.spotless:spotless-plugin-gradle:7.0.2" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f05d39d81870f8355ca43324f027298..37f853b1c84d2e2dd1c88441fcc755d7f6643668 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/de/gultsch/common/Linkify.java b/src/main/java/de/gultsch/common/Linkify.java index 12ad2c67b3ec36964b6455b7a704a75d46646a95..655e8dddc3a88ae4a22148d3b75edc8518e84174 100644 --- a/src/main/java/de/gultsch/common/Linkify.java +++ b/src/main/java/de/gultsch/common/Linkify.java @@ -30,7 +30,7 @@ package de.gultsch.common; import android.net.Uri; -import android.text.Editable; +import android.text.Spannable; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -66,7 +66,7 @@ public class Linkify { }; } - public static void addLinks(final Editable body) { + public static void addLinks(final Spannable body) { android.text.util.Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null); } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 6673f7dced1706ca795e1d19f51d1f6bc724fb6d..66ec318071198811dfcd9387c1f2acd07700ffa0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -796,7 +796,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public boolean isServiceOutage() { final var sos = this.serviceOutageStatus; - if (sos != null && ServiceOutageStatus.isPossibleOutage(this.status)) { + if (sos != null + && isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) + && ServiceOutageStatus.isPossibleOutage(this.status)) { return sos.isNow(); } return false; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 849831940de0fa7cb906a6f72acf9d00cc7fdead..f6cf075273ea55e1365df5b12c2d88a4f90631ab 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -1160,6 +1160,7 @@ public class XmppConnectionService extends Service { public void onSuccess(final ServiceOutageStatus sos) { Log.d(Config.LOGTAG, "fetched " + sos); account.setServiceOutageStatus(sos); + updateAccountUi(); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java index bcbfdef00a9207107c29b6ebac2a53711d64ab09..58c597b244ac07a527e29a030273e5ff42bf2dfc 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutActivity.java @@ -3,20 +3,27 @@ package eu.siacs.conversations.ui; import static eu.siacs.conversations.ui.XmppActivity.configureActionBar; import android.os.Bundle; - +import android.text.SpannableString; +import android.text.method.LinkMovementMethod; import androidx.databinding.DataBindingUtil; - +import de.gultsch.common.Linkify; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityAboutBinding; +import eu.siacs.conversations.ui.text.FixedURLSpan; public class AboutActivity extends ActionBarActivity { - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final ActivityAboutBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_about); + final ActivityAboutBinding binding = + DataBindingUtil.setContentView(this, R.layout.activity_about); + final var text = new SpannableString(getString(R.string.pref_about_message)); + Linkify.addLinks(text); + FixedURLSpan.fix(text); + binding.about.setText(text); + binding.about.setMovementMethod(LinkMovementMethod.getInstance()); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(binding.toolbar); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index baa0914eca9d9cc026d574a1877aafa20155a93b..fe2da072e2acc8aff33559e7414ddccb3c8d45c5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -10,7 +10,7 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.text.Editable; -import android.text.SpannableStringBuilder; +import android.text.SpannableString; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.view.Menu; @@ -557,7 +557,7 @@ public class ConferenceDetailsActivity extends XmppActivity this.binding.mucTitle.setVisibility(View.GONE); } if (printableValue(subject)) { - SpannableStringBuilder spannable = new SpannableStringBuilder(subject); + final var spannable = new SpannableString(subject); StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); Linkify.addLinks(spannable); FixedURLSpan.fix(spannable); @@ -568,7 +568,6 @@ public class ConferenceDetailsActivity extends XmppActivity .TextAppearance_Material3_BodyMedium : com.google.android.material.R.style .TextAppearance_Material3_BodyLarge); - this.binding.mucSubject.setAutoLinkMask(0); this.binding.mucSubject.setVisibility(View.VISIBLE); this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance()); } else { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 2cb112170ca6e65f9f49d29deb6beb4625709fef..c5603e42c85578ff16324f25e3980a337e5f66d6 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -16,9 +16,11 @@ import android.provider.Settings; import android.security.KeyChain; import android.security.KeyChainAliasCallback; import android.text.Editable; +import android.text.SpannableString; import android.text.TextUtils; import android.text.TextWatcher; import android.text.format.DateUtils; +import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -40,6 +42,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; +import de.gultsch.common.Linkify; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -58,6 +61,7 @@ import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.adapter.PresenceTemplateAdapter; +import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.PendingItem; @@ -1442,8 +1446,12 @@ public class EditAccountActivity extends OmemoActivity if (Strings.isNullOrEmpty(sosMessage)) { this.binding.sosMessage.setVisibility(View.GONE); } else { - this.binding.sosMessage.setText(sosMessage); + final var sosMessageSpannable = new SpannableString(sosMessage); + Linkify.addLinks(sosMessageSpannable); + FixedURLSpan.fix(sosMessageSpannable); + this.binding.sosMessage.setText(sosMessageSpannable); this.binding.sosMessage.setVisibility(View.VISIBLE); + this.binding.sosMessage.setMovementMethod(LinkMovementMethod.getInstance()); } final var expectedEnd = sos.getExpectedEnd(); if (expectedEnd <= 0) { @@ -1457,7 +1465,8 @@ public class EditAccountActivity extends OmemoActivity this, expectedEnd, DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_ABBREV_ALL + | DateUtils.FORMAT_NUMERIC_DATE + | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE))); } } else { diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java index c76494736fccdfd8819b92acd4456d3bfca0dc1b..7e7cecd8e0124959b6d36c55bf3a8f03c79752b3 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -34,7 +34,7 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.text.Editable; +import android.text.Spannable; import android.text.Spanned; import android.text.style.URLSpan; import android.util.Log; @@ -56,12 +56,12 @@ public class FixedURLSpan extends URLSpan { super(url); } - public static void fix(final Editable editable) { - for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) { - final int start = editable.getSpanStart(urlspan); - final int end = editable.getSpanEnd(urlspan); - editable.removeSpan(urlspan); - editable.setSpan( + public static void fix(final Spannable spannable) { + for (final URLSpan urlspan : spannable.getSpans(0, spannable.length() - 1, URLSpan.class)) { + final int start = spannable.getSpanStart(urlspan); + final int end = spannable.getSpanEnd(urlspan); + spannable.removeSpan(urlspan); + spannable.setSpan( new FixedURLSpan(urlspan.getURL()), start, end, diff --git a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java index 5e5bafd3e9d50ca7d4ea85a9045264aa523e117a..d2e561beca1c0585c2d7f509001424f414ca9d19 100644 --- a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java @@ -70,7 +70,7 @@ public class StylingHelper { } public static void format( - final Editable editable, int start, int end, @ColorInt int textColor) { + final Spannable editable, int start, int end, @ColorInt int textColor) { for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) { final int keywordLength = style.getKeyword().length(); editable.setSpan( @@ -85,7 +85,7 @@ public class StylingHelper { } } - public static void format(final Editable editable, @ColorInt final int textColor) { + public static void format(final Spannable editable, @ColorInt final int textColor) { format(editable, 0, editable.length() - 1, textColor); } @@ -121,7 +121,7 @@ public class StylingHelper { } private static void highlight( - final TextView view, final Editable editable, final String needle) { + final TextView view, final Spannable editable, final String needle) { final int length = needle.length(); String string = editable.toString(); int start = indexOfIgnoreCase(string, needle, 0); @@ -197,7 +197,7 @@ public class StylingHelper { } private static void makeKeywordOpaque( - final Editable editable, int start, int end, @ColorInt int fallbackTextColor) { + final Spannable editable, int start, int end, @ColorInt int fallbackTextColor) { QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class); @ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor; diff --git a/src/main/res/layout/activity_about.xml b/src/main/res/layout/activity_about.xml index 81960647fbfafc11f74fd58b2e74ab0bc1afc51d..823620ffdf419755fea1690664b9a9593d769813 100644 --- a/src/main/res/layout/activity_about.xml +++ b/src/main/res/layout/activity_about.xml @@ -1,4 +1,5 @@ - + diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 57db26b861b3883d97286452b5cfc6612770683c..98df185f4f0dfda27ace0a8b67c61d0e2ba9f598 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -147,8 +147,8 @@ + app:cardBackgroundColor="?colorErrorContainer" + tools:visibility="visible"> + android:textAppearance="?textAppearanceTitleLarge" + android:textColor="?colorOnErrorContainer" /> + android:textAppearance="?textAppearanceBodyMedium" + android:textColor="?colorOnErrorContainer" + android:textColorLink="?colorOnErrorContainer" + tools:text="Our service is currently performing server updates" /> + android:layout_marginTop="16sp" + android:textAppearance="?textAppearanceBodyLarge" + android:textColor="?colorOnErrorContainer" + tools:text="@string/sos_scheduled_return" /> diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index b7f6151e50d5c8c94dfb3c44baa59126b959b414..0af16d1bd3bb61d522be191a62c8614efd37c709 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -86,7 +86,6 @@ android:id="@+id/muc_subject" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:autoLink="web" android:textAppearance="?textAppearanceTitleMedium" /> diff --git a/src/main/res/layout/item_message_content.xml b/src/main/res/layout/item_message_content.xml index 06ace848307827b5ed05bf6121a90e925147d313..8114da6c17f377628ba6750ae0ec7c7eff72f736 100644 --- a/src/main/res/layout/item_message_content.xml +++ b/src/main/res/layout/item_message_content.xml @@ -11,7 +11,6 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="10dp" android:layout_marginTop="4dp" - android:autoLink="web" android:longClickable="false" android:textAppearance="?textAppearanceBodyMedium" /> From 39175223b400fd7e832f2e61047e2fcee14d570f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Apr 2025 10:15:55 +0200 Subject: [PATCH 18/19] add compile time config to merge bodies instead of ignoring --- src/main/java/eu/siacs/conversations/Config.java | 2 ++ .../android/xmpp/model/stanza/Message.java | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 8417b20221c923aa0bf71bd0c84535bb73d9d11b..628b3c3531b389c3f6dcd623bd241f2d61e1e519 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -136,6 +136,8 @@ public final class Config { public static final boolean IGNORE_ID_REWRITE_IN_MUC = true; public static final boolean MUC_LEAVE_BEFORE_JOIN = false; + // if this is set to true messages that contain multiple bodies (per language) will be ignored + public static final boolean TREAT_MULTI_CONTENT_AS_INVALID = false; public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5; public static final int MAM_MAX_MESSAGES = 750; diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java index dd5f7458e94937a81f4f8479decc7a91ab670915..bf64102e2e00b20d3ba8faccddeee0bd7575285f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java @@ -1,7 +1,11 @@ package im.conversations.android.xmpp.model.stanza; +import com.google.common.base.Joiner; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import im.conversations.android.annotation.XmlElement; @@ -31,7 +35,7 @@ public class Message extends Stanza { } private LocalizedContent getLocalizedContent(final Class clazz) { - final var builder = new ImmutableMap.Builder(); + final var builder = new ImmutableMultimap.Builder(); final var messageLanguage = this.getAttribute("xml:lang"); final var parentLanguage = Strings.isNullOrEmpty(messageLanguage) @@ -47,11 +51,12 @@ public class Message extends Stanza { } builder.put(language, content); } - try { - return LocalizedContent.get(builder.buildOrThrow()); - } catch (final IllegalArgumentException e) { + final var multiMap = builder.build().asMap(); + if (Config.TREAT_MULTI_CONTENT_AS_INVALID + && Iterables.any(multiMap.values(), v -> v.size() > 1)) { return null; } + return LocalizedContent.get(Maps.transformValues(multiMap, v -> Joiner.on('\n').join(v))); } public Type getType() { From 9dbfa784533b19e6f868586a3ea7f32235b57119 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 17 Apr 2025 11:54:04 +0200 Subject: [PATCH 19/19] target SDK 35 (Android 15) --- build.gradle | 8 +-- .../res/layout/activity_easy_invite.xml | 1 + .../res/layout/activity_magic_create.xml | 2 +- .../res/layout/activity_pick_server.xml | 2 +- .../res/layout/activity_welcome.xml | 1 + src/main/java/de/gultsch/common/MiniUri.java | 2 +- .../java/de/gultsch/minidns/DNSServer.java | 6 +- .../services/AbstractConnectionManager.java | 64 ++++++++++--------- .../ui/ConversationActivity.java | 15 ++--- .../conversations/xmpp/jingle/Media.java | 11 ++-- .../xmpp/jingle/RtpContentMap.java | 4 +- .../xmpp/jingle/TrackWrapper.java | 13 ++-- .../xmpp/jingle/VideoSourceWrapper.java | 17 ++--- .../xmpp/jingle/WebRTCWrapper.java | 10 +-- .../WebRTCDataChannelTransport.java | 6 +- .../layout-w945dp/activity_conversations.xml | 1 + src/main/res/layout/activity_add_reaction.xml | 3 +- .../res/layout/activity_change_password.xml | 3 +- .../res/layout/activity_channel_discovery.xml | 1 + .../res/layout/activity_choose_contact.xml | 1 + .../res/layout/activity_contact_details.xml | 1 + .../res/layout/activity_conversations.xml | 1 + src/main/res/layout/activity_edit_account.xml | 3 +- .../res/layout/activity_import_backup.xml | 1 + .../res/layout/activity_manage_accounts.xml | 1 + .../res/layout/activity_media_browser.xml | 1 + src/main/res/layout/activity_muc_details.xml | 1 + src/main/res/layout/activity_muc_users.xml | 1 + .../activity_publish_profile_picture.xml | 3 +- src/main/res/layout/activity_rtp_session.xml | 3 +- src/main/res/layout/activity_search.xml | 3 +- src/main/res/layout/activity_settings.xml | 3 +- .../res/layout/activity_share_location.xml | 1 + src/main/res/layout/activity_share_with.xml | 1 + .../res/layout/activity_show_location.xml | 3 +- .../layout/activity_start_conversation.xml | 5 +- src/main/res/layout/activity_trust_keys.xml | 3 +- src/main/res/values/themes.xml | 1 + .../res/layout/activity_choose_country.xml | 1 + .../res/layout/activity_enter_name.xml | 1 + .../res/layout/activity_enter_number.xml | 1 + src/quicksy/res/layout/activity_tos.xml | 1 + src/quicksy/res/layout/activity_verify.xml | 1 + 43 files changed, 114 insertions(+), 98 deletions(-) diff --git a/build.gradle b/build.gradle index eedd3851471b3a27cda9d78c828abf23c4acf593..9f3964e8e1a194e640b0aa6d94c5e471e92a5fde 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ dependencies { implementation "androidx.preference:preference:1.2.1" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'com.google.android.material:material:1.13.0-alpha12' - implementation 'androidx.work:work-runtime:2.9.1' + implementation 'androidx.work:work-runtime:2.10.0' implementation "androidx.emoji2:emoji2:1.5.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.5.0" @@ -96,7 +96,7 @@ dependencies { implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation "com.squareup.okhttp3:okhttp:4.12.0" - implementation 'com.google.guava:guava:33.4.0-android' + implementation 'com.google.guava:guava:33.4.6-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.52' implementation 'im.conversations.webrtc:webrtc-android:129.0.0' @@ -112,11 +112,11 @@ ext { android { namespace 'eu.siacs.conversations' - compileSdk 34 + compileSdk 35 defaultConfig { minSdkVersion 23 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 42140 versionName "2.18.1" archivesBaseName += "-$versionName" diff --git a/src/conversations/res/layout/activity_easy_invite.xml b/src/conversations/res/layout/activity_easy_invite.xml index 6f1c8fc57d386f4a45f4ef781dc37446ce61cd1f..3d1c20c36c09770a1d5bb003ef741271239295c1 100644 --- a/src/conversations/res/layout/activity_easy_invite.xml +++ b/src/conversations/res/layout/activity_easy_invite.xml @@ -5,6 +5,7 @@ = UI_REFRESH_THRESHOLD) { + if (force + || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() + >= UI_REFRESH_THRESHOLD) { LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime()); mXmppConnectionService.updateConversationUi(); } @@ -138,7 +139,8 @@ public class AbstractConnectionManager { } public PowerManager.WakeLock createWakeLock(final String name) { - final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class); + final PowerManager powerManager = + ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class); return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); } @@ -160,7 +162,7 @@ public class AbstractConnectionManager { } public static Extension of(String path) { - //TODO accept List pathSegments + // TODO accept List pathSegments final int pos = path.lastIndexOf('/'); final String filename = path.substring(pos + 1).toLowerCase(); final String[] parts = filename.split("\\."); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index da5231357e42cd486e205158aceabdc0a817cebc..8601d88f58cc51ad3f5c27ec82f96241fb5f1c9d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -2,18 +2,15 @@ package eu.siacs.conversations.ui; import android.content.Intent; import android.os.Bundle; - import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import eu.siacs.conversations.ui.util.SettingsUtils; - public class ConversationActivity extends AppCompatActivity { - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - startActivity(new Intent(this, ConversationsActivity.class)); - finish(); - } + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + startActivity(new Intent(this, ConversationsActivity.class)); + finish(); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java index 6a41c89067e0d853f5b8db0d4a7a4140e6aab11b..4a491143e198f70cf47da3a6b501a9a973c4b955 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/Media.java @@ -1,18 +1,17 @@ package eu.siacs.conversations.xmpp.jingle; +import androidx.annotation.NonNull; import com.google.common.collect.ImmutableSet; - import java.util.Locale; import java.util.Set; -import javax.annotation.Nonnull; - public enum Media { - - VIDEO, AUDIO, UNKNOWN; + VIDEO, + AUDIO, + UNKNOWN; @Override - @Nonnull + @NonNull public String toString() { return super.toString().toLowerCase(Locale.ROOT); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index e8079fe4663e71c482cd527345b987673ae33810..a7ee89cea1200acdfb710c03c6c32f1eebba3a8e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.xmpp.jingle; +import androidx.annotation.NonNull; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; @@ -25,7 +26,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import javax.annotation.Nonnull; public class RtpContentMap extends AbstractContentMap { @@ -491,7 +491,7 @@ public class RtpContentMap extends AbstractContentMap { public final T track; public final RtpSender rtpSender; @@ -63,7 +58,7 @@ class TrackWrapper { } public static RtpTransceiver getTransceiver( - @Nonnull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { + @NonNull final PeerConnection peerConnection, final TrackWrapper trackWrapper) { final RtpSender rtpSender = trackWrapper.rtpSender; final String rtpSenderId; try { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java index 89552fc1814b1d9215ce6727268923d64182d67a..3a8a4cc6d7440acb92e244f4349aa0115e6eb83d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/VideoSourceWrapper.java @@ -2,12 +2,15 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; import android.util.Log; - +import androidx.annotation.Nullable; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; - +import eu.siacs.conversations.Config; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; import org.webrtc.Camera2Enumerator; import org.webrtc.CameraEnumerationAndroid; import org.webrtc.CameraEnumerator; @@ -17,14 +20,6 @@ import org.webrtc.PeerConnectionFactory; import org.webrtc.SurfaceTextureHelper; import org.webrtc.VideoSource; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Set; - -import javax.annotation.Nullable; - -import eu.siacs.conversations.Config; - class VideoSourceWrapper { private static final int CAPTURING_RESOLUTION = 1920; @@ -151,7 +146,7 @@ class VideoSourceWrapper { return videoSourceWrapper; } } - if (deviceNames.size() == 0) { + if (deviceNames.isEmpty()) { return null; } else { return of(enumerator, Iterables.get(deviceNames, 0), deviceNames); 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 09e95a7cd89e5d055e068fa48597aed5c700f03e..404461046576de1bc291601daa83353b276b0c9a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -3,6 +3,8 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; import android.os.Build; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -22,8 +24,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.CandidatePairChangeEvent; @@ -661,7 +661,7 @@ public class WebRTCWrapper { MoreExecutors.directExecutor()); } - @Nonnull + @NonNull private ListenableFuture getPeerConnectionFuture() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { @@ -671,7 +671,7 @@ public class WebRTCWrapper { } } - @Nonnull + @NonNull private PeerConnection requirePeerConnection() { final PeerConnection peerConnection = this.peerConnection; if (peerConnection == null) { @@ -680,7 +680,7 @@ public class WebRTCWrapper { return peerConnection; } - @Nonnull + @NonNull private PeerConnectionFactory requirePeerConnectionFactory() { final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; if (peerConnectionFactory == null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java index 80b0322f4727cf418d7f37de58d9cb6feb1ddbf6..2540d6027daf5aa336b1e2e998b3ea8be877fd7e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java @@ -5,6 +5,7 @@ import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription; import android.content.Context; import android.util.Log; +import androidx.annotation.NonNull; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.io.Closeables; @@ -41,7 +42,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nonnull; import org.webrtc.CandidatePairChangeEvent; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; @@ -437,7 +437,7 @@ public class WebRTCDataChannelTransport implements Transport { localDescriptionExecutorService); } - @Nonnull + @NonNull private PeerConnectionFactory requirePeerConnectionFactory() { final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory; if (peerConnectionFactory == null) { @@ -446,7 +446,7 @@ public class WebRTCDataChannelTransport implements Transport { return peerConnectionFactory; } - @Nonnull + @NonNull private PeerConnection requirePeerConnection() { final var future = this.peerConnectionFuture; if (future != null && future.isDone()) { diff --git a/src/main/res/layout-w945dp/activity_conversations.xml b/src/main/res/layout-w945dp/activity_conversations.xml index 6e0b978c49d3b8bce75fc246b027c7e066a9eed6..bece0533efd9ffbcedd1df7248ae1ebf80708def 100644 --- a/src/main/res/layout-w945dp/activity_conversations.xml +++ b/src/main/res/layout-w945dp/activity_conversations.xml @@ -3,6 +3,7 @@ + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/src/main/res/layout/activity_change_password.xml b/src/main/res/layout/activity_change_password.xml index b5c9b2162747be61d5a42d51977d24258c86bacf..abcac84a06d6511fba9e81228692e878749807ad 100644 --- a/src/main/res/layout/activity_change_password.xml +++ b/src/main/res/layout/activity_change_password.xml @@ -4,7 +4,8 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> diff --git a/src/main/res/layout/activity_muc_details.xml b/src/main/res/layout/activity_muc_details.xml index 0af16d1bd3bb61d522be191a62c8614efd37c709..30f6bf9401f1df9cd16a4b40e21277730599b9e9 100644 --- a/src/main/res/layout/activity_muc_details.xml +++ b/src/main/res/layout/activity_muc_details.xml @@ -6,6 +6,7 @@ + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + android:layout_height="match_parent" + android:fitsSystemWindows="true"> @drawable/background_splash_screen true + true