diff --git a/CHANGELOG.md b/CHANGELOG.md index fedfe63c244ed38ba2a34ff84ea6d151d1474249..d64e76c2803a52e9f82b408e40487fe8fe078d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +### Version 2.16.6 + +* Offer higher automatic file accept values +* Provide more information in 'Server info' +* Various bug fixes + +### Version 2.16.5 + +* Minor bug fixes + +### Version 2.16.4 + +* Fix minor regression introduced in 2.16.3 + +### Version 2.16.3 + +* exclude older Oppo devices from call integration +* various bug fixes + +### Version 2.16.2 + +* Run Backup as foreground service to prevent process being stopped after 10 minutes + +### Version 2.16.1 + +* Fix call getting un-muted when switching output devices +* Exclude all Umidigi devices from call integration + +### Version 2.16.0 + +* Schedule regular backups +* Exclude all realme devices up to Android 11 from call integration +* Minor UI (message bubble) improvements + ### Version 2.15.3 * fix call integration on some Android 14 devices diff --git a/build.gradle b/build.gradle index 7b23257613f9c0723fcc202cbcdc99b65e17b3aa..2b169807e44559dc2312c85ba1a5b40f130aa66f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.1' + classpath 'com.android.tools.build:gradle:8.3.2' } } @@ -50,9 +50,13 @@ dependencies { implementation "androidx.core:core:1.10.1" coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + implementation project(':libs:annotation') + annotationProcessor project(':libs:annotation-processor') + + implementation 'androidx.viewpager:viewpager:1.0.0' - playstoreImplementation('com.google.firebase:firebase-messaging:23.4.1') { + playstoreImplementation('com.google.firebase:firebase-messaging:24.0.0') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' @@ -60,24 +64,22 @@ dependencies { cheogramPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") cheogramPlaystoreImplementation 'com.github.singpolyma:play-licensing:1c637ea03c' conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.2' + quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.1.0' implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.exifinterface:exifinterface:1.3.7' implementation 'androidx.cardview:cardview:1.0.0' implementation "androidx.preference:preference:1.2.1" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.work:work-runtime:2.9.0' implementation "androidx.emoji2:emoji2:1.4.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.4.0" - implementation 'org.bouncycastle:bcmail-jdk15on:1.64' - //zxing stopped supporting Java 7 so we have to stick with 3.3.3 - //https://github.com/zxing/zxing/issues/1170 - implementation 'com.google.zxing:core:3.3.3' + implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1' + implementation 'com.google.zxing:core:3.5.3' implementation 'org.minidns:minidns-hla:1.0.5' implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'org.whispersystems:signal-protocol-java:2.6.2' @@ -97,12 +99,12 @@ dependencies { implementation 'me.drakeet.support:toastcompat:1.1.0' implementation "com.leinardi.android:speed-dial:3.3.0" - implementation "com.squareup.retrofit2:retrofit:2.9.0" - implementation "com.squareup.retrofit2:converter-gson:2.9.0" + implementation "com.squareup.retrofit2:retrofit:2.11.0" + implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation 'com.google.guava:guava:32.1.3-android' - implementation 'io.michaelrocks:libphonenumber-android:8.13.28' + implementation 'io.michaelrocks:libphonenumber-android:8.13.35' implementation 'im.conversations.webrtc:webrtc-android:119.0.1' implementation 'io.github.nishkarsh:android-permissions:2.1.6' implementation 'androidx.recyclerview:recyclerview:1.1.0' @@ -299,7 +301,7 @@ android { } packagingOptions { resources { - excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF'] + excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF'] } } lint { diff --git a/fastlane/metadata/android/de-DE/changelogs/4211104.txt b/fastlane/metadata/android/de-DE/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..fa21b66e5b7c02df54d55478daeb4396e98518fd --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Wiederkehrende Sicherungen planen +* Realme-Geräte von der Anrufintegration ausgenommen +* Kleine Designverbesserungen (Chatblasen) diff --git a/fastlane/metadata/android/de-DE/changelogs/4211204.txt b/fastlane/metadata/android/de-DE/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..42a52939380d1ef3f5cc47a144a8f516e18b395c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Anruf wird nicht mehr stummgeschaltet, wenn das Ausgabegerät gewechselt wird +* Umidigi-Geräte von der Anrufintegration ausgenommen diff --git a/fastlane/metadata/android/de-DE/changelogs/4211304.txt b/fastlane/metadata/android/de-DE/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..127af85651e5b7d26fb5c33b045a9d5e0c82369e --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4211304.txt @@ -0,0 +1 @@ +* Sicherung als Vordergrunddienst ausführen, damit der Prozess nicht nach 10 Minuten gestoppt wird diff --git a/fastlane/metadata/android/de-DE/changelogs/4211404.txt b/fastlane/metadata/android/de-DE/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..ecd13e4023f3c4ddd57d09e6355fef9579b7797e --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* Ältere Oppo-Geräte von der Anrufintegration ausgenommen +* Verschiedene Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/4211604.txt b/fastlane/metadata/android/de-DE/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..a00810e4f6e08b6d44a747d22e22fe4cd4ea8f83 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4211604.txt @@ -0,0 +1 @@ +* Kleinere Fehlerbehebungen diff --git a/fastlane/metadata/android/en-US/changelogs/4211104.txt b/fastlane/metadata/android/en-US/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..a6dc59556d58cf3573f8272847a494f8367eba93 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Schedule regular backups +* Exclude all realme devices up to Android 11 from call integration +* Minor UI (message bubble) improvements diff --git a/fastlane/metadata/android/en-US/changelogs/4211204.txt b/fastlane/metadata/android/en-US/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..0e469b1efe1dbbb815eecc1fde8c262e220c3dc4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Fix call getting un-muted when switching output devices +* Exclude all Umidigi devices from call integration diff --git a/fastlane/metadata/android/en-US/changelogs/4211304.txt b/fastlane/metadata/android/en-US/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..4e9637aa144a7685c75666c2c585dc2c51c43051 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211304.txt @@ -0,0 +1 @@ +* Run Backup as foreground service to prevent process being stopped after 10 minutes diff --git a/fastlane/metadata/android/en-US/changelogs/4211404.txt b/fastlane/metadata/android/en-US/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..8c3bfc7ebdb9def198cd160feced925061bb10c9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* exclude older Oppo devices from call integration +* various bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/4211604.txt b/fastlane/metadata/android/en-US/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..3afc657f9176c2c66773ae36055c1eabcbe5b4e3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211604.txt @@ -0,0 +1 @@ +* Minor bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/4211704.txt b/fastlane/metadata/android/en-US/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..3c2e99507c343d12f4707931000266976417ae8c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* Offer higher automatic file accept values +* Provide more information in 'Server info' +* Various bug fixes diff --git a/fastlane/metadata/android/es-ES/changelogs/4211104.txt b/fastlane/metadata/android/es-ES/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..2744732e2101a964042774ba6f3a587a5e22d84a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Programe copias de seguridad periódicas +* Excluir todos los dispositivos realme hasta Android 11 de la integración de llamadas +* Mejoras menores en la interfaz de usuario (burbuja de mensaje) diff --git a/fastlane/metadata/android/es-ES/changelogs/4211204.txt b/fastlane/metadata/android/es-ES/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..f75aa4665f8eb48bb84f4a7d84693ffe8497f415 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* La llamada ya no se silencia cuando se cambia el dispositivo de salida +* Dispositivos Umidigi excluidos de la integración de llamadas diff --git a/fastlane/metadata/android/es-ES/changelogs/4211304.txt b/fastlane/metadata/android/es-ES/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..60217885a4a490941c3a19890bad0c639aa8f59e --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211304.txt @@ -0,0 +1 @@ +* Ejecute la copia de seguridad como servicio en primer plano para que el proceso no se detenga después de 10 minutos diff --git a/fastlane/metadata/android/es-ES/changelogs/4211404.txt b/fastlane/metadata/android/es-ES/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..71fdf6502ac0cddeabfdf22452013dddf8179abe --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* excluir dispositivos Oppo antiguos de la integración de llamadas +* varios arreglos diff --git a/fastlane/metadata/android/es-ES/changelogs/4211604.txt b/fastlane/metadata/android/es-ES/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..288dce67f8be1df87c8ad3c10e93a1cfa6237d7a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211604.txt @@ -0,0 +1 @@ +* Correcciones de errores menores diff --git a/fastlane/metadata/android/fr-FR/changelogs/349.txt b/fastlane/metadata/android/fr-FR/changelogs/349.txt new file mode 100644 index 0000000000000000000000000000000000000000..fb657c547ab70b21eda75966ac9ae9ff3029a72e --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/349.txt @@ -0,0 +1,4 @@ +* Introduction d'un paramètre expert pour faire la découverte de salons sur le serveur local au lieu de search.jabber.network +* Active les coches de délivrance par défaut et supprimer le paramètre +* Active ‘Le bouton Envoyer indique l'état’ par défaut et supprimer le paramètre +* Déplacer les paramètres du service de sauvegarde et de premier plan vers l'écran principal diff --git a/fastlane/metadata/android/gl-ES/changelogs/4211104.txt b/fastlane/metadata/android/gl-ES/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..bb3a29cb3829cd31b5e4f9bc6f1564a5fbd20938 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Programar copia de apoio +* Excluír a integración de chamadas en todos os dispositivos realme ata a versión Android 11 +* Pequenas melloras na interface (burbulla das mensaxes) diff --git a/fastlane/metadata/android/gl-ES/changelogs/4211204.txt b/fastlane/metadata/android/gl-ES/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..332791c8cb077494fea480b41f03bff6854570fc --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Arranxo da chamada que deixa de estar silenciada ao cambiar o dispositivo de saída +* Exclusión da integración de chamadas para todos os dispositivos Umidigi diff --git a/fastlane/metadata/android/gl-ES/changelogs/4211304.txt b/fastlane/metadata/android/gl-ES/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..7ca805abbefeea8aa10b592dc94e81f53f493fd8 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4211304.txt @@ -0,0 +1 @@ +* Crear a Copia de Apoio usando o servizo en primeiro plano para evitar que sexa detido após 10 minutos diff --git a/fastlane/metadata/android/gl-ES/changelogs/4211404.txt b/fastlane/metadata/android/gl-ES/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..bae283ec9337d8033076e363dd03519ac65f330e --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* excluír da integración de chamadas aos dispositivos Oppo antigos +* arranxos varios diff --git a/fastlane/metadata/android/gl-ES/changelogs/4211604.txt b/fastlane/metadata/android/gl-ES/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..ef27ca90434811b561de96e3e74bae86693aa06e --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4211604.txt @@ -0,0 +1 @@ +* Arranxo de problemas menores diff --git a/fastlane/metadata/android/it-IT/changelogs/4211104.txt b/fastlane/metadata/android/it-IT/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..9894b33eb06f59adacea076ae2cecad8859d64bc --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Programmazione di backup regolari +* Esclusione di tutti i dispositivi realme fino ad Android 11 dall'integrazione delle chiamate +* Piccoli miglioramenti dell'interfaccia (messaggi) diff --git a/fastlane/metadata/android/it-IT/changelogs/4211204.txt b/fastlane/metadata/android/it-IT/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..2e101d8e001d989001f6753f5a893bc77f371a89 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Corrette le chiamate che venivano smutate cambiando dispositivi di output +* Esclusi tutti i dispositivi Umidigi dall'integrazione di chiamate diff --git a/fastlane/metadata/android/it-IT/changelogs/4211304.txt b/fastlane/metadata/android/it-IT/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..5a83a3a699dbe94ffb849d281babd0902a94803c --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4211304.txt @@ -0,0 +1 @@ +* Avvia backup come servizio in primo piano per impedire che il servizio si fermi dopo 10 minuti diff --git a/fastlane/metadata/android/it-IT/changelogs/4211404.txt b/fastlane/metadata/android/it-IT/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f20152238235b044888b219380015a10bd00449 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* esclusione di dispositivi Oppo obsoleti dall'integrazione delle chiamate +* correzione di vari errori diff --git a/fastlane/metadata/android/it-IT/changelogs/4211604.txt b/fastlane/metadata/android/it-IT/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..f084c214e2c391573c00e7e12b6c04fa01f484b3 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4211604.txt @@ -0,0 +1 @@ +* Correzioni minori di errori diff --git a/fastlane/metadata/android/pl-PL/changelogs/4211104.txt b/fastlane/metadata/android/pl-PL/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..fba143dc9faf7b26fda6e2ffad954125bde98e9b --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Planowanie regularnej kopii zapasowej +* Wyłączenie wszystkich urządzeń Realme do Androida 11 z integracji rozmów +* Drobne poprawki interfejsu użytkownika (dymków wiadomości) diff --git a/fastlane/metadata/android/pl-PL/changelogs/4211204.txt b/fastlane/metadata/android/pl-PL/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..da160403c511b45e100df8eafd45d89258bd7c65 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Naprawienie wyłączania wyciszenia rozmowy przy przełączaniu urządzeń wyjściowych +* Wyłączenie wszystkich urządzeń Umidigi z integracji rozmów diff --git a/fastlane/metadata/android/pl-PL/changelogs/4211304.txt b/fastlane/metadata/android/pl-PL/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..318c2f3124f2597713eabbe47c9a3c2e55e023b5 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4211304.txt @@ -0,0 +1 @@ +* Uruchamianie kopii zapasowej jako usługi na pierwszym planie, żeby proces nie był zatrzymywany po 10 minutach diff --git a/fastlane/metadata/android/pl-PL/changelogs/4211404.txt b/fastlane/metadata/android/pl-PL/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..916a4c5fe2c5fa2f15c08efab5319b3caac4a3c8 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* Wyłączenie starszych urządzeń Umidigi z integracji rozmów +* Różne poprawki błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/4211604.txt b/fastlane/metadata/android/pl-PL/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f9a6e84501c3df605cbf3d649485b071f507f42 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4211604.txt @@ -0,0 +1 @@ +* Mniejsze poprawki błędów diff --git a/fastlane/metadata/android/sq/changelogs/4211104.txt b/fastlane/metadata/android/sq/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac42b4e67e39f40206122c81ba5be1189a751c8b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Planifikim kopjeruajtjesh periodike +* Përjashtim i krejt pajisjeve “realme”, deri te Android 11, nga integrim thirrjesh +* Përmirësime të vockla UI (flluskë mesazhi) diff --git a/fastlane/metadata/android/sq/changelogs/4211204.txt b/fastlane/metadata/android/sq/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b20cd394ca02858dc1b1b62c818194a1a1a23ce --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Ndreqje çheshtimi thirrjeje, kur ndërrohet pajisje dëgjimi +* Përjashtim i krejt pajisjeve Umidigi nga integrim thirrjesh diff --git a/fastlane/metadata/android/sq/changelogs/4211304.txt b/fastlane/metadata/android/sq/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..9b0191f551f5a645a7ddffb93828f0a0a8f7d9d1 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211304.txt @@ -0,0 +1 @@ +* Xhirim Kopjeruajtje si shërbim në prapaskenë, për të parandaluar ndalimin e procesit pas 10 minutash diff --git a/fastlane/metadata/android/sq/changelogs/4211404.txt b/fastlane/metadata/android/sq/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..6baac3ca45d268356aea58c85ab153595bea33e3 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* përjashtim pajisjesh të vjetra Oppo prej integrim thirrjesh +* ndreqje të metash të ndryshme diff --git a/fastlane/metadata/android/sq/changelogs/4211604.txt b/fastlane/metadata/android/sq/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..fda6c4ac95a94d1737c92aeb9c2fe69cab7fdc8a --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4211604.txt @@ -0,0 +1 @@ +* Ndreqje të metash të vockla diff --git a/fastlane/metadata/android/uk/changelogs/367.txt b/fastlane/metadata/android/uk/changelogs/367.txt index 4e697be13f077bb41714522ba476de35a0216ca0..8431ce8075eefa57f5258421d67f045d88a33e11 100644 --- a/fastlane/metadata/android/uk/changelogs/367.txt +++ b/fastlane/metadata/android/uk/changelogs/367.txt @@ -1,2 +1,2 @@ -* Виправлено вибір піктограми користувача на деяких пристроях з Android 10 +* Виправлено вибір аватара на деяких пристроях з Android 10 * Виправлення обміну файлами для великих файлів diff --git a/fastlane/metadata/android/uk/changelogs/395.txt b/fastlane/metadata/android/uk/changelogs/395.txt index 890b5c4739597ac5df3989d058aa069537f21898..b7d80af5796dda8c3d6925a162cbda3865fdd2c8 100644 --- a/fastlane/metadata/android/uk/changelogs/395.txt +++ b/fastlane/metadata/android/uk/changelogs/395.txt @@ -1,3 +1,3 @@ -* Додано «Повернутися до чату» на екрані звукового виклику +* Додано «Повернутися до чату» на екрані голосового виклику * Удосконалено комбінації клавіш * Виправлення помилок diff --git a/fastlane/metadata/android/uk/changelogs/402.txt b/fastlane/metadata/android/uk/changelogs/402.txt index 1f2ec0fbd50a2ffd6eaccd3a20420059b504bdb2..8058246597cc730cdc50c8b5919920c9a20a7c99 100644 --- a/fastlane/metadata/android/uk/changelogs/402.txt +++ b/fastlane/metadata/android/uk/changelogs/402.txt @@ -1,3 +1,3 @@ * Просте створення запрошень на серверах з підтримкою запрошень * Перегляд файлів GIF, отриманих з Movim -* Піктограми користувачів зберігаються у кеші +* Аватари зберігаються у кеші diff --git a/fastlane/metadata/android/uk/changelogs/42041.txt b/fastlane/metadata/android/uk/changelogs/42041.txt index f0c9635229e92a1a904cae432ba467767f9ce05d..2c9c5d4b62d067a88992dbe40270ed9b7b210f3f 100644 --- a/fastlane/metadata/android/uk/changelogs/42041.txt +++ b/fastlane/metadata/android/uk/changelogs/42041.txt @@ -1,5 +1,5 @@ * Реалізація Extensible SASL Profile, Bind 2.0 і Fast для швидшого повторного з'єднання * Реалізація Channel Binding * Додано можливість перемикатися з голосового на відеовиклик -* Додано можливість видаляти свою піктограму користувача +* Додано можливість видаляти свій аватар * Додано сповіщення про пропущені виклики diff --git a/fastlane/metadata/android/uk/changelogs/4211104.txt b/fastlane/metadata/android/uk/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..e0a5702fe3356ccfc949e2849786847ab3b58a26 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Планування регулярного резервного копіювання +* Виключення всіх пристроїв Realme до Android 11 з інтеграції викликів +* Незначні покращення інтерфейсу повідомлень diff --git a/fastlane/metadata/android/uk/changelogs/4211204.txt b/fastlane/metadata/android/uk/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..76b030cf621b1f737b38f9784772faafa77718b9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Виправлено ввімкнення звуку виклику при перемиканні пристроїв виводу +* Виключення всіх пристроїв Umidigi з інтеграції викликів diff --git a/fastlane/metadata/android/uk/changelogs/4211304.txt b/fastlane/metadata/android/uk/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..e50a9572ac8a7bd7e1b4c9495764b246a730bdd5 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211304.txt @@ -0,0 +1 @@ +* Резервне копіювання запускається як процес на передньому плані, щоб запобігти його зупинці через 10 хвилин diff --git a/fastlane/metadata/android/uk/changelogs/4211404.txt b/fastlane/metadata/android/uk/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..ff1007cbfdabb52cee8a35187858db1707cf6da1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* Виключення старих пристроїв Oppo з інтеграції викликів +* Виправлення різноманітних помилок diff --git a/fastlane/metadata/android/uk/changelogs/4211604.txt b/fastlane/metadata/android/uk/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..5055677c66cce09d060052a9941d17aecf3e6621 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4211604.txt @@ -0,0 +1 @@ +* Незначні виправлення помилок diff --git a/fastlane/metadata/android/zh-CN/changelogs/42037.txt b/fastlane/metadata/android/zh-CN/changelogs/42037.txt index bde3749452d5ba4ebbb8aa9011c67e35c837a54c..9af497aabebf2b274990cbaecf198bc15434c263 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/42037.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/42037.txt @@ -1,11 +1,11 @@ -版本2.10.9 +版本 2.10.9 * 进行音视频通话时请求蓝牙权限(如果您不使用蓝牙耳机可以拒绝) * 修复呼叫 Movim 时的错误 -* 修复群组聊天的显示错误头像的问题 +* 修复群聊显示错误头像的问题 * 始终要求选择退出电池优化 * 在“x 个已连接账号”通知上设置仅本地标志 * 修复与 Google 地图分享位置插件的交互 * 移除有关服务器费用的脚注 * 将文件存储在适合 Android 11 的位置 * 网络切换后尝试重新连接通话 -* 在来电屏幕中显示来电者JID和帐户JID +* 在来电屏幕中显示来电者 JID 和账号JID diff --git a/fastlane/metadata/android/zh-CN/changelogs/4210404.txt b/fastlane/metadata/android/zh-CN/changelogs/4210404.txt index 5fc634f3398044f42f98258768a4dc453611664e..ad241a2c7aa91e567f6fae13b4b73d4b19c57cf8 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/4210404.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/4210404.txt @@ -1,3 +1,3 @@ -* 修复安卓 8 上的音频/视频通话 +* 修复 Android 8 上的音频/视频通话 * 修复新呼叫集成中的竞态条件 * 修复视频压缩问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4210904.txt b/fastlane/metadata/android/zh-CN/changelogs/4210904.txt index f503dd304a91ca428d35a40efd659e30e7d7eb8e..fce371955e67d9971ad8ea55a370f1876d1c377a 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/4210904.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/4210904.txt @@ -1,2 +1,2 @@ * 修复 Android 6/7 上的 Quicksy 注册问题 -* 在通知通道上播放来电铃声 +* 在通知渠道上播放来电铃声 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211104.txt b/fastlane/metadata/android/zh-CN/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..6ae5e9f8ee89ddee3862a01eab0687da972dc1a1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* 计划定期备份 +* 从呼叫集成中排除所有 Android 11 以下的 realme 设备 +* 用户界面(消息气泡)小幅改进 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211204.txt b/fastlane/metadata/android/zh-CN/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..e3a4d283970b03a2a46af286276a9e303b8f162d --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* 修复切换输出设备时呼叫未静音的问题 +* 从呼叫集成中排除所有 Umidigi 设备 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211304.txt b/fastlane/metadata/android/zh-CN/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..fe176e4bf43ca62fde2b576629273af07faa5f9b --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211304.txt @@ -0,0 +1 @@ +* 作为前台服务运行备份,防止进程在 10 分钟后停止 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211404.txt b/fastlane/metadata/android/zh-CN/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..aa59d1f2eb5ffcc3a9b218fa7640b8f92ad4eb3f --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* 从呼叫集成中排除较旧的 OPPO 设备 +* 各种错误修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4211604.txt b/fastlane/metadata/android/zh-CN/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..9d049011e09872903f007ac9248a9b5ab7f131e1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4211604.txt @@ -0,0 +1 @@ +* 小错误修复 diff --git a/libs/annotation-processor/build.gradle b/libs/annotation-processor/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..6232f33c6e557d384e4e63db790d24e9ea0864c6 --- /dev/null +++ b/libs/annotation-processor/build.gradle @@ -0,0 +1,20 @@ +apply plugin: "java-library" + +repositories { + google() + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +dependencies { + + implementation project(':libs:annotation') + + annotationProcessor 'com.google.auto.service:auto-service:1.0.1' + api 'com.google.auto.service:auto-service-annotations:1.0.1' + implementation 'com.google.guava:guava:31.1-jre' + +} \ No newline at end of file diff --git a/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java b/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..c42cc5340537f328541761290dcb04eebae5d819 --- /dev/null +++ b/libs/annotation-processor/src/main/java/im/conversations/android/annotation/processor/XmlElementProcessor.java @@ -0,0 +1,185 @@ +package im.conversations.android.annotation.processor; + +import com.google.auto.service.AutoService; +import com.google.common.base.CaseFormat; +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.annotation.XmlPackage; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; +import javax.tools.JavaFileObject; + +@AutoService(Processor.class) +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedAnnotationTypes("im.conversations.android.annotation.XmlElement") +public class XmlElementProcessor extends AbstractProcessor { + + @Override + public boolean process(Set set, RoundEnvironment roundEnvironment) { + final Set elements = + roundEnvironment.getElementsAnnotatedWith(XmlElement.class); + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (final Element element : elements) { + if (element instanceof final TypeElement typeElement) { + final Id id = of(typeElement); + builder.put(id, typeElement.getQualifiedName().toString()); + } + } + final ImmutableMap maps = builder.build(); + if (maps.isEmpty()) { + return false; + } + final JavaFileObject extensionFile; + try { + extensionFile = + processingEnv + .getFiler() + .createSourceFile("im.conversations.android.xmpp.Extensions"); + } catch (final IOException e) { + throw new RuntimeException(e); + } + try (final PrintWriter out = new PrintWriter(extensionFile.openWriter())) { + out.println("package im.conversations.android.xmpp;"); + out.println("import com.google.common.collect.BiMap;"); + out.println("import com.google.common.collect.ImmutableBiMap;"); + out.println("import im.conversations.android.xmpp.ExtensionFactory;"); + out.println("import im.conversations.android.xmpp.model.Extension;"); + out.print("\n"); + out.println("public final class Extensions {"); + out.println( + "public static final BiMap>" + + " EXTENSION_CLASS_MAP;"); + out.println("static {"); + out.println( + "final var builder = new ImmutableBiMap.Builder>();"); + for (final Map.Entry entry : maps.entrySet()) { + Id id = entry.getKey(); + String clazz = entry.getValue(); + out.format( + "builder.put(new ExtensionFactory.Id(\"%s\",\"%s\"),%s.class);", + id.name, id.namespace, clazz); + out.print("\n"); + } + out.println("EXTENSION_CLASS_MAP = builder.build();"); + out.println("}"); + out.println(" private Extensions() {}"); + out.println("}"); + // writing generated file to out … + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + + private static Id of(final TypeElement typeElement) { + final XmlElement xmlElement = typeElement.getAnnotation(XmlElement.class); + final PackageElement packageElement = getPackageElement(typeElement); + final XmlPackage xmlPackage = + packageElement == null ? null : packageElement.getAnnotation(XmlPackage.class); + if (xmlElement == null) { + throw new IllegalStateException( + String.format( + "%s is not annotated as @XmlElement", + typeElement.getQualifiedName().toString())); + } + final String packageNamespace = xmlPackage == null ? null : xmlPackage.namespace(); + final String elementName = xmlElement.name(); + final String elementNamespace = xmlElement.namespace(); + final String namespace; + if (!Strings.isNullOrEmpty(elementNamespace)) { + namespace = elementNamespace; + } else if (!Strings.isNullOrEmpty(packageNamespace)) { + namespace = packageNamespace; + } else { + throw new IllegalStateException( + String.format( + "%s does not declare a namespace", + typeElement.getQualifiedName().toString())); + } + if (!hasEmptyDefaultConstructor(typeElement)) { + throw new IllegalStateException( + String.format( + "%s does not have an empty default constructor", + typeElement.getQualifiedName().toString())); + } + final String name; + if (Strings.isNullOrEmpty(elementName)) { + name = + CaseFormat.UPPER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, typeElement.getSimpleName().toString()); + } else { + name = elementName; + } + return new Id(name, namespace); + } + + private static PackageElement getPackageElement(final TypeElement typeElement) { + final Element parent = typeElement.getEnclosingElement(); + if (parent instanceof PackageElement) { + return (PackageElement) parent; + } else { + final Element nextParent = parent.getEnclosingElement(); + if (nextParent instanceof PackageElement) { + return (PackageElement) nextParent; + } else { + return null; + } + } + } + + private static boolean hasEmptyDefaultConstructor(final TypeElement typeElement) { + final List constructors = + ElementFilter.constructorsIn(typeElement.getEnclosedElements()); + for (final ExecutableElement constructor : constructors) { + if (constructor.getParameters().isEmpty() + && constructor.getModifiers().contains(Modifier.PUBLIC)) { + return true; + } + } + return false; + } + + public static class Id { + public final String name; + public final String namespace; + + public Id(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, namespace); + } + } +} diff --git a/libs/annotation/build.gradle b/libs/annotation/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..13a27e90c8f86b1aa7a00a15005dba57406dc570 --- /dev/null +++ b/libs/annotation/build.gradle @@ -0,0 +1,6 @@ +apply plugin: "java-library" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} \ No newline at end of file diff --git a/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java new file mode 100644 index 0000000000000000000000000000000000000000..68ff736352d3396aef81c6b6d9d57f5e72e547bb --- /dev/null +++ b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlElement.java @@ -0,0 +1,15 @@ +package im.conversations.android.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +public @interface XmlElement { + + String name() default ""; + + String namespace() default ""; +} diff --git a/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java new file mode 100644 index 0000000000000000000000000000000000000000..462fc6965b7a885c17d55a99262662169dde346e --- /dev/null +++ b/libs/annotation/src/main/java/im/conversations/android/annotation/XmlPackage.java @@ -0,0 +1,12 @@ +package im.conversations.android.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.PACKAGE) +public @interface XmlPackage { + String namespace(); +} diff --git a/proguard-rules.pro b/proguard-rules.pro index 7098945c6a1156274975ac2aa4fb05a1cc7d3b94..0f28bdddfda58b7b2a11dc9aae169d85ab2d67c1 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -1,6 +1,7 @@ -dontobfuscate -keep class eu.siacs.conversations.** +-keep class im.conversations.** -keep class org.whispersystems.** diff --git a/settings.gradle b/settings.gradle index 4193570fa762a8d407f5d06beec954146d2254ea..3cecbc88944cf4f85d51fa2a2c363dd3b327c062 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ +include ':libs:annotation', ':libs:annotation-processor:' + rootProject.name = 'Conversations' diff --git a/src/cheogram/java/com/cheogram/android/BobTransfer.java b/src/cheogram/java/com/cheogram/android/BobTransfer.java index b97b789cd2008390621d4c74c146e3ad03bced44..4006eb9b1c38fac09094dcece77be1164b3617af 100644 --- a/src/cheogram/java/com/cheogram/android/BobTransfer.java +++ b/src/cheogram/java/com/cheogram/android/BobTransfer.java @@ -30,7 +30,8 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; public class BobTransfer implements Transferable { protected int status = Transferable.STATUS_OFFER; @@ -93,13 +94,13 @@ public class BobTransfer implements Transferable { attempts.put(uri, System.currentTimeMillis()); changeStatus(Transferable.STATUS_DOWNLOADING); - IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final var request = new Iq(Iq.Type.GET); request.setTo(to); final Element dataq = request.addChild("data", "urn:xmpp:bob"); dataq.setAttribute("cid", uri.getSchemeSpecificPart()); - xmppConnectionService.sendIqPacket(account, request, (acct, packet) -> { + xmppConnectionService.sendIqPacket(account, request, (packet) -> { final Element data = packet.findChild("data", "urn:xmpp:bob"); - if (packet.getType() == IqPacket.TYPE.ERROR || data == null) { + if (packet.getType() == Iq.Type.ERROR || data == null) { Log.d(Config.LOGTAG, "BobTransfer failed: " + packet); finish(null); } else { diff --git a/src/cheogram/java/com/cheogram/android/FinishOnboarding.java b/src/cheogram/java/com/cheogram/android/FinishOnboarding.java index ff0d944dcabe911ef320269e51173054f3c3f3c6..86debf794f39d049568644534b92d9d5cd2e934b 100644 --- a/src/cheogram/java/com/cheogram/android/FinishOnboarding.java +++ b/src/cheogram/java/com/cheogram/android/FinishOnboarding.java @@ -16,7 +16,8 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; public class FinishOnboarding { private static final AtomicBoolean WORKING = new AtomicBoolean(false); @@ -32,14 +33,14 @@ public class FinishOnboarding { public static void finish(final XmppConnectionService xmppConnectionService, final XmppActivity activity, final Account onboardAccount, final Account newAccount) { if (!WORKING.compareAndSet(false, true)) return; - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(Jid.of("cheogram.com")); final Element c = packet.addChild("command", Namespace.COMMANDS); c.setAttribute("node", "change jabber id"); c.setAttribute("action", "execute"); Log.d(Config.LOGTAG, "" + packet); - xmppConnectionService.sendIqPacket(onboardAccount, packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(onboardAccount, packet, (iq) -> { Element command = iq.findChild("command", "http://jabber.org/protocol/commands"); if (command == null) { Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq); @@ -62,15 +63,15 @@ public class FinishOnboarding { iq.setAttribute("type", "set"); iq.removeAttribute("from"); iq.removeAttribute("id"); - xmppConnectionService.sendIqPacket(a, iq, (a2, iq2) -> { + xmppConnectionService.sendIqPacket(onboardAccount, iq, (iq2) -> { Element command2 = iq2.findChild("command", "http://jabber.org/protocol/commands"); if (command2 != null && command2.getAttribute("status") != null && command2.getAttribute("status").equals("completed")) { - final IqPacket regPacket = new IqPacket(IqPacket.TYPE.SET); + final var regPacket = new Iq(Iq.Type.SET); regPacket.setTo(Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register")); final Element c2 = regPacket.addChild("command", Namespace.COMMANDS); c2.setAttribute("node", "jabber:iq:register"); c2.setAttribute("action", "execute"); - xmppConnectionService.sendIqPacket(newAccount, regPacket, (a3, iq3) -> { + xmppConnectionService.sendIqPacket(newAccount, regPacket, (iq3) -> { Element command3 = iq3.findChild("command", "http://jabber.org/protocol/commands"); if (command3 == null) { Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3); @@ -93,7 +94,7 @@ public class FinishOnboarding { iq3.setAttribute("type", "set"); iq3.removeAttribute("from"); iq3.removeAttribute("id"); - xmppConnectionService.sendIqPacket(newAccount, iq3, (a4, iq4) -> { + xmppConnectionService.sendIqPacket(newAccount, iq3, (iq4) -> { Element command4 = iq4.findChild("command", "http://jabber.org/protocol/commands"); if (command4 != null && command4.getAttribute("status") != null && command4.getAttribute("status").equals("completed")) { xmppConnectionService.createContact(newAccount.getRoster().getContact(iq4.getFrom().asBareJid()), true); diff --git a/src/cheogram/res/xml/cache_paths.xml b/src/cheogram/res/xml/cache_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..a02c3fdfde6e1ee3c313c0275d5697b560775984 --- /dev/null +++ b/src/cheogram/res/xml/cache_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/conversations/fastlane/metadata/android/es-ES/short_description.txt b/src/conversations/fastlane/metadata/android/es-ES/short_description.txt index 7ed04767246cd7fab459ea0ebc43836a01ddd32f..11f7274bdebebf58375141e69e3b5a2fa1c70449 100644 --- a/src/conversations/fastlane/metadata/android/es-ES/short_description.txt +++ b/src/conversations/fastlane/metadata/android/es-ES/short_description.txt @@ -1 +1 @@ -Mensajería instantánea XMPP cifrada y fácil de usar para tu dispositivo móvil +Mensajería instantánea XMPP cifrada y fácil de usar para tu teléfono inteligente diff --git a/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt b/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt index 653bdec1053be7fd5ae5efa4a9f60085f43be614..12cf31963abc0687e336ead7cf835a78ecb9ba88 100644 --- a/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt +++ b/src/conversations/fastlane/metadata/android/fr-FR/full_description.txt @@ -1,4 +1,4 @@ -Facile à utiliser, fiable, respectueux de votre batterie. Prend en charge les images, les conversations de groupe et le chiffrement de bout-en-bout. +Facile à utiliser, fiable, respectueux de votre batterie. Prend en charge les images, les conversations de groupe et le chiffrement de bout en bout. Principes de conception : @@ -8,6 +8,7 @@ Principes de conception : * Nécessiter le moins de permissions possible Fonctionnalités : + * Chiffrement de bout-en-bout avec au choix, OMEMO ou OpenPGP * Envoi et réception d'images * Appels audio et vidéo chiffrés (DTLS-SRTP) @@ -27,12 +28,12 @@ Conversations fonctionne avec n'importe quel serveur XMPP. Cependant XMPP est un Ces XEP sont actuellement : -* XEP-0065: SOCKS5 Bytestreams (ou mod_proxy65). Sera utilisé pour transférer des fichiers si les deux correspondants sont derrière un pare-feu (NAT). -* XEP-0163: Personal Eventing Protocol pour les avatars -* XEP-0191: Blocking Command vous permet de mettre des spammeurs sur liste noire ou bloquer des contacts sans les retirer de vos contacts. -* XEP-0198: Stream Management permet à XMPP de survivre à des petites pannes de réseau et aux changements de la connexion TCP sous-jacente. -* XEP-0280: Message Carbons qui synchronise automatiquement les messages que vous envoyez à votre client de bureau et vous permet ainsi de passer sans heurt de votre client mobile à votre client de bureau et inversement dans une conversation. -* XEP-0237: Roster Versioning principalement pour économiser de la bande passante sur les connexions mobiles de mauvaise qualité. -* XEP-0313: Message Archive Management synchronise l'historique des messages avec le serveur. Retrouvez des messages qui ont été envoyés pendant que Conversations était hors ligne. -* XEP-0352: Client State Indication fait savoir au serveur si Conversations est ou n'est pas en arrière-plan. Permet au serveur d'économiser de la bande passante en différant des paquets non importants. -* XEP-0363: HTTP File Upload vous permet de partager des fichiers dans les conférences et avec des contacts hors-ligne. Nécessite un composant supplémentaire sur votre serveur. +* XEP-0065 : SOCKS5 Bytestreams (ou mod_proxy65). Sera utilisé pour transférer des fichiers si les deux correspondants sont derrière un pare-feu (NAT). +* XEP-0163 : Personal Eventing Protocol pour les avatars +* XEP-0191 : Blocking Command vous permet de mettre des spammeurs sur liste noire ou bloquer des contacts sans les retirer de vos contacts. +* XEP-0198 : Stream Management permet à XMPP de survivre à des petites pannes de réseau et aux changements de la connexion TCP sous-jacente. +* XEP-0280 : Message Carbons qui synchronise automatiquement les messages que vous envoyez à votre client de bureau et vous permet ainsi de passer sans heurt de votre client mobile à votre client de bureau et inversement dans une conversation. +* XEP-0237 : Roster Versioning principalement pour économiser de la bande passante sur les connexions mobiles de mauvaise qualité. +* XEP-0313 : Message Archive Management synchronise l'historique des messages avec le serveur. Retrouvez des messages qui ont été envoyés pendant que Conversations était hors ligne. +* XEP-0352 : Client State Indication fait savoir au serveur si Conversations est ou n'est pas en arrière-plan. Permet au serveur d'économiser de la bande passante en différant des paquets non importants. +* XEP-0363 : HTTP File Upload vous permet de partager des fichiers dans les conférences et avec des contacts hors-ligne. Nécessite un composant supplémentaire sur votre serveur. diff --git a/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt b/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt index b4ae66d63e11132d66b0742a0475b1c578f0b277..072c6ed1a866a42e0c34c3d4730bd9af5a63d4a3 100644 --- a/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt +++ b/src/conversations/fastlane/metadata/android/fr-FR/short_description.txt @@ -1 +1 @@ -Messagerie instantanée XMPP chiffrée, facile à utiliser avec votre appareil mobile +Messagerie instantanée XMPP chiffrée et facile avec votre appareil mobile diff --git a/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt b/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt index ade292722c0c54d2a8808802df9b0ddf550e6fc8..b41413005aafe88b527886a4d1d5f863d5f8155a 100644 --- a/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt +++ b/src/conversations/fastlane/metadata/android/ja-JP/short_description.txt @@ -1 +1 @@ -携帯端末で簡単に操作できるXMPP暗号化インスタント・メッセンジャー +暗号化対応、モバイル端末で簡単に使用できる XMPP インスタント・メッセンジャー diff --git a/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt b/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..3d12812f9cb3e361709c6a42382032b4e8752877 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,39 @@ +Eenvoudig te gebruiken, betrouwbaar, batterijvriendelijk. Met ingebouwde ondersteuning voor afbeeldingen, groepschats en e2e-codering. + +Ontwerpprincipes: + +* Fraai van uiterlijk en gemakkelijk te gebruiken zonder opoffering van de veiligheid of privacy +* Gebaseerd op bestaande, gevestigde protocollen +* Vereist geen Google-account of specifiek Google Cloud Messaging (GCM) +* Verlangt een minimum aan rechten + +Functies: + +* End-to-end-codering met OMEMO of OpenPGP +* Afbeeldingen verzenden en ontvangen +* Versleutelde audio- en videogesprekken (DTLS-SRTP) +* Intuïtieve gebruikersinterface volgens de richtlijnen van Android Design +* Afbeeldingen / avatars voor jouw contacten +* Synchronisatie met desktopclient +* Conferenties (met ondersteuning voor bladwijzers) +* Adresboekintegratie +* Meerdere accounts / uniform Postvak In +* Zeer lage impact op de levensduur van de batterij + +Conversations maakt het heel gemakkelijk om een account aan te maken op de gratis conversations.im-server. Conversations werkt daarbij ook met elke andere XMPP-server. Veel XMPP-servers worden beheerd door vrijwilligers en zijn gratis. + +XMPP-functies: + +Conversations werkt met elke bestaande XMPP-server. XMPP is echter een uitbreidbaar protocol. Deze extensies zijn ook gestandaardiseerd in zogenaamde XEP's. Conversations ondersteunt een aantal daarvan om de algehele gebruikerservaring te verbeteren. De kans bestaat dat jouw huidige XMPP-server deze extensies niet ondersteunt. Om het meeste uit gesprekken te halen, kun je overwegen om over te schakelen naar een XMPP-server die dat wel doet of - nog beter - je eigen XMPP-server voor jou en je vrienden te gebruiken. + +Deze XEP's zijn - vanaf nu: + +* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Wordt gebruikt om bestanden over te dragen als beide partijen achter een firewall zitten (NAT). +* XEP-0163: Personal Eventing Protocol for avatars +* XEP-0191: Blocking command laat je spammers op de zwarte lijst zetten of contacten blokkeren zonder ze uit je selectie te verwijderen. +* XEP-0198: Stream Management stelt XMPP in staat om kleine netwerkuitval en veranderingen van de onderliggende TCP-verbinding te overleven. +* XEP-0280: Message Carbons synchroniseert automatisch de berichten die je naar je desktopclient verzendt en stelt je zo in staat om, binnen één gesprek, naadloos over te schakelen van je mobiele client naar je desktopclient en terug. +* XEP-0237: Roster Versioning om bandbreedte te besparen op slechte mobiele verbindingen +* XEP-0313: Message Archive Management synchronisatie van berichtgeschiedenis met de server. Verzamelt berichten die zijn verzonden terwijl Conversations offline was. +* XEP-0352: Client State Indication laat de server weten of Conversations al dan niet op de achtergrond actief is. Hiermee kan de server bandbreedte besparen door onbelangrijke pakketten achter te houden. +* XEP-0363: HTTP File Upload stelt je in staat om bestanden te delen tijdens conferenties en met offline contacten. Vereist een extra component op je server. diff --git a/src/conversations/fastlane/metadata/android/nl-NL/short_description.txt b/src/conversations/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..76a5046f52a53a514a4c0198684bf85bd741a21c --- /dev/null +++ b/src/conversations/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Versleutelde, gebruiksvriendelijke XMPP messenger voor je mobiele apparaat diff --git a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 2f4c81671ca459553156fe6a8736f276c1bc0be3..6ab32682fd866ac7d906d058220fdbe2ea7598a4 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/conversations/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -109,7 +109,6 @@ public class ManageAccountActivity extends XmppActivity registerForContextMenu(binding.accountList); } - @Override public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { if (selectedAccount != null) { @@ -352,8 +351,14 @@ public class ManageAccountActivity extends XmppActivity } } - private void disableAccount(Account account) { + private void disableAccount(final Account account) { account.setOption(Account.OPTION_DISABLED, true); + if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": quick start disabled. account will regain this capability on the next connect"); + } if (!xmppConnectionService.updateAccount(account)) { Toast.makeText(this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); } diff --git a/src/conversations/res/values-et/strings.xml b/src/conversations/res/values-et/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/conversations/res/values-et/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/conversations/res/values-fr/strings.xml b/src/conversations/res/values-fr/strings.xml index 68de25b54251a43abc609d88a7bb38fec262176c..730443ae6eaa2ae8422ec7da8a89d417db5bb4bd 100644 --- a/src/conversations/res/values-fr/strings.xml +++ b/src/conversations/res/values-fr/strings.xml @@ -3,7 +3,8 @@ Choisissez votre fournisseur XMPP Utiliser conversations.im Créer un nouveau compte - Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant.\nRemarque : Certains fournisseurs de messagerie proposent également des comptes XMPP. + Avez-vous déjà un compte XMPP ? Cela peut être le cas si vous utilisez déjà un autre client XMPP ou si vous avez déjà utilisé Conversations auparavant. Sinon, vous pouvez créer un nouveau compte XMPP dès maintenant. +\nRemarque : Certains fournisseurs mail proposent également des comptes XMPP. XMPP est un réseau de messagerie instantanée indépendant du fournisseur. Vous pouvez utiliser cette application avec n’importe quel serveur XMPP de votre choix. \nToutefois, pour votre commodité, nous avons facilité la création d’un compte sur conversations.im ; un fournisseur spécialement conçu pour Conversations. Vous avez été invité à %1$s. Nous allons vous guider à travers le processus de création d’un compte.\nEn choisissant %1$s comme fournisseur, vous pourrez communiquer avec les utilisateurs des autres fournisseurs en leur donnant votre adresse XMPP complète. @@ -11,7 +12,7 @@ Votre invitation au serveur Code de provisionnement mal formaté Appuyez sur le bouton partager pour envoyer à votre contact une invitation pour %1$s. - Si vos contacts sont à proximité, ils peuvent aussi scanner le code ci-dessous pour accepter votre invitation. + Si votre contact se trouve près de vous, il peut aussi scanner le code ci-dessous pour accepter votre invitation. Rejoignez %1$s et discutez avec moi : %2$s Partager une invitation avec … \ No newline at end of file diff --git a/src/conversations/res/values-ja/strings.xml b/src/conversations/res/values-ja/strings.xml index 851af0d4a0be9fa670c4e72575d413694ba3323e..adb2eb808d94e143e5cd3df3e77385c187d7e039 100644 --- a/src/conversations/res/values-ja/strings.xml +++ b/src/conversations/res/values-ja/strings.xml @@ -2,10 +2,11 @@ XMPP プロバイダーを選択してください conversations.im を利用する - 新規アカウントを作成 - XMPP アカウントをお持ちですか?既にほかの XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はこちら。初めての方は、今すぐ新規 XMPP アカウントを作成できます。\nヒント: e メールのプロバイダーが XMPP アカウントも提供している場合があります。 - XMPP は、プロバイダーに依存しないインスタントメッセージのプロトコルです。 XMPP サーバーならどこでも、このアプリを使用することができます。 -\nよろしければ、 Conversations に最適化されたプロバイダー conversations.im で簡単にアカウントを作成することもできます。 + アカウントを新規作成 + XMPP アカウントをお持ちですか? 既に他の XMPP クライアントを利用しているか、 Conversations を利用したことがある場合はアカウントをお持ちです。初めての方は、今すぐ XMPP アカウントを新規作成できます。 +\nヒント: いくつかのメールプロバイダーは XMPP アカウントも提供しています。 + XMPP は、プロバイダーに依存しないインスタントメッセージのネットワークです。どの XMPP サーバーでもこのアプリを使用できます。 +\nConversations に最適化された conversations.im で簡単にアカウントを作成することもできます。 %1$s へ招待されました。アカウント作成手順をご案内します。 \n%1$s をプロバイダーに選択してほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 %1$s へ招待されました。ユーザー名は既に選択されています。アカウント作成手順をご案内します。 \nほかのプロバイダーのユーザーと会話するには、 XMPP のフルアドレスを相手にお知らせください。 サーバーの招待 diff --git a/src/conversations/res/values-pt-rBR/strings.xml b/src/conversations/res/values-pt-rBR/strings.xml index 210d814e165b9718dcae9ce84b0c44d59efaa73a..7e2b18910d5029cca757f6654326a7a60d2c5849 100644 --- a/src/conversations/res/values-pt-rBR/strings.xml +++ b/src/conversations/res/values-pt-rBR/strings.xml @@ -4,8 +4,8 @@ Usar o conversations.im Criar uma nova conta Você já possui uma conta XMPP? Esse pode ser o seu caso caso já esteja usando um outro cliente XMPP ou tenha usado o Conversations antes. Caso contrário, você pode criar uma nova conta XMPP agora.\nDica: alguns provedores de e-mail também fornecem contas XMPP. - O XMPP é uma rede de mensageria instantânea independente de provedor. Você pode usar esse cliente com qualquer servidor XMPP que você escolher. -\nEntretanto, para sua conveniência, nós simplificamos o processo de criação de uma conta em conversations.im, um provedor especialmente configurado para se usar com o Conversations. + XMPP é uma rede de mensagens instantâneas independente de provedor. Você pode usar este aplicativo com qualquer servidor XMPP de sua escolha. +\nNo entanto, para sua comodidade, facilitamos criar uma conta em conversas.im; um provedor especificamente adequado para uso com Conversations. Você foi convidado para %1$s. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nAo escolher %1$s como um provedor você conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Seu convite do servidor diff --git a/src/conversations/res/values-sv/strings.xml b/src/conversations/res/values-sv/strings.xml index 062a0c26fd9e2c5779bf239e9505298d027863ec..d90427a1696cd7eb657d77132385f7960129cb65 100644 --- a/src/conversations/res/values-sv/strings.xml +++ b/src/conversations/res/values-sv/strings.xml @@ -12,8 +12,8 @@ Dela inbjudan med… Du har blivit inbjuden till %1$s. Ett användarnamn har redan valts åt dig. Vi guidar dig genom processen för att skapa ett konto. \nDu kommer att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. - XMPP är ett leverantörsoberoende snabbmeddelandenätverk. Du kan använda den här klienten med vilken XMPP-server du än väljer. -\nMen för din bekvämlighet har vi gjort det enkelt att skapa ett konto på conversations.im; en leverantör som är speciellt lämpad för användning med Conversations. + XMPP är ett leverantörsoberoende snabbmeddelandenätverk. Du kan använda den här appen med en valfri XMPP-server som du själv väljer. +\nMen för din bekvämlighet har vi gjort det enkelt att skapa ett konto på conversations.im; en leverantör som är speciellt lämpad för appen Conversations. Du har blivit inbjuden till %1$s. Vi guidar dig genom processen för att skapa ett konto. \nNär du väljer %1$s som leverantör kommer du att kunna kommunicera med användare av andra leverantörer genom att ge dem din fullständiga XMPP-adress. \ No newline at end of file diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index b16b819f7b288f0229938a714499e72571b1a6ac..e1507867a4fffc811492721f88e39a81b136be03 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -13,7 +13,7 @@ \n向其他 XMPP 用户提供您的完整地址,就能和对方交流。 您的服务器邀请 配置代码格式不正确 - 轻击分享按钮,向您的联系人发送加入 %1$s 的邀请。 + 点击分享按钮,向您的联系人发送加入 %1$s 的邀请。 如果您的联系人在附近,对方也可以扫描下方二维码接受邀请。 加入 %1$s 和我聊天:%2$s 分享邀请至… diff --git a/src/free/java/eu/siacs/conversations/services/PushManagementService.java b/src/free/java/eu/siacs/conversations/services/PushManagementService.java index f436da434e14d0e90f2ff14233cc1f5a83d0d811..c6c5d232466c9861062f2a9600c3d9fb0a4fbf65 100644 --- a/src/free/java/eu/siacs/conversations/services/PushManagementService.java +++ b/src/free/java/eu/siacs/conversations/services/PushManagementService.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.services; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; public class PushManagementService { @@ -11,11 +10,7 @@ public class PushManagementService { this.mXmppConnectionService = service; } - void registerPushTokenOnServer(Account account) { - //stub implementation. only affects playstore flavor - } - - void unregisterChannel(Account account, String hash) { + public void registerPushTokenOnServer(Account account) { //stub implementation. only affects playstore flavor } @@ -26,8 +21,4 @@ public class PushManagementService { public boolean isStub() { return true; } - - public boolean availableAndUseful(Account account) { - return false; - } } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index a0eb77b17d895700925c8d43ed1f7fb818016117..8d986b59ac640c64ad610caa9187626a9418b148 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -142,10 +142,6 @@ - - diff --git a/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java b/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java new file mode 100644 index 0000000000000000000000000000000000000000..e5e406ca7a6ff6a947f99b0a7a99101c900c6b71 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/ResolutionUnsuccessfulException.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2022 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package de.gultsch.minidns; + +import org.minidns.MiniDnsException; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; + +import java.io.Serial; + +public class ResolutionUnsuccessfulException extends MiniDnsException { + + /** + * + */ + @Serial + private static final long serialVersionUID = 1L; + + public final Question question; + public final RESPONSE_CODE responseCode; + + public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) { + super("Asking for " + question + " yielded an error response " + responseCode); + this.question = question; + this.responseCode = responseCode; + } +} diff --git a/src/main/java/de/gultsch/minidns/ResolverResult.java b/src/main/java/de/gultsch/minidns/ResolverResult.java new file mode 100644 index 0000000000000000000000000000000000000000..3d80fb8df4d3dbffa43fa2924c2991497e2a8483 --- /dev/null +++ b/src/main/java/de/gultsch/minidns/ResolverResult.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015-2022 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package de.gultsch.minidns; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.MiniDnsException; +import org.minidns.MiniDnsException.NullResultException; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE; +import org.minidns.dnssec.DnssecResultNotAuthenticException; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.record.Data; + +public class ResolverResult { + + protected final Question question; + private final RESPONSE_CODE responseCode; + private final Set data; + private final boolean isAuthenticData; + protected final Set unverifiedReasons; + protected final DnsMessage answer; + protected final DnsQueryResult result; + + public ResolverResult(Question question, DnsQueryResult result, Set unverifiedReasons) throws NullResultException { + // TODO: Is this null check still needed? + if (result == null) { + throw new MiniDnsException.NullResultException(question.asMessageBuilder().build()); + } + + this.result = result; + + DnsMessage answer = result.response; + this.question = question; + this.responseCode = answer.responseCode; + this.answer = answer; + + Set r = answer.getAnswersFor(question); + if (r == null) { + this.data = Collections.emptySet(); + } else { + this.data = Collections.unmodifiableSet(r); + } + + if (unverifiedReasons == null) { + this.unverifiedReasons = null; + isAuthenticData = false; + } else { + this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons); + isAuthenticData = this.unverifiedReasons.isEmpty(); + } + } + + public boolean wasSuccessful() { + return responseCode == RESPONSE_CODE.NO_ERROR; + } + + public Set getAnswers() { + throwIseIfErrorResponse(); + return data; + } + + public Set getAnswersOrEmptySet() { + return data; + } + + public RESPONSE_CODE getResponseCode() { + return responseCode; + } + + public boolean isAuthenticData() { + throwIseIfErrorResponse(); + return isAuthenticData; + } + + /** + * Get the reasons the result could not be verified if any exists. + * + * @return The reasons the result could not be verified or null. + */ + public Set getUnverifiedReasons() { + throwIseIfErrorResponse(); + return unverifiedReasons; + } + + public Question getQuestion() { + return question; + } + + public void throwIfErrorResponse() throws ResolutionUnsuccessfulException { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException; + } + + private ResolutionUnsuccessfulException resolutionUnsuccessfulException; + + public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() { + if (wasSuccessful()) return null; + + if (resolutionUnsuccessfulException == null) { + resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode); + } + + return resolutionUnsuccessfulException; + } + + private DnssecResultNotAuthenticException dnssecResultNotAuthenticException; + + public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() { + if (!wasSuccessful()) + return null; + if (isAuthenticData) + return null; + + if (dnssecResultNotAuthenticException == null) { + dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons()); + } + + return dnssecResultNotAuthenticException; + } + + /** + * Get the raw answer DNS message we received. This is likely not what you want, try {@link #getAnswers()} instead. + * + * @return the raw answer DNS Message. + * @see #getAnswers() + */ + public DnsMessage getRawAnswer() { + return answer; + } + + public DnsQueryResult getDnsQueryResult() { + return result; + } + + @Override + public final String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append(getClass().getName()).append('\n') + .append("Question: ").append(question).append('\n') + .append("Response Code: ").append(responseCode).append('\n'); + + if (responseCode == RESPONSE_CODE.NO_ERROR) { + if (isAuthenticData) { + sb.append("Results verified via DNSSEC\n"); + } + if (hasUnverifiedReasons()) { + sb.append(unverifiedReasons).append('\n'); + } + sb.append(answer.answerSection); + } + + return sb.toString(); + } + + boolean hasUnverifiedReasons() { + return unverifiedReasons != null && !unverifiedReasons.isEmpty(); + } + + protected void throwIseIfErrorResponse() { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException(); + if (resolutionUnsuccessfulException != null) + throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful", + resolutionUnsuccessfulException); + } +} diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index dde4ff8db8e6233c55565dbd404c06c7e0c059cc..fe07997151b2b16d2d0c0415954e88dc6dad5e2a 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/src/main/java/eu/siacs/conversations/AppSettings.java @@ -10,6 +10,8 @@ import androidx.preference.PreferenceManager; import com.google.common.base.Strings; +import java.security.SecureRandom; + public class AppSettings { public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service"; @@ -44,6 +46,9 @@ public class AppSettings { public static final String COLORFUL_CHAT_BUBBLES = "use_green_background"; public static final String LARGE_FONT = "large_font"; + private static final String ACCEPT_INVITES_FROM_STRANGERS = "accept_invites_from_strangers"; + private static final String INSTALLATION_ID = "im.conversations.android.install_id"; + private final Context context; public AppSettings(final Context context) { @@ -133,4 +138,25 @@ public class AppSettings { public boolean isRequireChannelBinding() { return getBooleanPreference(REQUIRE_CHANNEL_BINDING, R.bool.require_channel_binding); } + + public synchronized long getInstallationId() { + final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + final long existing = sharedPreferences.getLong(INSTALLATION_ID, 0); + if (existing != 0) { + return existing; + } + final var secureRandom = new SecureRandom(); + final var installationId = secureRandom.nextLong(); + sharedPreferences.edit().putLong(INSTALLATION_ID, installationId).apply(); + return installationId; + } + + public synchronized void resetInstallationId() { + final var secureRandom = new SecureRandom(); + final var installationId = secureRandom.nextLong(); + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putLong(INSTALLATION_ID, installationId) + .apply(); + } } diff --git a/src/main/java/eu/siacs/conversations/Conversations.java b/src/main/java/eu/siacs/conversations/Conversations.java index 8f1d77d49b4ce682025fb7a1f978b505732e1a40..cc46f9c640e211a4486734c448583c333a5f7526 100644 --- a/src/main/java/eu/siacs/conversations/Conversations.java +++ b/src/main/java/eu/siacs/conversations/Conversations.java @@ -1,5 +1,6 @@ package eu.siacs.conversations; +import android.annotation.SuppressLint; import android.app.Application; import android.content.Context; import android.content.SharedPreferences; @@ -15,9 +16,17 @@ import eu.siacs.conversations.utils.ThemeHelper; public class Conversations extends Application { + @SuppressLint("StaticFieldLeak") + private static Context CONTEXT; + + public static Context getContext() { + return Conversations.CONTEXT; + } + @Override public void onCreate() { super.onCreate(); + CONTEXT = this.getApplicationContext(); ExceptionHelper.init(getApplicationContext()); applyThemeSettings(); } diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java index ed17c5695bcf4cbca881dd97882a39c376fb34c1..4e43dd93ab487f9159c7261d3aca2c8509b9b5a4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java @@ -3,11 +3,13 @@ package eu.siacs.conversations.crypto; import android.util.Log; import android.util.Pair; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; +import org.bouncycastle.asn1.ASN1Object; import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1TaggedObject; import org.bouncycastle.asn1.DERIA5String; -import org.bouncycastle.asn1.DERTaggedObject; import org.bouncycastle.asn1.DERUTF8String; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.x500.RDN; @@ -37,13 +39,15 @@ public class XmppDomainVerifier { private static final String SRV_NAME = "1.3.6.1.5.5.7.8.7"; private static final String XMPP_ADDR = "1.3.6.1.5.5.7.8.5"; - private static List getCommonNames(X509Certificate certificate) { + private static List getCommonNames(final X509Certificate certificate) { List domains = new ArrayList<>(); try { X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject(); RDN[] rdns = x500name.getRDNs(BCStyle.CN); for (int i = 0; i < rdns.length; ++i) { - domains.add(IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue())); + domains.add( + IETFUtils.valueToString( + x500name.getRDNs(BCStyle.CN)[i].getFirst().getValue())); } return domains; } catch (CertificateEncodingException e) { @@ -51,26 +55,26 @@ public class XmppDomainVerifier { } } - private static Pair parseOtherName(byte[] otherName) { + private static Pair parseOtherName(final byte[] otherName) { try { ASN1Primitive asn1Primitive = ASN1Primitive.fromByteArray(otherName); - if (asn1Primitive instanceof DERTaggedObject) { - ASN1Primitive inner = ((DERTaggedObject) asn1Primitive).getObject(); - if (inner instanceof DLSequence) { - DLSequence sequence = (DLSequence) inner; - if (sequence.size() >= 2 && sequence.getObjectAt(1) instanceof DERTaggedObject) { - String oid = sequence.getObjectAt(0).toString(); - ASN1Primitive value = ((DERTaggedObject) sequence.getObjectAt(1)).getObject(); - if (value instanceof DERUTF8String) { - return new Pair<>(oid, ((DERUTF8String) value).getString()); - } else if (value instanceof DERIA5String) { - return new Pair<>(oid, ((DERIA5String) value).getString()); + if (asn1Primitive instanceof ASN1TaggedObject taggedObject) { + final ASN1Object inner = taggedObject.getBaseObject(); + if (inner instanceof DLSequence sequence) { + if (sequence.size() >= 2 + && sequence.getObjectAt(1) instanceof ASN1TaggedObject evenInner) { + final String oid = sequence.getObjectAt(0).toString(); + final ASN1Object value = evenInner.getBaseObject(); + if (value instanceof DERUTF8String derutf8String) { + return new Pair<>(oid, derutf8String.getString()); + } else if (value instanceof DERIA5String deria5String) { + return new Pair<>(oid, deria5String.getString()); } } } } return null; - } catch (IOException e) { + } catch (final IOException e) { return null; } } @@ -98,14 +102,15 @@ public class XmppDomainVerifier { return false; } - public boolean verify(final String unicodeDomain, final String unicodeHostname, SSLSession sslSession) throws SSLPeerUnverifiedException { + public boolean verify( + final String unicodeDomain, final String unicodeHostname, SSLSession sslSession) + throws SSLPeerUnverifiedException { final String domain = IDN.toASCII(unicodeDomain); final String hostname = unicodeHostname == null ? null : IDN.toASCII(unicodeHostname); final Certificate[] chain = sslSession.getPeerCertificates(); - if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) { + if (chain.length == 0 || !(chain[0] instanceof X509Certificate certificate)) { return false; } - final X509Certificate certificate = (X509Certificate) chain[0]; final List commonNames = getCommonNames(certificate); if (isSelfSigned(certificate)) { if (commonNames.size() == 1 && matchDomain(domain, commonNames)) { @@ -115,11 +120,11 @@ public class XmppDomainVerifier { } try { final ValidDomains validDomains = parseValidDomains(certificate); - Log.d(LOGTAG, "searching for " + domain + " in srvNames: " + validDomains.srvNames + " xmppAddrs: " + validDomains.xmppAddrs + " domains:" + validDomains.domains); + Log.d(LOGTAG, "searching for " + domain + " in " + validDomains); if (hostname != null) { Log.d(LOGTAG, "also trying to verify hostname " + hostname); } - return validDomains.xmppAddrs.contains(domain) + return validDomains.xmppAddresses.contains(domain) || validDomains.srvNames.contains("_xmpp-client." + domain) || matchDomain(domain, validDomains.domains) || (hostname != null && matchDomain(hostname, validDomains.domains)); @@ -128,7 +133,8 @@ public class XmppDomainVerifier { } } - public static ValidDomains parseValidDomains(final X509Certificate certificate) throws CertificateParsingException { + public static ValidDomains parseValidDomains(final X509Certificate certificate) + throws CertificateParsingException { final List commonNames = getCommonNames(certificate); final Collection> alternativeNames = certificate.getSubjectAlternativeNames(); final List xmppAddrs = new ArrayList<>(); @@ -148,7 +154,9 @@ public class XmppDomainVerifier { xmppAddrs.add(otherName.second.toLowerCase(Locale.US)); break; default: - Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second); + Log.d( + LOGTAG, + "oid: " + otherName.first + " value: " + otherName.second); } } } else if (type == 2) { @@ -159,30 +167,40 @@ public class XmppDomainVerifier { } } } - if (srvNames.size() == 0 && xmppAddrs.size() == 0 && domains.size() == 0) { + if (srvNames.isEmpty() && xmppAddrs.isEmpty() && domains.isEmpty()) { domains.addAll(commonNames); } return new ValidDomains(xmppAddrs, srvNames, domains); } public static final class ValidDomains { - final List xmppAddrs; + final List xmppAddresses; final List srvNames; final List domains; - private ValidDomains(List xmppAddrs, List srvNames, List domains) { - this.xmppAddrs = xmppAddrs; + private ValidDomains( + List xmppAddresses, List srvNames, List domains) { + this.xmppAddresses = xmppAddresses; this.srvNames = srvNames; this.domains = domains; } public List all() { ImmutableList.Builder all = new ImmutableList.Builder<>(); - all.addAll(xmppAddrs); + all.addAll(xmppAddresses); all.addAll(srvNames); all.addAll(domains); return all.build(); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("xmppAddresses", xmppAddresses) + .add("srvNames", srvNames) + .add("domains", domains) + .toString(); + } } private boolean isSelfSigned(X509Certificate certificate) { 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 f072aebaae3b56875cb054609859f2d0dd64679d..55fb4cdcf7d9efce57de9ab8c7fd95fb86c7fd6b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -61,7 +61,6 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.DescriptionTransport; import eu.siacs.conversations.xmpp.jingle.OmemoVerification; import eu.siacs.conversations.xmpp.jingle.OmemoVerifiedRtpContentMap; @@ -70,8 +69,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.pep.PublishOptions; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { @@ -392,20 +390,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { Log.d(Config.LOGTAG, getLogprefix(account) + "publishOwnDeviceIdIfNeeded called, but PEP is broken. Ignoring... "); return; } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().asBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.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 - Element item = mXmppConnectionService.getIqParser().getItem(packet); - Set deviceIds = mXmppConnectionService.getIqParser().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 + 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); } + }); } @@ -455,40 +451,37 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private void publishDeviceIdsAndRefineAccessModel(final Set ids, final boolean firstAttempt) { final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(ids, publishOptions); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final Element error = packet.getType() == IqPacket.TYPE.ERROR ? packet.findChild("error") : null; - final boolean preConditionNotMet = PublishOptions.preconditionNotMet(packet); - 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); + 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); } - if (packet.getType() == IqPacket.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" + packet.findChild("error")); - } + @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")); + } + } } }); @@ -506,26 +499,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { verifier.initSign(x509PrivateKey, SECURE_RANDOM); verifier.update(axolotlPublicKey.serialize()); byte[] signature = verifier.sign(); - IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); + 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, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - 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); - } + 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); - } - }); - } + @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(); @@ -549,109 +539,106 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (this.changeAccessMode.get()) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server gained publish-options capabilities. changing access model"); } - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + final Iq packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().asBareJid(), getOwnDeviceId()); + mXmppConnectionService.sendIqPacket(account, packet, response -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - return; //ignore timeout. do nothing - } + if (response.getType() == Iq.Type.TIMEOUT) { + return; //ignore timeout. do nothing + } - if (packet.getType() == IqPacket.TYPE.ERROR) { - Element error = packet.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" + packet); - return; - } + 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; } + } - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - boolean flush = false; - if (bundle == null) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet); - 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:" + packet); + 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; } - 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 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; - } - } catch (InvalidKeyIdException e) { + // 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; } + } 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; + } - // 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) { + // 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."); + } + 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); - } + if (changed || changeAccessMode.get()) { + if (account.getPrivateKeyAlias() != null && Config.X509_VERIFICATION) { + mXmppConnectionService.publishDisplayName(account); + publishDeviceVerificationAndBundle(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(); - } + 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()); } }); } @@ -669,44 +656,41 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { final boolean wipe, final boolean firstAttempt) { final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null; - final IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( + 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, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, IqPacket packet) { - final boolean preconditionNotMet = PublishOptions.preconditionNotMet(packet); - 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 (packet.getType() == IqPacket.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(); + 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); } - } else if (packet.getType() == IqPacket.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: " + packet.toString()); + + @Override + public void onPushFailed() { + publishDeviceBundle(signedPreKeyRecord, preKeyRecords, announceAfter, wipe, false); } - 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; } }); } @@ -759,9 +743,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return Futures.immediateFuture(session); } final SettableFuture future = SettableFuture.create(); - final IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - Pair verification = mXmppConnectionService.getIqParser().verification(response); + 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"); @@ -846,7 +830,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void fetchDeviceIds(final Jid jid, OnDeviceIdsFetched callback) { - IqPacket packet; + final Iq packet; synchronized (this.fetchDeviceIdsMap) { List callbacks = this.fetchDeviceIdsMap.get(jid); if (callbacks != null) { @@ -866,11 +850,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } if (packet != null) { - mXmppConnectionService.sendIqPacket(account, packet, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + mXmppConnectionService.sendIqPacket(account, packet, response -> { + if (response.getType() == Iq.Type.RESULT) { fetchDeviceListStatus.put(jid, true); - Element item = mXmppConnectionService.getIqParser().getItem(response); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + final Element item = IqParser.getItem(response); + final Set deviceIds = IqParser.deviceIds(item); registerDevices(jid, deviceIds); final List callbacks; synchronized (fetchDeviceIdsMap) { @@ -882,7 +866,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } } } else { - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { fetchDeviceListStatus.remove(jid); } else { fetchDeviceListStatus.put(jid, false); @@ -929,16 +913,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } final Jid jid = Jid.of(address.getName()); final boolean oneOfOurs = jid.asBareJid().equals(account.getJid().asBareJid()); - IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(jid, address.getDeviceId()); - mXmppConnectionService.sendIqPacket(account, bundlesPacket, (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + 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() == IqPacket.TYPE.RESULT) { + } else if (packet.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final List preKeyBundleList = parser.preKeys(packet); - final PreKeyBundle bundle = parser.bundle(packet); + 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); @@ -1544,7 +1527,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { axolotlMessage.addDevice(session, true); try { final Jid jid = Jid.of(session.getRemoteAddress().getName()); - MessagePacket 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); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index bd35361740364d8202dfc547259ee86f06350feb..0672a372f0b9800b9723c22fd5ca0afb00ee160e 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -146,10 +146,10 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Option; import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; import static eu.siacs.conversations.entities.Bookmark.printableValue; +import im.conversations.android.xmpp.model.stanza.Iq; public class Conversation extends AbstractEntity implements Blockable, Comparable, Conversational, AvatarService.Avatarable { public static final String TABLENAME = "conversations"; @@ -1597,7 +1597,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl show(); CommandSession session = new CommandSession(command.getAttribute("name"), command.getAttribute("node"), xmppConnectionService); - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(command.getAttributeAsJid("jid")); final Element c = packet.addChild("command", Namespace.COMMANDS); c.setAttribute("node", command.getAttribute("node")); @@ -1615,7 +1615,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } }, 1000); } else { - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { session.updateWithResponse(iq); }, 120L); } @@ -1642,7 +1642,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void startMucConfig(XmppConnectionService xmppConnectionService) { MucConfigSession session = new MucConfigSession(xmppConnectionService); - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + final var packet = new Iq(Iq.Type.GET); packet.setTo(Conversation.this.getJid().asBareJid()); packet.addChild("query", "http://jabber.org/protocol/muc#owner"); @@ -1658,7 +1658,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } }, 1000); } else { - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { session.updateWithResponse(iq); }, 120L); } @@ -2779,7 +2779,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl protected Item mkItem(Element el, int pos) { int viewType = TYPE_ERROR; - if (response != null && response.getType() == IqPacket.TYPE.RESULT) { + if (response != null && response.getType() == Iq.Type.RESULT) { if (el.getName().equals("note")) { viewType = TYPE_NOTE; } else if (el.getNamespace().equals("jabber:x:oob")) { @@ -2880,7 +2880,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl protected String mTitle; protected String mNode; protected CommandPageBinding mBinding = null; - protected IqPacket response = null; + protected Iq response = null; protected Element responseElement = null; protected boolean expectingRemoval = false; protected List reported = null; @@ -2890,7 +2890,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl protected GridLayoutManager layoutManager; protected WebView actionToWebview = null; protected int fillableFieldCount = 0; - protected IqPacket pendingResponsePacket = null; + protected Iq pendingResponsePacket = null; protected boolean waitingForRefresh = false; CommandSession(String title, String node, XmppConnectionService xmppConnectionService) { @@ -2909,7 +2909,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return mNode; } - public void updateWithResponse(final IqPacket iq) { + public void updateWithResponse(final Iq iq) { if (getView() != null && getView().isAttachedToWindow()) { getView().post(() -> updateWithResponseUiThread(iq)); } else { @@ -2917,7 +2917,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } } - protected void updateWithResponseUiThread(final IqPacket iq) { + protected void updateWithResponseUiThread(final Iq iq) { Timer oldTimer = this.loadingTimer; this.loadingTimer = new Timer(); oldTimer.cancel(); @@ -2934,7 +2934,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl boolean actionsCleared = false; Element command = iq.findChild("command", "http://jabber.org/protocol/commands"); - if (iq.getType() == IqPacket.TYPE.RESULT && command != null) { + if (iq.getType() == Iq.Type.RESULT && command != null) { if (mNode.equals("jabber:iq:register") && command.getAttribute("status") != null && command.getAttribute("status").equals("completed")) { xmppConnectionService.createContact(getAccount().getRoster().getContact(iq.getFrom()), true); } @@ -3098,7 +3098,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public int getItemCount() { if (loading) return 1; if (response == null) return 0; - if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) { + if (response.getType() == Iq.Type.RESULT && responseElement != null && responseElement.getNamespace().equals("jabber:x:data")) { int i = 0; for (Element el : responseElement.getChildren()) { if (!el.getNamespace().equals("jabber:x:data")) continue; @@ -3131,7 +3131,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (items.get(position) != null) return items.get(position); if (response == null) return null; - if (response.getType() == IqPacket.TYPE.RESULT && responseElement != null) { + if (response.getType() == Iq.Type.RESULT && responseElement != null) { if (responseElement.getNamespace().equals("jabber:x:data")) { int i = 0; for (Element el : responseElement.getChildren()) { @@ -3314,7 +3314,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return false; } - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(response.getFrom()); final Element c = packet.addChild("command", Namespace.COMMANDS); c.setAttribute("node", mNode); @@ -3357,7 +3357,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (c.getAttribute("action") == null) c.setAttribute("action", action); executing = true; - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { updateWithResponse(iq); }, 120L); @@ -3492,7 +3492,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl actionsAdapter.notifyDataSetChanged(); if (pendingResponsePacket != null) { - final IqPacket pending = pendingResponsePacket; + final var pending = pendingResponsePacket; pendingResponsePacket = null; updateWithResponseUiThread(pending); } @@ -3567,7 +3567,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } @Override - protected void updateWithResponseUiThread(final IqPacket iq) { + protected void updateWithResponseUiThread(final Iq iq) { Timer oldTimer = this.loadingTimer; this.loadingTimer = new Timer(); oldTimer.cancel(); @@ -3583,7 +3583,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl layoutManager.setSpanCount(1); final Element query = iq.findChild("query", "http://jabber.org/protocol/muc#owner"); - if (iq.getType() == IqPacket.TYPE.RESULT && query != null) { + if (iq.getType() == Iq.Type.RESULT && query != null) { final Data form = Data.parse(query.findChild("x", "jabber:x:data")); final String title = form.getTitle(); if (title != null) { @@ -3602,7 +3602,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (actionsAdapter.getPosition("cancel") < 0) { actionsAdapter.insert(Pair.create("cancel", "cancel"), 0); } - } else if (iq.getType() == IqPacket.TYPE.RESULT) { + } else if (iq.getType() == Iq.Type.RESULT) { expectingRemoval = true; removeSession(this); return; @@ -3616,7 +3616,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl @Override public synchronized boolean execute(String action) { if ("cancel".equals(action)) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(response.getFrom()); final Element form = packet .addChild("query", "http://jabber.org/protocol/muc#owner") @@ -3628,7 +3628,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (!"save".equals(action)) return true; - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + final var packet = new Iq(Iq.Type.SET); packet.setTo(response.getFrom()); String formType = responseElement == null ? null : responseElement.getAttribute("type"); @@ -3644,7 +3644,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } executing = true; - xmppConnectionService.sendIqPacket(getAccount(), packet, (a, iq) -> { + xmppConnectionService.sendIqPacket(getAccount(), packet, (iq) -> { updateWithResponse(iq); }, 120L); diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 2227e63b690cbb6457ef794456b8657fdfe77f90..d874b600e5b86094690544e4605fa45b29844229 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -28,6 +28,14 @@ import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xml.Element; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + public class MucOptions { public static final String STATUS_CODE_SELF_PRESENCE = "110"; @@ -198,6 +206,11 @@ public class MucOptions { } } + public boolean allowPmRaw() { + final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowpm"); + return field == null || Arrays.asList("anyone","participants").contains(field.getValue()); + } + public boolean participating() { return self.getRole().ranks(Role.PARTICIPANT) || !moderated(); } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index 1168d205e1d482c9072d9d5222717278b4e322e2..e784b3e8cb8f0bfc9542e27860463f2fce0c21c6 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -24,7 +24,7 @@ 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 eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class ServiceDiscoveryResult { public static final String TABLENAME = "discovery_results"; @@ -36,7 +36,7 @@ public class ServiceDiscoveryResult { protected final List features; protected final List forms; private final List identities; - public ServiceDiscoveryResult(final IqPacket packet) { + public ServiceDiscoveryResult(final Iq packet) { this.identities = new ArrayList<>(); this.features = new ArrayList<>(); this.forms = new ArrayList<>(); @@ -279,7 +279,7 @@ public class ServiceDiscoveryResult { return values; } - public static class Identity implements Comparable { + public static class Identity implements Comparable { protected final String type; protected final String lang; protected final String name; @@ -327,8 +327,21 @@ public class ServiceDiscoveryResult { return this.name; } - public int compareTo(@NonNull Object other) { - Identity o = (Identity) other; + 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())); @@ -342,18 +355,5 @@ public class ServiceDiscoveryResult { return r; } - - 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; - } - } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index e2ad9bdc53fd6dc22d5433181c4f8d014d89c2da..42ab107a1e92ee5a78eb9e9b994200fb24553b59 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -45,7 +45,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class IqGenerator extends AbstractGenerator { @@ -53,8 +53,8 @@ public class IqGenerator extends AbstractGenerator { super(service); } - public IqPacket discoResponse(final Account account, final IqPacket request) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.RESULT); + public Iq discoResponse(final Account account, final Iq request) { + final var packet = new Iq(Iq.Type.RESULT); packet.setId(request.getId()); packet.setTo(request.getFrom()); final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); @@ -69,8 +69,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket versionResponse(final IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + public Iq versionResponse(final Iq request) { + final var packet = request.generateResponse(Iq.Type.RESULT); Element query = packet.query("jabber:iq:version"); query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name)); query.addChild("version").setContent(getIdentityVersion()); @@ -93,8 +93,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket entityTimeResponse(IqPacket request) { - final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + public Iq entityTimeResponse(final Iq request) { + final Iq packet = request.generateResponse(Iq.Type.RESULT); Element time = packet.addChild("time", "urn:xmpp:time"); final long now = System.currentTimeMillis(); time.addChild("utc").setContent(getTimestamp(now)); @@ -113,14 +113,14 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket purgeOfflineMessages() { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public static Iq purgeOfflineMessages() { + final Iq packet = new Iq(Iq.Type.SET); packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge"); return packet; } - protected IqPacket publish(final String node, final Element item, final Bundle options) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + protected Iq publish(final String node, final Element item, final Bundle options) { + final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element publish = pubsub.addChild("publish"); publish.setAttribute("node", node); @@ -132,12 +132,12 @@ public class IqGenerator extends AbstractGenerator { return packet; } - protected IqPacket publish(final String node, final Element item) { + protected Iq publish(final String node, final Element item) { return publish(node, item, null); } - private IqPacket retrieve(String node, Element item) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + private Iq retrieve(String node, Element item) { + final var packet = new Iq(Iq.Type.GET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element items = pubsub.addChild("items"); items.setAttribute("node", node); @@ -147,36 +147,36 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket retrieveVcard4(final Jid jid) { - final IqPacket packet = retrieve("urn:xmpp:vcard4", null); + public Iq retrieveVcard4(final Jid jid) { + final var packet = retrieve("urn:xmpp:vcard4", null); packet.setTo(jid); return packet; } - public IqPacket retrieveBookmarks() { + public Iq retrieveBookmarks() { return retrieve(Namespace.BOOKMARKS2, null); } - public IqPacket retrieveMds() { + public Iq retrieveMds() { return retrieve(Namespace.MDS_DISPLAYED, null); } - public IqPacket publishNick(String nick) { + public Iq publishNick(String nick) { final Element item = new Element("item"); item.setAttribute("id", "current"); item.addChild("nick", Namespace.NICK).setContent(nick); return publish(Namespace.NICK, item); } - public IqPacket deleteNode(final String node) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq deleteNode(final String node) { + final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB_OWNER); pubsub.addChild("delete").setAttribute("node", node); return packet; } - public IqPacket deleteItem(final String node, final String id) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq deleteItem(final String node, final String id) { + final var packet = new Iq(Iq.Type.SET); final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB); final Element retract = pubsub.addChild("retract"); retract.setAttribute("node", node); @@ -185,7 +185,7 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket publishAvatar(Avatar avatar, Bundle options) { + public Iq publishAvatar(Avatar avatar, Bundle options) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element data = item.addChild("data", Namespace.AVATAR_DATA); @@ -193,14 +193,14 @@ public class IqGenerator extends AbstractGenerator { return publish(Namespace.AVATAR_DATA, item, options); } - public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) { + public Iq publishElement(final String namespace, final Element element, String id, final Bundle options) { final Element item = new Element("item"); item.setAttribute("id", id); item.addChild(element); return publish(namespace, item, options); } - public IqPacket publishAvatarMetadata(final Avatar avatar, final Bundle options) { + public Iq publishAvatarMetadata(final Avatar avatar, final Bundle options) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); final Element metadata = item @@ -214,57 +214,57 @@ public class IqGenerator extends AbstractGenerator { return publish(Namespace.AVATAR_METADATA, item, options); } - public IqPacket retrievePepAvatar(final Avatar avatar) { + public Iq retrievePepAvatar(final Avatar avatar) { final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - final IqPacket packet = retrieve(Namespace.AVATAR_DATA, item); + final var packet = retrieve(Namespace.AVATAR_DATA, item); packet.setTo(avatar.owner); return packet; } - public IqPacket retrieveVcardAvatar(final Avatar avatar) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq retrieveVcardAvatar(final Avatar avatar) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(avatar.owner); packet.addChild("vCard", "vcard-temp"); return packet; } - public IqPacket retrieveVcardAvatar(final Jid to) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq retrieveVcardAvatar(final Jid to) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(to); packet.addChild("vCard", "vcard-temp"); return packet; } - public IqPacket retrieveAvatarMetaData(final Jid to) { - final IqPacket packet = retrieve("urn:xmpp:avatar:metadata", null); + public Iq retrieveAvatarMetaData(final Jid to) { + final Iq packet = retrieve("urn:xmpp:avatar:metadata", null); if (to != null) { packet.setTo(to); } return packet; } - public IqPacket retrieveDeviceIds(final Jid to) { - final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); + public Iq retrieveDeviceIds(final Jid to) { + final var packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); if (to != null) { packet.setTo(to); } return packet; } - public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); + public Iq retrieveBundlesForDevice(final Jid to, final int deviceid) { + final var packet = retrieve(AxolotlService.PEP_BUNDLES + ":" + deviceid, null); packet.setTo(to); return packet; } - public IqPacket retrieveVerificationForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); + public Iq retrieveVerificationForDevice(final Jid to, final int deviceid) { + final var packet = retrieve(AxolotlService.PEP_VERIFICATION + ":" + deviceid, null); packet.setTo(to); return packet; } - public IqPacket publishDeviceIds(final Set ids, final Bundle publishOptions) { + public Iq publishDeviceIds(final Set ids, final Bundle publishOptions) { final Element item = new Element("item"); item.setAttribute("id", "current"); final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); @@ -314,7 +314,7 @@ public class IqGenerator extends AbstractGenerator { return displayed; } - public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, + public Iq publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, final Set preKeyRecords, final int deviceId, Bundle publishOptions) { final Element item = new Element("item"); item.setAttribute("id", "current"); @@ -338,7 +338,7 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions); } - public IqPacket publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { + public Iq publishVerification(byte[] signature, X509Certificate[] certificates, final int deviceId) { final Element item = new Element("item"); item.setAttribute("id", "current"); final Element verification = item.addChild("verification", AxolotlService.PEP_PREFIX); @@ -356,8 +356,8 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item); } - public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq queryMessageArchiveManagement(final MessageArchiveService.Query mam) { + final Iq packet = new Iq(Iq.Type.SET); final Element query = packet.query(mam.version.namespace); query.setAttribute("queryid", mam.getQueryId()); final Data data = new Data(); @@ -387,15 +387,15 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket generateGetBlockList() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + public Iq generateGetBlockList() { + final Iq iq = new Iq(Iq.Type.GET); iq.addChild("blocklist", Namespace.BLOCKING); return iq; } - public IqPacket generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + public Iq generateSetBlockRequest(final Jid jid, final boolean reportSpam, final String serverMsgId) { + final Iq iq = new Iq(Iq.Type.SET); final Element block = iq.addChild("block", Namespace.BLOCKING); final Element item = block.addChild("item").setAttribute("jid", jid); if (reportSpam) { @@ -411,15 +411,15 @@ public class IqGenerator extends AbstractGenerator { return iq; } - public IqPacket generateSetUnblockRequest(final Jid jid) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + public Iq generateSetUnblockRequest(final Jid jid) { + final Iq iq = new Iq(Iq.Type.SET); final Element block = iq.addChild("unblock", Namespace.BLOCKING); block.addChild("item").setAttribute("jid", jid); return iq; } - public IqPacket generateSetPassword(final Account account, final String newPassword) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq generateSetPassword(final Account account, final String newPassword) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(account.getDomain()); final Element query = packet.addChild("query", Namespace.REGISTER); final Jid jid = account.getJid(); @@ -428,14 +428,14 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket changeAffiliation(Conversation conference, Jid jid, String affiliation) { + public Iq changeAffiliation(Conversation conference, Jid jid, String affiliation) { List jids = new ArrayList<>(); jids.add(jid); return changeAffiliation(conference, jids, affiliation); } - public IqPacket changeAffiliation(Conversation conference, List jids, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq changeAffiliation(Conversation conference, List jids, String affiliation) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(conference.getJid().asBareJid()); packet.setFrom(conference.getAccount().getJid()); Element query = packet.query("http://jabber.org/protocol/muc#admin"); @@ -447,8 +447,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket changeRole(Conversation conference, String nick, String role) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq changeRole(Conversation conference, String nick, String role) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(conference.getJid().asBareJid()); packet.setFrom(conference.getAccount().getJid()); Element item = packet.query("http://jabber.org/protocol/muc#admin").addChild("item"); @@ -457,11 +457,11 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket moderateMessage(Account account, Message m, String reason) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq moderateMessage(Account account, Message m, String reason) { + final var packet = new Iq(Iq.Type.SET); packet.setTo(m.getConversation().getJid().asBareJid()); packet.setFrom(account.getJid()); - Element moderate = + final var moderate = packet.addChild("apply-to", "urn:xmpp:fasten:0") .setAttribute("id", m.getServerMsgId()) .addChild("moderate", "urn:xmpp:message-moderate:0"); @@ -470,8 +470,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq requestHttpUploadSlot(Jid host, DownloadableFile file, String name, String mime) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(host); Element request = packet.addChild("request", Namespace.HTTP_UPLOAD); request.setAttribute("filename", name == null ? convertFilename(file.getName()) : name); @@ -480,8 +480,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(host); Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY); request.addChild("filename").setContent(convertFilename(file.getName())); @@ -507,8 +507,8 @@ public class IqGenerator extends AbstractGenerator { } } - public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) { - final IqPacket register = new IqPacket(IqPacket.TYPE.SET); + public static Iq generateCreateAccountWithCaptcha(final Account account, final String id, final Data data) { + final Iq register = new Iq(Iq.Type.SET); register.setFrom(account.getJid().asBareJid()); register.setTo(account.getDomain()); register.setId(id); @@ -519,12 +519,12 @@ public class IqGenerator extends AbstractGenerator { return register; } - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { + public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId) { return pushTokenToAppServer(appServer, token, deviceId, null); } - public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(appServer); final Element command = packet.addChild("command", Namespace.COMMANDS); command.setAttribute("node", "register-push-fcm"); @@ -540,8 +540,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { - final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) { + final Iq packet = new Iq(Iq.Type.SET); packet.setTo(appServer); final Element command = packet.addChild("command", Namespace.COMMANDS); command.setAttribute("node", "unregister-push-fcm"); @@ -554,8 +554,8 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket enablePush(final Jid jid, final String node, final String secret) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq enablePush(final Jid jid, final String node, final String secret) { + final Iq packet = new Iq(Iq.Type.SET); Element enable = packet.addChild("enable", Namespace.PUSH); enable.setAttribute("jid", jid); enable.setAttribute("node", node); @@ -569,16 +569,16 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket disablePush(final Jid jid, final String node) { - IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + public Iq disablePush(final Jid jid, final String node) { + Iq packet = new Iq(Iq.Type.SET); Element disable = packet.addChild("disable", Namespace.PUSH); disable.setAttribute("jid", jid); disable.setAttribute("node", node); return packet; } - public IqPacket queryAffiliation(Conversation conversation, String affiliation) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq queryAffiliation(Conversation conversation, String affiliation) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(conversation.getJid().asBareJid()); packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation", affiliation); return packet; @@ -611,16 +611,16 @@ public class IqGenerator extends AbstractGenerator { return options; } - public IqPacket requestPubsubConfiguration(Jid jid, String node) { + public Iq requestPubsubConfiguration(Jid jid, String node) { return pubsubConfiguration(jid, node, null); } - public IqPacket publishPubsubConfiguration(Jid jid, String node, Data data) { + public Iq publishPubsubConfiguration(Jid jid, String node, Data data) { return pubsubConfiguration(jid, node, data); } - private IqPacket pubsubConfiguration(Jid jid, String node, Data data) { - IqPacket packet = new IqPacket(data == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); + private Iq pubsubConfiguration(Jid jid, String node, Data data) { + final Iq packet = new Iq(data == null ? Iq.Type.GET : Iq.Type.SET); packet.setTo(jid); Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); Element configure = pubsub.addChild("configure").setAttribute("node", node); @@ -630,43 +630,43 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket queryDiscoItems(Jid jid) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq queryDiscoItems(final Jid jid) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(jid); packet.query(Namespace.DISCO_ITEMS); return packet; } - public IqPacket queryDiscoItems(Jid jid, String node) { - IqPacket packet = queryDiscoItems(jid); - final Element query = packet.query(Namespace.DISCO_ITEMS); + public Iq queryDiscoItems(Jid jid, String node) { + final var packet = queryDiscoItems(jid); + final var query = packet.query(Namespace.DISCO_ITEMS); query.setAttribute("node", node); return packet; } - public IqPacket queryDiscoInfo(Jid jid) { - IqPacket packet = new IqPacket(IqPacket.TYPE.GET); + public Iq queryDiscoInfo(final Jid jid) { + final Iq packet = new Iq(Iq.Type.GET); packet.setTo(jid); packet.addChild("query",Namespace.DISCO_INFO); return packet; } - public IqPacket bobResponse(IqPacket request) { + public Iq bobResponse(Iq request) { try { - String bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid"); - Cid cid = BobTransfer.cid(bobCid); - DownloadableFile f = mXmppConnectionService.getFileForCid(cid); + final var bobCid = request.findChild("data", "urn:xmpp:bob").getAttribute("cid"); + final var cid = BobTransfer.cid(bobCid); + final var f = mXmppConnectionService.getFileForCid(cid); if (f == null || !f.canRead()) { throw new IOException("No such file"); } else if (f.getSize() > 129000) { - final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); + final var response = request.generateResponse(Iq.Type.ERROR); + final var error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("policy-violation", "urn:ietf:params:xml:ns:xmpp-stanzas"); return response; } else { - final IqPacket response = request.generateResponse(IqPacket.TYPE.RESULT); - final Element data = response.addChild("data", "urn:xmpp:bob"); + final var response = request.generateResponse(Iq.Type.RESULT); + final var data = response.addChild("data", "urn:xmpp:bob"); data.setAttribute("cid", bobCid); data.setAttribute("type", f.getMimeType()); ByteArrayOutputStream b64 = new ByteArrayOutputStream((int) f.getSize() * 2); @@ -678,8 +678,8 @@ public class IqGenerator extends AbstractGenerator { return response; } } catch (final IOException | IllegalStateException e) { - final IqPacket response = request.generateResponse(IqPacket.TYPE.ERROR); - final Element error = response.addChild("error"); + final var response = request.generateResponse(Iq.Type.ERROR); + final var error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("item-not-found", "urn:ietf:params:xml:ns:xmpp-stanzas"); return response; diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 8627b15562133d8c53910ee31cb15d57a9b60038..463bad4e3599188220377bcddf1548a97a46e5f5 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -23,7 +23,6 @@ import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class MessageGenerator extends AbstractGenerator { private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo"; @@ -33,25 +32,25 @@ public class MessageGenerator extends AbstractGenerator { super(service); } - private MessagePacket preparePacket(Message message, boolean legacyEncryption) { + private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message, boolean legacyEncryption) { Conversation conversation = (Conversation) message.getConversation(); Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); final boolean isWithSelf = conversation.getContact().isSelf(); if (conversation.getMode() == Conversation.MODE_SINGLE) { packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); if (!isWithSelf) { packet.addChild("request", "urn:xmpp:receipts"); } } else if (message.isPrivateMessage()) { packet.setTo(message.getCounterpart()); - packet.setType(MessagePacket.TYPE_CHAT); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.addChild("x", "http://jabber.org/protocol/muc#user"); packet.addChild("request", "urn:xmpp:receipts"); } else { packet.setTo(message.getCounterpart().asBareJid()); - packet.setType(MessagePacket.TYPE_GROUPCHAT); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT); } if (conversation.isSingleOrPrivateAndNonAnonymous() && !message.isPrivateMessage()) { packet.addChild("markable", "urn:xmpp:chat-markers:0"); @@ -78,7 +77,7 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public void addDelay(MessagePacket packet, long timestamp) { + public void addDelay(im.conversations.android.xmpp.model.stanza.Message packet, long timestamp) { final SimpleDateFormat mDateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); @@ -87,8 +86,8 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = preparePacket(message, true); + public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { + im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true); if (axolotlMessage == null) { return null; } @@ -101,17 +100,18 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); + public im.conversations.android.xmpp.model.stanza.Message generateKeyTransportMessage(Jid to, XmppAxolotlMessage axolotlMessage) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(to); packet.setAxolotlMessage(axolotlMessage.toElement()); packet.addChild("store", "urn:xmpp:hints"); return packet; } - public MessagePacket generateChat(Message message) { - MessagePacket packet = preparePacket(message, false); + public im.conversations.android.xmpp.model.stanza.Message generateChat(Message message) { + im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, false); + String content; if (message.hasFileOnRemoteHost()) { final Message.FileParams fileParams = message.getFileParams(); @@ -139,8 +139,8 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generatePgpChat(Message message) { - MessagePacket packet = preparePacket(message, true); + public im.conversations.android.xmpp.model.stanza.Message generatePgpChat(Message message) { + final im.conversations.android.xmpp.model.stanza.Message packet = preparePacket(message, true); if (message.hasFileOnRemoteHost()) { Message.FileParams fileParams = message.getFileParams(); final String url = fileParams.url; @@ -163,10 +163,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generateChatState(Conversation conversation) { + public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) { final Account account = conversation.getAccount(); - MessagePacket packet = new MessagePacket(); - packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(conversation.getMode() == Conversation.MODE_MULTI ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(conversation.getJid().asBareJid()); packet.setFrom(account.getJid()); packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); @@ -175,11 +175,11 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket confirm(final Message message) { + public im.conversations.android.xmpp.model.stanza.Message confirm(final Message message) { final boolean groupChat = message.getConversation().getMode() == Conversational.MODE_MULTI; final Jid to = message.getCounterpart(); - final MessagePacket packet = new MessagePacket(); - packet.setType(groupChat ? MessagePacket.TYPE_GROUPCHAT : MessagePacket.TYPE_CHAT); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(groupChat ? im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT : im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(groupChat ? to.asBareJid() : to); final Element displayed = packet.addChild("displayed", "urn:xmpp:chat-markers:0"); if (groupChat) { @@ -197,20 +197,20 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket conferenceSubject(Conversation conversation, String subject) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_GROUPCHAT); + public im.conversations.android.xmpp.model.stanza.Message conferenceSubject(Conversation conversation, String subject) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT); packet.setTo(conversation.getJid().asBareJid()); packet.addChild("subject").setContent(subject); packet.setFrom(conversation.getAccount().getJid().asBareJid()); return packet; } - public MessagePacket requestVoice(Jid jid) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); + public im.conversations.android.xmpp.model.stanza.Message requestVoice(Jid jid) { + final var packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL); packet.setTo(jid.asBareJid()); - Data form = new Data(); + final var form = new Data(); form.setFormType("http://jabber.org/protocol/muc#request"); form.put("muc#role", "participant"); form.submit(); @@ -218,9 +218,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket directInvite(final Conversation conversation, final Jid contact) { - MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_NORMAL); + public im.conversations.android.xmpp.model.stanza.Message directInvite(final Conversation conversation, final Jid contact) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.NORMAL); packet.setTo(contact); packet.setFrom(conversation.getAccount().getJid()); Element x = packet.addChild("x", "jabber:x:conference"); @@ -236,8 +236,8 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket invite(final Conversation conversation, final Jid contact) { - final MessagePacket packet = new MessagePacket(); + public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) { + final var packet = new im.conversations.android.xmpp.model.stanza.Message(); packet.setTo(conversation.getJid().asBareJid()); packet.setFrom(conversation.getAccount().getJid()); Element x = new Element("x"); @@ -249,8 +249,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket received(Account account, final Jid from, final String id, ArrayList namespaces, int type) { - final MessagePacket receivedPacket = new MessagePacket(); + 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 var receivedPacket = + new im.conversations.android.xmpp.model.stanza.Message(); receivedPacket.setType(type); receivedPacket.setTo(from); receivedPacket.setFrom(account.getJid()); @@ -261,8 +262,8 @@ public class MessageGenerator extends AbstractGenerator { return receivedPacket; } - public MessagePacket received(Account account, Jid to, String id) { - MessagePacket packet = new MessagePacket(); + public im.conversations.android.xmpp.model.stanza.Message received(Account account, Jid to, String id) { + im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); packet.setFrom(account.getJid()); packet.setTo(to); packet.addChild("received", "urn:xmpp:receipts").setAttribute("id", id); @@ -270,10 +271,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionFinish( + public im.conversations.android.xmpp.model.stanza.Message sessionFinish( final Jid with, final String sessionId, final Reason reason) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); packet.setTo(with); final Element finish = packet.addChild("finish", Namespace.JINGLE_MESSAGE); finish.setAttribute("id", sessionId); @@ -283,9 +284,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + public im.conversations.android.xmpp.model.stanza.Message sessionProposal(final JingleConnectionManager.RtpSessionProposal proposal) { + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those packet.setTo(proposal.with); packet.setId(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + proposal.sessionId); final Element propose = packet.addChild("propose", Namespace.JINGLE_MESSAGE); @@ -298,9 +299,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + public im.conversations.android.xmpp.model.stanza.Message sessionRetract(final JingleConnectionManager.RtpSessionProposal proposal) { + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those packet.setTo(proposal.with); final Element propose = packet.addChild("retract", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", proposal.sessionId); @@ -309,9 +310,9 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket sessionReject(final Jid with, final String sessionId) { - final MessagePacket packet = new MessagePacket(); - packet.setType(MessagePacket.TYPE_CHAT); //we want to carbon copy those + public im.conversations.android.xmpp.model.stanza.Message sessionReject(final Jid with, final String sessionId) { + final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + packet.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); //we want to carbon copy those packet.setTo(with); final Element propose = packet.addChild("reject", Namespace.JINGLE_MESSAGE); propose.setAttribute("id", sessionId); diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 4d2d38ce54d510df73f0cf9fb1a2afe198a8c073..ca166be4bc6f6338a6fb1659f0532b32ea169dd7 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -9,7 +9,6 @@ import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceGenerator extends AbstractGenerator { @@ -17,20 +16,20 @@ public class PresenceGenerator extends AbstractGenerator { super(service); } - private PresencePacket subscription(String type, Contact contact) { - PresencePacket packet = new PresencePacket(); + private im.conversations.android.xmpp.model.stanza.Presence subscription(String type, Contact contact) { + im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); packet.setAttribute("type", type); packet.setTo(contact.getJid()); packet.setFrom(contact.getAccount().getJid().asBareJid()); return packet; } - public PresencePacket requestPresenceUpdatesFrom(final Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) { return requestPresenceUpdatesFrom(contact, null); } - public PresencePacket requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { - PresencePacket packet = subscription("subscribe", contact); + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { + im.conversations.android.xmpp.model.stanza.Presence packet = subscription("subscribe", contact); String displayName = contact.getAccount().getDisplayName(); if (!TextUtils.isEmpty(displayName)) { packet.addChild("nick", Namespace.NICK).setContent(displayName); @@ -41,24 +40,24 @@ public class PresenceGenerator extends AbstractGenerator { return packet; } - public PresencePacket stopPresenceUpdatesFrom(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(Contact contact) { return subscription("unsubscribe", contact); } - public PresencePacket stopPresenceUpdatesTo(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(Contact contact) { return subscription("unsubscribed", contact); } - public PresencePacket sendPresenceUpdatesTo(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(Contact contact) { return subscription("subscribed", contact); } - public PresencePacket selfPresence(Account account, Presence.Status status) { + public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Status status) { return selfPresence(account, status, true, null); } - public PresencePacket selfPresence(final Account account, final Presence.Status status, final boolean personal, final String nickname) { - final PresencePacket packet = new PresencePacket(); + public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Status status, final boolean personal, final String nickname) { + final im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); if (personal) { final String sig = account.getPgpSignature(); final String message = account.getPresenceStatusMessage(); @@ -87,16 +86,16 @@ public class PresenceGenerator extends AbstractGenerator { return packet; } - public PresencePacket leave(final MucOptions mucOptions) { - PresencePacket presencePacket = new PresencePacket(); - presencePacket.setTo(mucOptions.getSelf().getFullJid()); - presencePacket.setFrom(mucOptions.getAccount().getJid()); - presencePacket.setAttribute("type", "unavailable"); - return presencePacket; + public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) { + im.conversations.android.xmpp.model.stanza.Presence presence = new im.conversations.android.xmpp.model.stanza.Presence(); + presence.setTo(mucOptions.getSelf().getFullJid()); + presence.setFrom(mucOptions.getAccount().getJid()); + presence.setAttribute("type", "unavailable"); + return presence; } - public PresencePacket sendOfflinePresence(Account account) { - PresencePacket packet = new PresencePacket(); + public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(Account account) { + im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); packet.setFrom(account.getJid()); packet.setAttribute("type", "unavailable"); return packet; diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 0fe254b82d2ee088f26dc7aae593ae598340f7ce..5f60adb8d89fd175b3b978bde97b7d58a1b587cf 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -2,11 +2,27 @@ package eu.siacs.conversations.http; import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; +import android.content.Context; import android.os.Build; import android.util.Log; import androidx.core.util.Consumer; +import eu.siacs.conversations.BuildConfig; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.TrustManagers; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.TLSSocketFactory; + +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; + import org.apache.http.conn.ssl.StrictHostnameVerifier; import java.io.IOException; @@ -16,7 +32,9 @@ import java.net.InetSocketAddress; import java.net.Proxy; import java.net.UnknownHostException; import java.security.KeyManagementException; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; @@ -26,19 +44,6 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.BuildConfig; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.AbstractConnectionManager; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.TLSSocketFactory; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.ResponseBody; - public class HttpConnectionManager extends AbstractConnectionManager { private final List downloadConnections = new ArrayList<>(); @@ -46,7 +51,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { public static final Executor EXECUTOR = Executors.newFixedThreadPool(4); - public static final OkHttpClient OK_HTTP_CLIENT; + private static final OkHttpClient OK_HTTP_CLIENT; static { OK_HTTP_CLIENT = new OkHttpClient.Builder() @@ -209,4 +214,27 @@ public class HttpConnectionManager extends AbstractConnectionManager { return filename; } + + public static OkHttpClient okHttpClient(final Context context) { + final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); + try { + final X509TrustManager trustManager; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context); + } else { + trustManager = TrustManagers.createDefaultTrustManager(); + } + final SSLSocketFactory socketFactory = + new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); + builder.sslSocketFactory(socketFactory, trustManager); + } catch (final IOException + | KeyManagementException + | NoSuchAlgorithmException + | KeyStoreException + | CertificateException e) { + Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt"); + throw new RuntimeException(e); + } + return builder.build(); + } } diff --git a/src/main/java/eu/siacs/conversations/http/SlotRequester.java b/src/main/java/eu/siacs/conversations/http/SlotRequester.java index d0a39ca5d55dc6f9ddbd5aaae2e131312b18c225..b3af132b7e44b148257d8c5d68217bbb24ea2820 100644 --- a/src/main/java/eu/siacs/conversations/http/SlotRequester.java +++ b/src/main/java/eu/siacs/conversations/http/SlotRequester.java @@ -43,7 +43,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.IqResponseException; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -67,9 +67,9 @@ public class SlotRequester { private ListenableFuture requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) { final SettableFuture future = SettableFuture.create(); - final IqPacket request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + final Iq request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime); + service.sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY); if (slotElement != null) { try { @@ -97,9 +97,9 @@ public class SlotRequester { private ListenableFuture requestHttpUpload(Account account, Jid host, DownloadableFile file, String fname, String mime) { final SettableFuture future = SettableFuture.create(); - final IqPacket request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, mime); - service.sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, mime); + service.sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD); if (slotElement != null) { try { diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 02f1b91c1d63137d2619d975f3847c56dbb34384..63fd084f4cb1aafc879941345b76da4c9bb87dd0 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -18,14 +18,16 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import im.conversations.android.xmpp.model.stanza.Stanza; public abstract class AbstractParser { - protected XmppConnectionService mXmppConnectionService; + protected final XmppConnectionService mXmppConnectionService; + protected final Account account; - protected AbstractParser(XmppConnectionService service) { + protected AbstractParser(final XmppConnectionService service, final Account account) { this.mXmppConnectionService = service; + this.account = account; } public static Long parseTimestamp(Element element, Long d) { @@ -36,8 +38,8 @@ public abstract class AbstractParser { long min = Long.MAX_VALUE; boolean returnDefault = true; final Jid to; - if (ignoreCsiAndSm && element instanceof AbstractStanza) { - to = ((AbstractStanza) element).getTo(); + if (ignoreCsiAndSm && element instanceof Stanza stanza) { + to = stanza.getTo(); } else { to = null; } @@ -125,7 +127,7 @@ public abstract class AbstractParser { contact.setLastResource(from.isBareJid() ? "" : from.getResource()); } - protected String avatarData(Element items) { + protected static String avatarData(Element items) { Element item = items.findChild("item"); if (item == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index c401c29d212a458d9a90d8e7aaa86b8a8ab20358..95b239802f50b767fe0c3fb23a71f21ea234095a 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -38,18 +39,17 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; -public class IqParser extends AbstractParser implements OnIqPacketReceived { +public class IqParser extends AbstractParser implements Consumer { - public IqParser(final XmppConnectionService service) { - super(service); + public IqParser(final XmppConnectionService service, final Account account) { + super(service, account); } - public static List items(IqPacket packet) { + public static List items(final Iq packet) { ArrayList items = new ArrayList<>(); final Element query = packet.findChild("query", Namespace.DISCO_ITEMS); if (query == null) { @@ -66,7 +66,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return items; } - public static Room parseRoom(IqPacket packet) { + public static Room parseRoom(Iq packet) { final Element query = packet.findChild("query", Namespace.DISCO_INFO); if (query == null) { return null; @@ -144,7 +144,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.syncRoster(account); } - public String avatarData(final IqPacket packet) { + public static String avatarData(final Iq packet) { final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); if (pubsub == null) { return null; @@ -153,10 +153,10 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { if (items == null) { return null; } - return super.avatarData(items); + return AbstractParser.avatarData(items); } - public Element getItem(final IqPacket packet) { + public static Element getItem(final Iq packet) { final Element pubsub = packet.findChild("pubsub", Namespace.PUBSUB); if (pubsub == null) { return null; @@ -169,7 +169,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } @NonNull - public Set deviceIds(final Element item) { + public static Set deviceIds(final Element item) { Set deviceIds = new HashSet<>(); if (item != null) { final Element list = item.findChild("list"); @@ -190,7 +190,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return deviceIds; } - private Integer signedPreKeyId(final Element bundle) { + private static Integer signedPreKeyId(final Element bundle) { final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); if (signedPreKeyPublic == null) { return null; @@ -202,7 +202,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - private ECPublicKey signedPreKeyPublic(final Element bundle) { + private static ECPublicKey signedPreKeyPublic(final Element bundle) { ECPublicKey publicKey = null; final String signedPreKeyPublic = bundle.findChildContent("signedPreKeyPublic"); if (signedPreKeyPublic == null) { @@ -216,7 +216,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return publicKey; } - private byte[] signedPreKeySignature(final Element bundle) { + private static byte[] signedPreKeySignature(final Element bundle) { final String signedPreKeySignature = bundle.findChildContent("signedPreKeySignature"); if (signedPreKeySignature == null) { return null; @@ -229,7 +229,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - private IdentityKey identityKey(final Element bundle) { + private static IdentityKey identityKey(final Element bundle) { final String identityKey = bundle.findChildContent("identityKey"); if (identityKey == null) { return null; @@ -242,7 +242,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - public Map preKeyPublics(final IqPacket packet) { + public static Map preKeyPublics(final Iq packet) { Map preKeyRecords = new HashMap<>(); Element item = getItem(packet); if (item == null) { @@ -285,7 +285,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(input)); } - public Pair verification(final IqPacket packet) { + public static Pair verification(final Iq packet) { Element item = getItem(packet); Element verification = item != null ? item.findChild("verification", AxolotlService.PEP_PREFIX) : null; Element chain = verification != null ? verification.findChild("chain") : null; @@ -313,7 +313,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } - public PreKeyBundle bundle(final IqPacket bundle) { + public static PreKeyBundle bundle(final Iq bundle) { final Element bundleItem = getItem(bundle); if (bundleItem == null) { return null; @@ -337,7 +337,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); } - public List preKeys(final IqPacket preKeys) { + public static List preKeys(final Iq preKeys) { List bundles = new ArrayList<>(); Map preKeyPublics = preKeyPublics(preKeys); if (preKeyPublics != null) { @@ -352,15 +352,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - final boolean isGet = packet.getType() == IqPacket.TYPE.GET; - if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) { + public void accept(final Iq packet) { + final boolean isGet = packet.getType() == Iq.Type.GET; + if (packet.getType() == Iq.Type.ERROR || packet.getType() == Iq.Type.TIMEOUT) { return; } if (packet.hasChild("query", Namespace.ROSTER) && packet.fromServer(account)) { final Element query = packet.findChild("query"); // If this is in response to a query for the whole roster: - if (packet.getType() == IqPacket.TYPE.RESULT) { + if (packet.getType() == Iq.Type.RESULT) { account.getRoster().markAllAsNotInRoster(); } this.rosterItems(account, query); @@ -374,7 +374,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { (block != null ? block.getChildren() : null); // If this is a response to a blocklist query, clear the block list and replace with the new one. // Otherwise, just update the existing blocklist. - if (packet.getType() == IqPacket.TYPE.RESULT) { + if (packet.getType() == Iq.Type.RESULT) { account.clearBlocklist(); account.getXmppConnection().getFeatures().setBlockListRequested(true); } @@ -390,7 +390,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } } account.getBlocklist().addAll(jids); - if (packet.getType() == IqPacket.TYPE.SET) { + if (packet.getType() == Iq.Type.SET) { boolean removed = false; for (Jid jid : jids) { removed |= mXmppConnectionService.removeBlockedConversations(account, jid); @@ -402,15 +402,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } // Update the UI mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - if (packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + if (packet.getType() == Iq.Type.SET) { + final Iq response = packet.generateResponse(Iq.Type.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } } else if (packet.hasChild("unblock", Namespace.BLOCKING) && - packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) { + packet.fromServer(account) && packet.getType() == Iq.Type.SET) { Log.d(Config.LOGTAG, "Received unblock update from server"); final Collection items = packet.findChild("unblock", Namespace.BLOCKING).getChildren(); - if (items.size() == 0) { + if (items.isEmpty()) { // No children to unblock == unblock all account.getBlocklist().clear(); } else { @@ -426,7 +426,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { account.getBlocklist().removeAll(jids); } mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + final Iq response = packet.generateResponse(Iq.Type.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") || packet.hasChild("data", "http://jabber.org/protocol/ibb") @@ -434,18 +434,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.getJingleConnectionManager() .deliverIbbPacket(account, packet); } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); + final Iq response = mXmppConnectionService.getIqGenerator().discoResponse(account, packet); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("query", "jabber:iq:version") && isGet) { - final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); + final Iq response = mXmppConnectionService.getIqGenerator().versionResponse(packet); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); + final Iq response = packet.generateResponse(Iq.Type.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); } else if (packet.hasChild("time", "urn:xmpp:time") && isGet) { - final IqPacket response; + final Iq response; if (mXmppConnectionService.useTorToConnect() || account.isOnion()) { - response = packet.generateResponse(IqPacket.TYPE.ERROR); + response = packet.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("not-allowed", "urn:ietf:params:xml:ns:xmpp-stanzas"); @@ -453,18 +453,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet); } mXmppConnectionService.sendIqPacket(account, response, null); - } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == IqPacket.TYPE.SET) { + } else if (packet.hasChild("push", Namespace.UNIFIED_PUSH) && packet.getType() == Iq.Type.SET) { final Jid transport = packet.getFrom(); final Element push = packet.findChild("push", Namespace.UNIFIED_PUSH); final boolean success = push != null && mXmppConnectionService.processUnifiedPushMessage( account, transport, push); - final IqPacket response; + final Iq response; if (success) { - response = packet.generateResponse(IqPacket.TYPE.RESULT); + response = packet.generateResponse(Iq.Type.RESULT); } else { - response = packet.generateResponse(IqPacket.TYPE.ERROR); + response = packet.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.setAttribute("code", "404"); @@ -476,8 +476,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { final Conversation conversation = mXmppConnectionService.find(account, packet.getFrom()); if (packet.hasChild("data", "urn:xmpp:bob") && isGet && (conversation == null ? contact != null && contact.canInferPresence() : conversation.canInferPresence())) { mXmppConnectionService.sendIqPacket(account, mXmppConnectionService.getIqGenerator().bobResponse(packet), null); - } else if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) { - final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR); + } else if (packet.getType() == Iq.Type.GET || packet.getType() == Iq.Type.SET) { + final var response = packet.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("feature-not-implemented", "urn:ietf:params:xml:ns:xmpp-stanzas"); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 84a360d31432db078807fa66b315bff124d985d6..b46bd28bc22a5f8b9223292b7c82a712752f2490 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import io.ipfs.cid.Cid; @@ -60,17 +61,20 @@ import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.carbons.Received; +import im.conversations.android.xmpp.model.carbons.Sent; +import im.conversations.android.xmpp.model.forward.Forwarded; -public class MessageParser extends AbstractParser implements OnMessagePacketReceived { +public class MessageParser extends AbstractParser implements Consumer { private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ENGLISH); private static final List JINGLE_MESSAGE_ELEMENT_NAMES = Arrays.asList("accept", "propose", "proceed", "reject", "retract", "ringing", "finish"); - public MessageParser(XmppConnectionService service) { - super(service); + public MessageParser(final XmppConnectionService service, final Account account) { + super(service, account); } private static String extractStanzaId(Element packet, boolean isTypeGroupChat, Conversation conversation) { @@ -109,7 +113,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return result != null ? result : fallback; } - private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final MessagePacket packet) { + private boolean extractChatState(Conversation c, final boolean isTypeGroupChat, final im.conversations.android.xmpp.model.stanza.Message packet) { ChatState state = ChatState.parse(packet); if (state != null && c != null) { final Account account = c.getAccount(); @@ -251,7 +255,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { Element item = items.findChild("item"); - Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(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); @@ -358,10 +362,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece mXmppConnectionService.updateAccountUi(); } - private boolean handleErrorMessage(final Account account, final MessagePacket packet) { - if (packet.getType() == MessagePacket.TYPE_ERROR) { + private boolean handleErrorMessage(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) { + if (packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.ERROR) { if (packet.fromServer(account)) { - final Pair forwarded = packet.getForwardedMessagePacket("received", Namespace.CARBONS); + final var forwarded = getForwardedMessagePacket(packet,"received", Namespace.CARBONS); if (forwarded != null) { return handleErrorMessage(account, forwarded.first); } @@ -404,11 +408,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } @Override - public void onMessagePacketReceived(Account account, MessagePacket original) { + public void accept(final im.conversations.android.xmpp.model.stanza.Message original) { if (handleErrorMessage(account, original)) { return; } - final MessagePacket packet; + final im.conversations.android.xmpp.model.stanza.Message packet; Long timestamp = null; boolean isCarbon = false; String serverMsgId = null; @@ -422,7 +426,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece final MessageArchiveService.Query query = queryId == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(queryId); final boolean offlineMessagesRetrieved = account.getXmppConnection().isOfflineMessagesRetrieved(); if (query != null && query.validFrom(original.getFrom())) { - final Pair f = original.getForwardedMessagePacket("result", query.version.namespace); + final var f = getForwardedMessagePacket(original,"result", query.version.namespace); if (f == null) { return; } @@ -442,9 +446,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received mam result with invalid from (" + original.getFrom() + ") or queryId (" + queryId + ")"); return; } else if (original.fromServer(account)) { - Pair f; - f = original.getForwardedMessagePacket("received", Namespace.CARBONS); - f = f == null ? original.getForwardedMessagePacket("sent", Namespace.CARBONS) : f; + Pair f; + f = getForwardedMessagePacket(original, Received.class); + f = f == null ? getForwardedMessagePacket(original, Sent.class) : f; packet = f != null ? f.first : original; if (handleErrorMessage(account, packet)) { return; @@ -514,7 +518,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece return; } - boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; + boolean isTypeGroupChat = packet.getType() == im.conversations.android.xmpp.model.stanza.Message.Type.GROUPCHAT; if (query != null && !query.muc() && isTypeGroupChat) { Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": received groupchat (" + from + ") message on regular MAM request. skipping"); return; @@ -1297,6 +1301,34 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } + private static Pair getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, Class clazz) { + final var extension = original.getExtension(clazz); + final var forwarded = extension == null ? null : extension.getExtension(Forwarded.class); + if (forwarded == null) { + return null; + } + final Long timestamp = AbstractParser.parseTimestamp(forwarded, null); + final var forwardedMessage = forwarded.getMessage(); + if (forwardedMessage == null) { + return null; + } + return new Pair<>(forwardedMessage,timestamp); + } + + private static Pair getForwardedMessagePacket(final im.conversations.android.xmpp.model.stanza.Message original, final String name, final String namespace) { + final Element wrapper = original.findChild(name, namespace); + final var forwardedElement = wrapper == null ? null : wrapper.findChild("forwarded",Namespace.FORWARD); + if (forwardedElement instanceof Forwarded forwarded) { + final Long timestamp = AbstractParser.parseTimestamp(forwarded, null); + final var forwardedMessage = forwarded.getMessage(); + if (forwardedMessage == null) { + return null; + } + return new Pair<>(forwardedMessage,timestamp); + } + return null; + } + private void dismissNotification(Account account, Jid counterpart, MessageArchiveService.Query query, final String id) { final Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid()); if (conversation != null && (query == null || query.isCatchup())) { @@ -1309,7 +1341,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece } } - private void processMessageReceipts(final Account account, final MessagePacket packet, final String remoteMsgId, MessageArchiveService.Query query) { + private void processMessageReceipts(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"); if (query == null) { @@ -1321,7 +1353,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece receiptsNamespaces.add("urn:xmpp:receipts"); } if (receiptsNamespaces.size() > 0) { - final MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, + final var receipt = mXmppConnectionService.getMessageGenerator().received(account, packet.getFrom(), remoteMsgId, receiptsNamespaces, diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 955cf9d1e1a606f570f4b4922d160378f70ab25b..a254dfe63ef1bde106a22a21b069487283ac37fb 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -19,22 +19,21 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; import org.openintents.openpgp.util.OpenPgpUtils; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; -public class PresenceParser extends AbstractParser implements OnPresencePacketReceived { +public class PresenceParser extends AbstractParser implements Consumer { - public PresenceParser(XmppConnectionService service) { - super(service); + public PresenceParser(final XmppConnectionService service, final Account account) { + super(service, account); } - public void parseConferencePresence(PresencePacket packet, Account account) { + public void parseConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Account account) { final Conversation conversation = packet.getFrom() == null ? null @@ -58,9 +57,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe } } - private void processConferencePresence(PresencePacket packet, Conversation conversation) { - - + private void processConferencePresence(final im.conversations.android.xmpp.model.stanza.Presence packet, Conversation conversation) { final Account account = conversation.getAccount(); final MucOptions mucOptions = conversation.getMucOptions(); final Jid jid = conversation.getAccount().getJid(); @@ -300,7 +297,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe return codes; } - private void parseContactPresence(final PresencePacket packet, final Account account) { + private void parseContactPresence(final im.conversations.android.xmpp.model.stanza.Presence packet, final Account account) { final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); final Jid from = packet.getFrom(); if (from == null || from.equals(account.getJid())) { @@ -434,7 +431,7 @@ public class PresenceParser extends AbstractParser implements OnPresencePacketRe } @Override - public void onPresencePacketReceived(Account account, PresencePacket packet) { + public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { if (packet.hasChild("x", Namespace.MUC_USER)) { this.parseConferencePresence(packet, account); } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index cf525bfa1dd0fdce3b903d94f7606aa78609ac80..f2605bc9a274bc29b018dbf3f67d6f7cb8a1b257 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -53,6 +53,21 @@ import com.madebyevan.thumbhash.ThumbHash; import com.wolt.blurhashkt.BlurHashDecoder; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AttachFileToConversationRunnable; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.adapter.MediaAdapter; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.FileUtils; +import eu.siacs.conversations.utils.FileWriterException; +import eu.siacs.conversations.utils.MimeUtils; +import eu.siacs.conversations.xmpp.pep.Avatar; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -85,22 +100,6 @@ import org.tomlj.TomlTable; import io.ipfs.cid.Cid; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.DownloadableFile; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.AttachFileToConversationRunnable; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.adapter.MediaAdapter; -import eu.siacs.conversations.ui.util.Attachment; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.FileUtils; -import eu.siacs.conversations.utils.FileWriterException; -import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.xmpp.pep.Avatar; -import eu.siacs.conversations.xml.Element; - public class FileBackend { private static final Object THUMBNAIL_LOCK = new Object(); @@ -784,16 +783,16 @@ public class FileBackend { } private void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { - Log.d( - Config.LOGTAG, - "copy file (" + uri.toString() + ") to private storage " + file.getAbsolutePath()); - file.getParentFile().mkdirs(); + final var parentDirectory = file.getParentFile(); + if (parentDirectory != null && parentDirectory.mkdirs()) { + Log.d(Config.LOGTAG,"created directory "+parentDirectory.getAbsolutePath()); + } try { if (!file.createNewFile() && file.length() > 0) { if (file.canRead() && file.getName().startsWith("zb2")) return; // We have this content already throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } - } catch (IOException e) { + } catch (final IOException e) { throw new FileCopyException(R.string.error_unable_to_create_temporary_file); } try (final OutputStream os = new FileOutputStream(file); @@ -803,12 +802,12 @@ public class FileBackend { } try { ByteStreams.copy(is, os); - } catch (IOException e) { + } catch (final IOException e) { throw new FileWriterException(file); } try { os.flush(); - } catch (IOException e) { + } catch (final IOException e) { throw new FileWriterException(file); } } catch (final FileNotFoundException e) { @@ -817,7 +816,7 @@ public class FileBackend { } catch (final FileWriterException e) { cleanup(file); throw new FileCopyException(R.string.error_unable_to_create_temporary_file); - } catch (final SecurityException | IllegalStateException e) { + } catch (final SecurityException | IllegalStateException | IllegalArgumentException e) { cleanup(file); throw new FileCopyException(R.string.error_security_exception); } catch (final IOException e) { @@ -828,7 +827,7 @@ public class FileBackend { public void copyFileToPrivateStorage(Message message, Uri uri, String type) throws FileCopyException { - String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); + final String mime = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime=" + mime + ")"); String extension = MimeUtils.guessExtensionFromMimeType(mime); if (extension == null) { @@ -1168,9 +1167,9 @@ public class FileBackend { } public BitmapDrawable getFallbackThumbnail(final Message message, int size, boolean cacheOnly) { - List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; + final var thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; if (thumbs != null && !thumbs.isEmpty()) { - for (Element thumb : thumbs) { + for (final var thumb : thumbs) { final var uriS = thumb.getAttribute("uri"); if (uriS == null) continue; Uri uri = Uri.parse(uriS); @@ -1240,9 +1239,9 @@ public class FileBackend { if ((thumbnail == null) && (!cacheOnly)) { synchronized (THUMBNAIL_LOCK) { - List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; + final var thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; if (thumbs != null && !thumbs.isEmpty()) { - for (Element thumb : thumbs) { + for (final var thumb : thumbs) { final var uriS = thumb.getAttribute("uri"); if (uriS == null) continue; Uri uri = Uri.parse(uriS); diff --git a/src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java b/src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java deleted file mode 100644 index 71ec74f53bf121ab981a37d8d4ae07a14245c19d..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/receiver/WorkManagerEventReceiver.java +++ /dev/null @@ -1,32 +0,0 @@ -package eu.siacs.conversations.receiver; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -import androidx.work.WorkManager; - -import com.google.common.base.Strings; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.ui.fragment.settings.BackupSettingsFragment; - -public class WorkManagerEventReceiver extends BroadcastReceiver { - - public static final String ACTION_STOP_BACKUP = "eu.siacs.conversations.receiver.STOP_BACKUP"; - - @Override - public void onReceive(final Context context, final Intent intent) { - final var action = Strings.nullToEmpty(intent == null ? null : intent.getAction()); - if (action.equals(ACTION_STOP_BACKUP)) { - stopBackup(context); - } - } - - private void stopBackup(final Context context) { - Log.d(Config.LOGTAG, "trying to stop one-off backup worker"); - final var workManager = WorkManager.getInstance(context); - workManager.cancelUniqueWork(BackupSettingsFragment.CREATE_ONE_OFF_BACKUP); - } -} diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 404489149591adae484af16bc5d786dde13f70bb..5b23d1533fa8440bebc2a332f4f3ad779d6ab796 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -42,8 +42,12 @@ public class CallIntegration extends Connection { * *

Samsung Galaxy Tab A claims to have FEATURE_CONNECTION_SERVICE but then throws * SecurityException when invoking placeCall(). Both Stock and LineageOS have this problem. + * + *

Lenovo Yoga Smart Tab YT-X705F claims to have FEATURE_CONNECTION_SERVICE but throws + * SecurityException */ - private static final List BROKEN_DEVICE_MODELS = Arrays.asList("OnePlus6", "gtaxlwifi"); + private static final List BROKEN_DEVICE_MODELS = + Arrays.asList("OnePlus6", "gtaxlwifi", "YT-X705F"); public static final int DEFAULT_TONE_VOLUME = 60; private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90; @@ -393,9 +397,7 @@ public class CallIntegration extends Connection { public void success() { Log.d(Config.LOGTAG, "CallIntegration.success()"); - final var toneGenerator = - new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME); - toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375); } @@ -410,9 +412,7 @@ public class CallIntegration extends Connection { public void error() { Log.d(Config.LOGTAG, "CallIntegration.error()"); - final var toneGenerator = - new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME); - toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); } @@ -429,8 +429,7 @@ public class CallIntegration extends Connection { public void busy() { Log.d(Config.LOGTAG, "CallIntegration.busy()"); - final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80); - toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); + startTone(80, ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500); this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500); } @@ -458,6 +457,17 @@ public class CallIntegration extends Connection { Log.d(Config.LOGTAG, "destroyed!"); } + private void startTone(final int volume, final int toneType, final int durationMs) { + final ToneGenerator toneGenerator; + try { + toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, volume); + } catch (final RuntimeException e) { + Log.e(Config.LOGTAG, "could not initialize tone generator", e); + return; + } + toneGenerator.startTone(toneType, durationMs); + } + public static Uri address(final Jid contact) { return Uri.parse(String.format("xmpp:%s", contact.toEscapedString())); } @@ -532,6 +542,12 @@ public class CallIntegration extends Connection { && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { return false; } + // we are relatively sure that old Oppo devices are broken too. We get reports of 'number + // not sent' from Oppo R15x (Android 10) + if ("OPPO".equalsIgnoreCase(Build.MANUFACTURER) + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + return false; + } // we only know of one Umidigi device (BISON_GT2_5G) that doesn't work (audio is not being // routed properly) However with those devices being extremely rare it's impossible to gauge // how many might be effected and no Naomi Wu around to clarify with the company directly diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java index 49169b7d29951b2287e003ca46eb17b54503cc54..623653fbc21387e3e9bc7dcb49abcca952b0cb36 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegrationConnectionService.java @@ -36,6 +36,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; +import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @@ -126,9 +127,17 @@ public class CallIntegrationConnectionService extends ConnectionService { // actually attempted // sendJingleFinishMessage(service, contact, Reason.CONNECTIVITY_ERROR); } else { - final var proposal = - service.getJingleConnectionManager() - .proposeJingleRtpSession(account, with, media); + final JingleConnectionManager.RtpSessionProposal proposal; + try { + proposal = + service.getJingleConnectionManager() + .proposeJingleRtpSession(account, with, media); + } catch (final IllegalStateException e) { + return Connection.createFailedConnection( + new DisconnectCause( + DisconnectCause.ERROR, + "Phone is busy. Probably race condition. Try again in a moment")); + } if (proposal == null) { // TODO instead of just null checking try to get the sessionID return Connection.createFailedConnection( diff --git a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java index 0a027da2d2ef96ce44de45a9ad42155e7ad268fe..e2a828ac63209592dabcce2b9c85c0096bc4e6c5 100644 --- a/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java +++ b/src/main/java/eu/siacs/conversations/services/ChannelDiscoveryService.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.services; -import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; -import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; @@ -12,17 +10,15 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.TrustManagers; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.http.services.MuclumbusService; import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.utils.TLSSocketFactory; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.XmppConnection; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; import okhttp3.OkHttpClient; import okhttp3.ResponseBody; @@ -34,10 +30,6 @@ import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -47,9 +39,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - public class ChannelDiscoveryService { private final XmppConnectionService service; @@ -68,25 +57,7 @@ public class ChannelDiscoveryService { this.muclumbusService = null; return; } - final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); - try { - final X509TrustManager trustManager; - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - trustManager = TrustManagers.defaultWithBundledLetsEncrypt(service); - } else { - trustManager = TrustManagers.createDefaultTrustManager(); - } - final SSLSocketFactory socketFactory = - new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); - builder.sslSocketFactory(socketFactory, trustManager); - } catch (final IOException - | KeyManagementException - | NoSuchAlgorithmException - | KeyStoreException - | CertificateException e) { - Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt"); - throw new RuntimeException(e); - } + final OkHttpClient.Builder builder = HttpConnectionManager.okHttpClient(service).newBuilder(); if (service.useTorToConnect()) { builder.proxy(HttpConnectionManager.getProxy()); } @@ -203,7 +174,7 @@ public class ChannelDiscoveryService { final String query, Map mucServices, final OnChannelSearchResultsFound listener) { final Map localMucService = mucServices == null ? getLocalMucServices() : mucServices; Log.d(Config.LOGTAG, "checking with " + localMucService.size() + " muc services"); - if (localMucService.size() == 0) { + if (localMucService.isEmpty()) { listener.onChannelSearchResultsFound(Collections.emptyList()); return; } @@ -217,38 +188,37 @@ public class ChannelDiscoveryService { } final AtomicInteger queriesInFlight = new AtomicInteger(); final List rooms = new ArrayList<>(); - for (Map.Entry entry : localMucService.entrySet()) { - IqPacket itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); + for (final Map.Entry entry : localMucService.entrySet()) { + Iq itemsRequest = service.getIqGenerator().queryDiscoItems(entry.getKey()); queriesInFlight.incrementAndGet(); + final var account = entry.getValue(); service.sendIqPacket( - entry.getValue(), + account, itemsRequest, - (account, itemsResponse) -> { - if (itemsResponse.getType() == IqPacket.TYPE.RESULT) { + (itemsResponse) -> { + if (itemsResponse.getType() == Iq.Type.RESULT) { final List items = IqParser.items(itemsResponse); - for (Jid item : items) { + for (final Jid item : items) { if (item.isDomainJid()) continue; // Only looking for MUCs for now, and by spec they have a localpart - IqPacket infoRequest = + final Iq infoRequest = service.getIqGenerator().queryDiscoInfo(item); queriesInFlight.incrementAndGet(); service.sendIqPacket( account, infoRequest, - new OnIqPacketReceived() { - @Override - public void onIqPacketReceived( - Account account, IqPacket infoResponse) { - if (infoResponse.getType() - == IqPacket.TYPE.RESULT) { - final Room room = - IqParser.parseRoom(infoResponse); - if (room != null) { - rooms.add(room); - } + infoResponse -> { + if (infoResponse.getType() + == Iq.Type.RESULT) { + final Room room = + IqParser.parseRoom(infoResponse); + if (room != null) { + rooms.add(room); } if (queriesInFlight.decrementAndGet() <= 0) { finishDiscoSearch(rooms, query, mucServices, listener); } + } else { + queriesInFlight.decrementAndGet(); } }, 20L); } diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 356914230861b8f196404c56fdb6e12b3f80d0d9..c9be19551e3c456bbb403396d0ce3dcec1c9708e 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -41,7 +41,6 @@ import android.util.Log; import android.util.SparseArray; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.util.Consumer; import com.google.common.base.Charsets; import com.google.common.base.Joiner; @@ -86,6 +85,7 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Locale; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 702c54b05e9897b85363ffe364176591e9fcd361..91913aadd37f8a74761895fee9f0329633d711dd 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -23,8 +23,8 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; import eu.siacs.conversations.xmpp.mam.MamReference; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Message; public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { @@ -81,7 +81,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return false; } - public static Element findResult(MessagePacket packet) { + public static Element findResult(Message packet) { for (Version version : values()) { Element result = packet.findChild("result", version.namespace); if (result != null) { @@ -233,17 +233,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { throw new IllegalStateException("Attempted to run MAM query for archived conversation"); } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString()); - final IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); - this.mXmppConnectionService.sendIqPacket(account, packet, (a, p) -> { + final Iq packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + this.mXmppConnectionService.sendIqPacket(account, packet, (p) -> { final Element fin = p.findChild("fin", query.version.namespace); - if (p.getType() == IqPacket.TYPE.TIMEOUT) { + if (p.getType() == Iq.Type.TIMEOUT) { synchronized (this.queries) { this.queries.remove(query); if (query.hasCallback()) { query.callback(false); } } - } else if (p.getType() == IqPacket.TYPE.RESULT && fin != null) { + } else if (p.getType() == Iq.Type.RESULT && fin != null) { final boolean running; synchronized (this.queries) { running = this.queries.contains(query); @@ -253,10 +253,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring MAM iq result because query had been killed"); } - } else if (p.getType() == IqPacket.TYPE.RESULT && query.isLegacy()) { + } else if (p.getType() == Iq.Type.RESULT && query.isLegacy()) { //do nothing } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); + Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": error executing mam: " + p.toString()); try { finalizeQuery(query, true); } catch (final IllegalStateException e) { @@ -303,7 +303,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - boolean inCatchup(Account account) { + public boolean inCatchup(Account account) { synchronized (this.queries) { for (Query query : queries) { if (query.account == account && query.isCatchup() && query.getWith() == null) { diff --git a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java index 4aab05ceeb00ddefc6d366c1dfd3e94ac4e7036b..92ae9d9ec668533d097680e333ac58bb4bc33248 100644 --- a/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java +++ b/src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java @@ -32,8 +32,9 @@ import eu.siacs.conversations.receiver.UnifiedPushDistributor; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Presence; + import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.List; @@ -82,7 +83,7 @@ public class UnifiedPushBroker { } private void sendDirectedPresence(final Account account, Jid to) { - final PresencePacket presence = new PresencePacket(); + final var presence = new Presence(); presence.setTo(to); service.sendPresencePacket(account, presence); } @@ -146,7 +147,7 @@ public class UnifiedPushBroker { UnifiedPushDistributor.hash(account.getUuid(), renewal.application); final String hashedInstance = UnifiedPushDistributor.hash(account.getUuid(), renewal.instance); - final IqPacket registration = new IqPacket(IqPacket.TYPE.SET); + final Iq registration = new Iq(Iq.Type.SET); registration.setTo(transport.transport); final Element register = registration.addChild("register", Namespace.UNIFIED_PUSH); register.setAttribute("application", hashedApplication); @@ -160,7 +161,7 @@ public class UnifiedPushBroker { this.service.sendIqPacket( account, registration, - (a, response) -> processRegistration(transport, renewal, messenger, response)); + (response) -> processRegistration(transport, renewal, messenger, response)); } } @@ -168,8 +169,8 @@ public class UnifiedPushBroker { final Transport transport, final UnifiedPushDatabase.PushTarget renewal, final Messenger messenger, - final IqPacket response) { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq response) { + if (response.getType() == Iq.Type.RESULT) { final Element registered = response.findChild("registered", Namespace.UNIFIED_PUSH); if (registered == null) { return; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f9427c566e59ef11f504f6577fe9a21658334dda..bcfd0e315fdbf9ec57c8188f7a6892f240543341 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -56,7 +56,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; -import androidx.core.util.Consumer; import com.cheogram.android.EmojiSearch; import com.cheogram.android.WebxdcUpdate; @@ -111,6 +110,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import io.ipfs.cid.Cid; @@ -145,8 +145,6 @@ import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.parser.IqParser; -import eu.siacs.conversations.parser.MessageParser; -import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.persistance.UnifiedPushDatabase; @@ -186,11 +184,8 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnGatewayResult; -import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; -import eu.siacs.conversations.xmpp.OnMessagePacketReceived; -import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; @@ -204,9 +199,7 @@ 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 eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import im.conversations.android.xmpp.model.stanza.Iq; import me.leolin.shortcutbadger.ShortcutBadger; import okhttp3.HttpUrl; @@ -255,12 +248,12 @@ public class XmppConnectionService extends Service { private final Set mInProgressAvatarFetches = new HashSet<>(); private final Set mOmittedPepAvatarFetches = new HashSet<>(); private final HashSet mLowPingTimeoutMode = new HashSet<>(); - private final OnIqPacketReceived mDefaultIqHandler = (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { - Element error = packet.findChild("error"); + private final Consumer mDefaultIqHandler = (packet) -> { + if (packet.getType() != Iq.Type.RESULT) { + final var error = packet.getError(); String text = error != null ? error.findChildContent("text") : null; if (text != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received iq error - " + text); + Log.d(Config.LOGTAG, "received iq error: " + text); } } }; @@ -272,6 +265,7 @@ public class XmppConnectionService extends Service { private long mLastMucPing = 0; private Map mScheduledMessages = new HashMap<>(); private long mLastStickerRescan = 0; + private final AppSettings appSettings = new AppSettings(this); private final FileBackend fileBackend = new FileBackend(this); private MemorizingTrustManager mMemorizingTrustManager; private final NotificationService mNotificationService = new NotificationService(this); @@ -282,9 +276,6 @@ public class XmppConnectionService extends Service { private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false); private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); private final AtomicReference ongoingCall = new AtomicReference<>(); - private final OnMessagePacketReceived mMessageParser = new MessageParser(this); - private final OnPresencePacketReceived mPresenceParser = new PresenceParser(this); - private final IqParser mIqParser = new IqParser(this); private final MessageGenerator mMessageGenerator = new MessageGenerator(this); public OnContactStatusChanged onContactStatusChanged = (contact, online) -> { Conversation conversation = find(getConversations(), contact); @@ -371,79 +362,6 @@ public class XmppConnectionService extends Service { public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); - private final OnBindListener mOnBindListener = new OnBindListener() { - - @Override - public void onBind(final Account account) { - synchronized (mInProgressAvatarFetches) { - for (Iterator iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) { - final String KEY = iterator.next(); - if (KEY.startsWith(account.getJid().asBareJid() + "_")) { - iterator.remove(); - } - } - } - boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true); - boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0)); - if (loggedInSuccessfully || gainedFeature) { - databaseBackend.updateAccount(account); - } - - if (loggedInSuccessfully) { - if (!TextUtils.isEmpty(account.getDisplayName())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing"); - publishDisplayName(account); - } - } - - account.getRoster().clearPresences(); - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.clear(); - } - synchronized (account.inProgressConferencePings) { - account.inProgressConferencePings.clear(); - } - mJingleConnectionManager.notifyRebound(account); - mQuickConversationsService.considerSyncBackground(false); - fetchRosterFromServer(account); - - final XmppConnection connection = account.getXmppConnection(); - - if (connection.getFeatures().bookmarks2()) { - fetchBookmarks2(account); - } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) { - fetchBookmarks(account); - } - - if (connection.getFeatures().mds()) { - fetchMessageDisplayedSynchronization(account); - } else { - Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds"); - } - final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval(); - final boolean catchup = getMessageArchiveService().inCatchup(account); - final boolean trackOfflineMessageRetrieval; - if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) { - trackOfflineMessageRetrieval = false; - sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages"); - } - }); - } else { - trackOfflineMessageRetrieval = true; - } - sendPresence(account); - account.getXmppConnection().trackOfflineMessageRetrieval(trackOfflineMessageRetrieval); - if (mPushManagementService.available(account)) { - mPushManagementService.registerPushTokenOnServer(account); - } - connectMultiModeConversations(account); - syncDirtyContacts(account); - - unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account); - } - }; private final AtomicLong mLastExpiryRun = new AtomicLong(0); private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); @@ -609,6 +527,10 @@ public class XmppConnectionService extends Service { } } + public AppSettings getAppSettings() { + return this.appSettings; + } + public FileBackend getFileBackend() { return this.fileBackend; } @@ -1512,13 +1434,11 @@ public class XmppConnectionService extends Service { toggleForegroundService(); this.destroyed = false; OmemoSetting.load(this); - ExceptionHelper.init(getApplicationContext()); try { Security.insertProviderAt(Conscrypt.newProvider(), 1); } catch (Throwable throwable) { Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); } - Resolver.init(this); updateMemorizingTrustManager(); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int cacheSize = maxMemory / 15; @@ -1910,12 +1830,8 @@ public class XmppConnectionService extends Service { public XmppConnection createConnection(final Account account) { final XmppConnection connection = new XmppConnection(account, this); - connection.setOnMessagePacketReceivedListener(this.mMessageParser); connection.setOnStatusChangedListener(this.statusListener); - connection.setOnPresencePacketReceivedListener(this.mPresenceParser); - connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket)); - connection.setOnBindListener(this.mOnBindListener); connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService); @@ -1928,7 +1844,7 @@ public class XmppConnectionService extends Service { public void sendChatState(Conversation conversation) { if (sendChatStates()) { - MessagePacket packet = mMessageGenerator.generateChatState(conversation); + final var packet = mMessageGenerator.generateChatState(conversation); sendMessagePacket(conversation.getAccount(), packet); } } @@ -1966,7 +1882,7 @@ public class XmppConnectionService extends Service { } } - MessagePacket packet = null; + im.conversations.android.xmpp.model.stanza.Message packet = null; final boolean addToConversation = !message.edited() && message.getRawBody() != null; boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); @@ -2282,13 +2198,13 @@ public class XmppConnectionService extends Service { callback.inviteRequestFailed(getString(R.string.server_does_not_support_easy_onboarding_invites)); return; } - final IqPacket request = new IqPacket(IqPacket.TYPE.SET); + final Iq request = new Iq(Iq.Type.SET); request.setTo(jid); final Element command = request.addChild("command", Namespace.COMMANDS); command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE); command.setAttribute("action", "execute"); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, request, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element resultCommand = response.findChild("command", Namespace.COMMANDS); final Element x = resultCommand == null ? null : resultCommand.findChild("x", Namespace.DATA); if (x != null) { @@ -2303,7 +2219,7 @@ public class XmppConnectionService extends Service { } callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite)); Log.d(Config.LOGTAG, response.toString()); - } else if (response.getType() == IqPacket.TYPE.ERROR) { + } else if (response.getType() == Iq.Type.ERROR) { callback.inviteRequestFailed(IqParser.errorMessage(response)); } else { callback.inviteRequestFailed(getString(R.string.remote_server_timeout)); @@ -2312,54 +2228,42 @@ public class XmppConnectionService extends Service { } - public void fetchRosterFromServer(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); - if (!"".equals(account.getRosterVersion())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": fetching roster version " + account.getRosterVersion()); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster"); - } - iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion()); - sendIqPacket(account, iqPacket, mIqParser); - } - public void fetchBookmarks(final Account account) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); + final Iq iqPacket = new Iq(Iq.Type.GET); final Element query = iqPacket.query("jabber:iq:private"); query.addChild("storage", Namespace.BOOKMARKS); - final OnIqPacketReceived callback = (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + 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); - processBookmarksInitial(a, bookmarks, false); + processBookmarksInitial(account, bookmarks, false); } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fetch bookmarks"); } }; sendIqPacket(account, iqPacket, callback); } public void fetchBookmarks2(final Account account) { - final IqPacket retrieve = mIqGenerator.retrieveBookmarks(); - sendIqPacket(account, retrieve, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq retrieve = mIqGenerator.retrieveBookmarks(); + sendIqPacket(account, retrieve, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); - final Map bookmarks = Bookmark.parseFromPubsub(pubsub, a); - processBookmarksInitial(a, bookmarks, true); + final Map bookmarks = Bookmark.parseFromPubsub(pubsub, account); + processBookmarksInitial(account, bookmarks, true); } }); } - private void fetchMessageDisplayedSynchronization(final Account account) { + public void fetchMessageDisplayedSynchronization(final Account account) { Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds"); final var retrieve = mIqGenerator.retrieveMds(); sendIqPacket( account, retrieve, - (a, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() != Iq.Type.RESULT) { return; } final var pubSub = response.findChild("pubsub", Namespace.PUBSUB); @@ -2517,11 +2421,11 @@ public class XmppConnectionService extends Service { if (connection == null) return; if (connection.getFeatures().bookmarks2()) { - final IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); + final Iq request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString()); Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2"); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition()); + sendIqPacket(account, request, (response) -> { + if (response.getType() == Iq.Type.ERROR) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition()); } }); } else if (connection.getFeatures().bookmarksConversion()) { @@ -2535,7 +2439,7 @@ public class XmppConnectionService extends Service { if (!account.areBookmarksLoaded()) return; Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml"); - IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); + final Iq iqPacket = new Iq(Iq.Type.SET); Element query = iqPacket.query("jabber:iq:private"); Element storage = query.addChild("storage", "storage:bookmarks"); for (final Bookmark bookmark : account.getBookmarks()) { @@ -2562,9 +2466,9 @@ public class XmppConnectionService extends Service { } private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options, final boolean retry) { - final IqPacket packet = mIqGenerator.publishElement(node, element, id, options); - sendIqPacket(account, packet, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq packet = mIqGenerator.publishElement(node, element, id, options); + sendIqPacket(account, packet, (response) -> { + if (response.getType() == Iq.Type.RESULT) { return; } if (retry && PublishOptions.preconditionNotMet(response)) { @@ -2879,11 +2783,11 @@ public class XmppConnectionService extends Service { public void maybeRegisterWithMuc(Conversation c, String nickArg) { final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg; - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + final var register = new Iq(Iq.Type.GET); register.query(Namespace.REGISTER); register.setTo(c.getJid().asBareJid()); - sendIqPacket(c.getAccount(), register, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(c.getAccount(), register, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element query = response.query(Namespace.REGISTER); String username = query.findChildContent("username", Namespace.REGISTER); if (username == null) username = query.findChildContent("nick", Namespace.REGISTER); @@ -2908,11 +2812,11 @@ public class XmppConnectionService extends Service { } form.put("muc#register_roomnick", nick); form.submit(); - final IqPacket finish = new IqPacket(IqPacket.TYPE.SET); + final var finish = new Iq(Iq.Type.SET); finish.query(Namespace.REGISTER).addChild(form); finish.setTo(c.getJid().asBareJid()); - sendIqPacket(c.getAccount(), finish, (a2, response2) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(c.getAccount(), finish, (response2) -> { + if (response.getType() == Iq.Type.RESULT) { Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick); } else { Log.w(Config.LOGTAG, "Error registering with channel: " + response2); @@ -2930,11 +2834,11 @@ public class XmppConnectionService extends Service { } public void deregisterWithMuc(Conversation c) { - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + final Iq register = new Iq(Iq.Type.GET); register.query(Namespace.REGISTER).addChild("remove"); register.setTo(c.getJid().asBareJid()); - sendIqPacket(c.getAccount(), register, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(c.getAccount(), register, (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid()); } else { Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response); @@ -3114,6 +3018,10 @@ public class XmppConnectionService extends Service { return this.unifiedPushBroker.renewUnifiedPushEndpoints(null); } + public UnifiedPushBroker getUnifiedPushBroker() { + return this.unifiedPushBroker; + } + private void provisionAccount(final String address, final String password) { final Jid jid = Jid.ofEscaped(address); final Account account = new Account(jid, password); @@ -3218,12 +3126,12 @@ public class XmppConnectionService extends Service { } public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { - final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); - sendIqPacket(account, iq, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - a.setPassword(newPassword); - a.setOption(Account.OPTION_MAGIC_CREATE, false); - databaseBackend.updateAccount(a); + final Iq iq = getIqGenerator().generateSetPassword(account, newPassword); + sendIqPacket(account, iq, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { + account.setPassword(newPassword); + account.setOption(Account.OPTION_MAGIC_CREATE, false); + databaseBackend.updateAccount(account); callback.onPasswordChangeSucceeded(); } else { callback.onPasswordChangeFailed(); @@ -3232,12 +3140,12 @@ public class XmppConnectionService extends Service { } public void unregisterAccount(final Account account, final Consumer callback) { - final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET); + final Iq iqPacket = new Iq(Iq.Type.SET); final Element query = iqPacket.addChild("query",Namespace.REGISTER); query.addChild("remove"); - sendIqPacket(account, iqPacket, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - deleteAccount(a); + sendIqPacket(account, iqPacket, (response) -> { + if (response.getType() == Iq.Type.RESULT) { + deleteAccount(account); callback.accept(true); } else { callback.accept(false); @@ -3571,7 +3479,7 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "app switched into background"); } - private void connectMultiModeConversations(Account account) { + public void connectMultiModeConversations(Account account) { List conversations = getConversations(); for (Conversation conversation : conversations) { if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { @@ -3595,20 +3503,20 @@ public class XmppConnectionService extends Service { } } final Jid self = conversation.getMucOptions().getSelf().getFullJid(); - final IqPacket ping = new IqPacket(IqPacket.TYPE.GET); + final Iq ping = new Iq(Iq.Type.GET); ping.setTo(self); ping.addChild("ping", Namespace.PING); - sendIqPacket(conversation.getAccount(), ping, (a, response) -> { - if (response.getType() == IqPacket.TYPE.ERROR) { - Element error = response.findChild("error"); + sendIqPacket(conversation.getAccount(), ping, (response) -> { + if (response.getType() == Iq.Type.ERROR) { + final var error = response.getError(); if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error"); } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin"); joinMuc(conversation); } - } else if (response.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back fine"); + } else if (response.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " came back fine"); } synchronized (account.inProgressConferencePings) { account.inProgressConferencePings.remove(conversation); @@ -3667,7 +3575,7 @@ public class XmppConnectionService extends Service { final Jid joinJid = mucOptions.getSelf().getFullJid(); Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString()); - PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null, mucOptions.getSelf().getNick()); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null, mucOptions.getSelf().getNick()); packet.setTo(joinJid); Element x = packet.addChild("x", "http://jabber.org/protocol/muc"); if (conversation.getMucOptions().getPassword() != null) { @@ -3759,16 +3667,16 @@ public class XmppConnectionService extends Service { final var affiliations = new ArrayList(); affiliations.add("outcast"); if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner")); - OnIqPacketReceived callback = new OnIqPacketReceived() { + final Consumer callback = new Consumer() { private int i = 0; private boolean success = true; @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + public void accept(Iq response) { final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; - Element query = packet.query("http://jabber.org/protocol/muc#admin"); - if (packet.getType() == IqPacket.TYPE.RESULT && query != null) { + Element query = response.query("http://jabber.org/protocol/muc#admin"); + if (response.getType() == Iq.Type.RESULT && query != null) { for (Element child : query.getChildren()) { if ("item".equals(child.getName())) { MucOptions.User user = AbstractParser.parseItem(conversation, child); @@ -3856,29 +3764,29 @@ public class XmppConnectionService extends Service { } private void deletePepNode(final Account account, final String node, final Runnable runnable) { - final IqPacket request = mIqGenerator.deleteNode(node); - sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully deleted pep node "+node); + final Iq request = mIqGenerator.deleteNode(node); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully deleted pep node "+node); if (runnable != null) { runnable.run(); } } else { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": failed to delete "+ packet); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": failed to delete "+ packet); } }); } private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { - final IqPacket retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); - sendIqPacket(account, retrieveVcard, (a, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid()); + sendIqPacket(account, retrieveVcard, (response) -> { + if (response.getType() != Iq.Type.RESULT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": no vCard set. nothing to do"); return; } final Element vcard = response.findChild("vCard", "vcard-temp"); if (vcard == null) { - Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do"); + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": no vCard set. nothing to do"); return; } Element photo = vcard.findChild("PHOTO"); @@ -3886,12 +3794,12 @@ public class XmppConnectionService extends Service { photo = vcard.addChild("PHOTO"); } photo.clearChildren(); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); - publication.setTo(a.getJid().asBareJid()); + final Iq publication = new Iq(Iq.Type.SET); + publication.setTo(account.getJid().asBareJid()); publication.addChild(vcard); - sendIqPacket(account, publication, (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG,a1.getJid().asBareJid()+": successfully deleted vcard avatar"); + sendIqPacket(account, publication, (publicationResponse) -> { + if (publicationResponse.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully deleted vcard avatar"); runnable.run(); } else { Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); @@ -3956,7 +3864,7 @@ public class XmppConnectionService extends Service { if (options.online()) { Account account = conversation.getAccount(); final Jid joinJid = options.getSelf().getFullJid(); - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick()); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick()); packet.setTo(joinJid); sendPresencePacket(account, packet); } @@ -3976,7 +3884,7 @@ public class XmppConnectionService extends Service { @Override public void onSuccess() { - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); packet.setTo(joinJid); sendPresencePacket(account, packet); callback.success(conversation); @@ -3988,7 +3896,7 @@ public class XmppConnectionService extends Service { } }); - final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); packet.setTo(joinJid); sendPresencePacket(account, packet); } else { @@ -4152,9 +4060,9 @@ public class XmppConnectionService extends Service { return; } - IqPacket request = mIqGenerator.queryDiscoInfo(jid.asBareJid()); - sendIqPacket(account, request, (acct, reply) -> { - ServiceDiscoveryResult result = new ServiceDiscoveryResult(reply); + final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid()); + sendIqPacket(account, request, (reply) -> { + final var result = new ServiceDiscoveryResult(reply); cb.accept( result.getFeatures().contains("http://jabber.org/protocol/muc") && result.hasIdentity("conference", null) @@ -4167,39 +4075,37 @@ public class XmppConnectionService extends Service { } public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) { - IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final MucOptions mucOptions = conversation.getMucOptions(); - final Bookmark bookmark = conversation.getBookmark(); - final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); + final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); + final var account = conversation.getAccount(); + sendIqPacket(account, request, response -> { + if (response.getType() == Iq.Type.RESULT) { + final MucOptions mucOptions = conversation.getMucOptions(); + final Bookmark bookmark = conversation.getBookmark(); + final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); - if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid()); - updateConversation(conversation); - } + if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid()); + updateConversation(conversation); + } - if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { - if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { - createBookmark(account, bookmark); - } + if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { + if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) { + createBookmark(account, bookmark); } + } - if (callback != null) { - callback.onConferenceConfigurationFetched(conversation); - } + if (callback != null) { + callback.onConferenceConfigurationFetched(conversation); + } - updateConversationUi(); - } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch"); - } else { - if (callback != null) { - callback.onFetchFailed(conversation, packet.getErrorCondition()); - } + updateConversationUi(); + } else if (response.getType() == Iq.Type.TIMEOUT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch"); + } else { + if (callback != null) { + callback.onFetchFailed(conversation, response.getErrorCondition()); } } }); @@ -4211,33 +4117,27 @@ public class XmppConnectionService extends Service { public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) { Log.d(Config.LOGTAG, "pushing node configuration"); - sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); - Element configuration = pubsub == null ? null : pubsub.findChild("configure"); - Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA); - if (x != null) { - Data data = Data.parse(x); - data.submit(options); - sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node); - callback.onPushSucceeded(); - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { - callback.onPushFailed(); - } - } - }); - } else if (callback != null) { - callback.onPushFailed(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) { + sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), responseToRequest -> { + if (responseToRequest.getType() == Iq.Type.RESULT) { + Element pubsub = responseToRequest.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner"); + Element configuration = pubsub == null ? null : pubsub.findChild("configure"); + Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA); + if (x != null) { + final Data data = Data.parse(x); + data.submit(options); + sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), responseToPublish -> { + if (responseToPublish.getType() == Iq.Type.RESULT && callback != null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node); + callback.onPushSucceeded(); + } else if (responseToPublish.getType() == Iq.Type.ERROR && callback != null) { + callback.onPushFailed(); + } + }); + } else if (callback != null) { callback.onPushFailed(); } + } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) { + callback.onPushFailed(); } }); } @@ -4251,54 +4151,56 @@ public class XmppConnectionService extends Service { final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom")); options.putString("members_by_default", moderated ? "0" : "1"); } - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + if (options.containsKey("muc#roomconfig_allowpm")) { + // ejabberd :-/ + final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm")); + options.putString("allow_private_messages", allow ? "1" : "0"); + options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody"); + } + final var account = conversation.getAccount(); + final Iq request = new Iq(Iq.Type.GET); request.setTo(conversation.getJid().asBareJid()); request.query("http://jabber.org/protocol/muc#owner"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - final Data data = Data.parse(packet.query().findChild("x", Namespace.DATA)); - data.submit(options); - final IqPacket set = new IqPacket(IqPacket.TYPE.SET); - set.setTo(conversation.getJid().asBareJid()); - set.query("http://jabber.org/protocol/muc#owner").addChild(data); - sendIqPacket(account, set, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (callback != null) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - callback.onPushSucceeded(); - } else { - callback.onPushFailed(); - } - } - } - }); - } else { + sendIqPacket(account, request, response -> { + if (response.getType() == Iq.Type.RESULT) { + final Data data = Data.parse(response.query().findChild("x", Namespace.DATA)); + data.submit(options); + final Iq set = new Iq(Iq.Type.SET); + set.setTo(conversation.getJid().asBareJid()); + set.query("http://jabber.org/protocol/muc#owner").addChild(data); + sendIqPacket(account, set, packet -> { if (callback != null) { - callback.onPushFailed(); + if (packet.getType() == Iq.Type.RESULT) { + callback.onPushSucceeded(); + } else { + Log.d(Config.LOGTAG,"failed: "+packet.toString()); + callback.onPushFailed(); + } } + }); + } else { + if (callback != null) { + callback.onPushFailed(); } } }); } public void pushSubjectToConference(final Conversation conference, final String subject) { - MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); + final var packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject)); this.sendMessagePacket(conference.getAccount(), packet); } public void requestVoice(final Account account, final Jid jid) { - MessagePacket packet = this.getMessageGenerator().requestVoice(jid); + final var packet = this.getMessageGenerator().requestVoice(jid); this.sendMessagePacket(account, packet); } public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) { final Jid jid = user.asBareJid(); - final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); - sendIqPacket(conference.getAccount(), request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + final Iq request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString()); + sendIqPacket(conference.getAccount(), request, (response) -> { + if (response.getType() == Iq.Type.RESULT) { conference.getMucOptions().changeAffiliation(jid, affiliation); getAvatarService().clear(conference); if (callback != null) { @@ -4315,39 +4217,37 @@ public class XmppConnectionService extends Service { } public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { - IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString()); - sendIqPacket(conference.getAccount(), request, (account, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { + final var account =conference.getAccount(); + final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString()); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() != Iq.Type.RESULT) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick); } }); } public void moderateMessage(final Account account, final Message m, final String reason) { - IqPacket request = this.mIqGenerator.moderateMessage(account, m, reason); - sendIqPacket(account, request, (a, packet) -> { - if (packet.getType() != IqPacket.TYPE.RESULT) { + final var request = this.mIqGenerator.moderateMessage(account, m, reason); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() != Iq.Type.RESULT) { showErrorToastInUi(R.string.unable_to_moderate); - Log.d(Config.LOGTAG, a.getJid().asBareJid() + " unable to moderate: " + packet); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet); } }); } public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) { - IqPacket request = new IqPacket(IqPacket.TYPE.SET); + final Iq request = new Iq(Iq.Type.SET); request.setTo(conversation.getJid().asBareJid()); request.query("http://jabber.org/protocol/muc#owner").addChild("destroy"); - sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - if (callback != null) { - callback.onRoomDestroySucceeded(); - } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { - if (callback != null) { - callback.onRoomDestroyFailed(); - } + sendIqPacket(conversation.getAccount(), request, response -> { + if (response.getType() == Iq.Type.RESULT) { + if (callback != null) { + callback.onRoomDestroySucceeded(); + } + } else if (response.getType() == Iq.Type.ERROR) { + if (callback != null) { + callback.onRoomDestroyFailed(); } } }); @@ -4404,7 +4304,7 @@ public class XmppConnectionService extends Service { updateConversationUi(); } - protected void syncDirtyContacts(Account account) { + public void syncDirtyContacts(Account account) { for (Contact contact : account.getRoster().getContacts()) { if (contact.getOption(Contact.Options.DIRTY_PUSH)) { pushContactToServer(contact); @@ -4448,7 +4348,7 @@ public class XmppConnectionService extends Service { final boolean sendUpdates = contact .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); iq.query(Namespace.ROSTER).addChild(contact.asElement()); account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); if (sendUpdates) { @@ -4500,10 +4400,11 @@ public class XmppConnectionService extends Service { } private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) { - final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar); - sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> { - boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found"); - if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) { + final var account = conversation.getAccount(); + final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar); + sendIqPacket(account, retrieve, (response) -> { + boolean itemNotFound = response.getType() == Iq.Type.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found"); + if (response.getType() == Iq.Type.RESULT || itemNotFound) { Element vcard = response.findChild("vCard", "vcard-temp"); if (vcard == null) { vcard = new Element("vCard", "vcard-temp"); @@ -4515,11 +4416,11 @@ public class XmppConnectionService extends Service { photo.clearChildren(); photo.addChild("TYPE").setContent(avatar.type); photo.addChild("BINVAL").setContent(avatar.image); - IqPacket publication = new IqPacket(IqPacket.TYPE.SET); + final Iq publication = new Iq(Iq.Type.SET); publication.setTo(conversation.getJid().asBareJid()); publication.addChild(vcard); - sendIqPacket(account, publication, (a1, publicationResponse) -> { - if (publicationResponse.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, publication, (publicationResponse) -> { + if (publicationResponse.getType() == Iq.Type.RESULT) { callback.onAvatarPublicationSucceeded(); } else { Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); @@ -4545,71 +4446,64 @@ public class XmppConnectionService extends Service { public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": publishing avatar. options=" + options); - IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - publishAvatarMetadata(account, avatar, options, true, callback); - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, Namespace.AVATAR_DATA, options, new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node"); - publishAvatar(account, avatar, options, false, callback); - } + final Iq packet = this.mIqGenerator.publishAvatar(avatar, options); + this.sendIqPacket(account, packet, result -> { + if (result.getType() == Iq.Type.RESULT) { + publishAvatarMetadata(account, avatar, options, true, callback); + } else if (retry && PublishOptions.preconditionNotMet(result)) { + pushNodeConfiguration(account, Namespace.AVATAR_DATA, options, new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node"); + publishAvatar(account, avatar, options, false, callback); + } - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar node"); - publishAvatar(account, avatar, null, false, callback); - } - }); - } else { - Element error = result.findChild("error"); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : "")); - if (callback != null) { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); + @Override + public void onPushFailed() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar node"); + publishAvatar(account, avatar, null, false, callback); } + }); + } else { + Element error = result.findChild("error"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : "")); + if (callback != null) { + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); } } }); } public void publishAvatarMetadata(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) { - final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options); - sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket result) { - if (result.getType() == IqPacket.TYPE.RESULT) { - if (account.setAvatar(avatar.getFilename())) { - getAvatarService().clear(account); - databaseBackend.updateAccount(account); - notifyAccountAvatarHasChanged(account); - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB"); - if (callback != null) { - callback.onAvatarPublicationSucceeded(); + final Iq packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options); + sendIqPacket(account, packet, result -> { + if (result.getType() == Iq.Type.RESULT) { + if (account.setAvatar(avatar.getFilename())) { + getAvatarService().clear(account); + databaseBackend.updateAccount(account); + notifyAccountAvatarHasChanged(account); + } + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB"); + if (callback != null) { + callback.onAvatarPublicationSucceeded(); + } + } else if (retry && PublishOptions.preconditionNotMet(result)) { + pushNodeConfiguration(account, Namespace.AVATAR_METADATA, options, new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar meta data node"); + publishAvatarMetadata(account, avatar, options, false, callback); } - } else if (retry && PublishOptions.preconditionNotMet(result)) { - pushNodeConfiguration(account, Namespace.AVATAR_METADATA, options, new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar meta data node"); - publishAvatarMetadata(account, avatar, options, false, callback); - } - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar meta data node"); - publishAvatarMetadata(account, avatar, null, false, callback); - } - }); - } else { - if (callback != null) { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); + @Override + public void onPushFailed() { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar meta data node"); + publishAvatarMetadata(account, avatar, null, false, callback); } + }); + } else { + if (callback != null) { + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); } } }); @@ -4620,10 +4514,10 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken"); return; } - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { + final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(account, packet, new Consumer() { - private Avatar parseAvatar(IqPacket packet) { + private Avatar parseAvatar(Iq packet) { Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); if (pubsub != null) { Element items = pubsub.findChild("items"); @@ -4634,16 +4528,16 @@ public class XmppConnectionService extends Service { return null; } - private boolean errorIsItemNotFound(IqPacket packet) { + private boolean errorIsItemNotFound(Iq packet) { Element error = packet.findChild("error"); - return packet.getType() == IqPacket.TYPE.ERROR + return packet.getType() == Iq.Type.ERROR && error != null && error.hasChild("item-not-found"); } @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) { + public void accept(final Iq packet) { + if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) { Avatar serverAvatar = parseAvatar(packet); if (serverAvatar == null && account.getAvatar() != null) { Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar()); @@ -4659,6 +4553,17 @@ public class XmppConnectionService extends Service { }); } + public void cancelAvatarFetches(final Account account) { + synchronized (mInProgressAvatarFetches) { + for (final Iterator iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) { + final String KEY = iterator.next(); + if (KEY.startsWith(account.getJid().asBareJid() + "_")) { + iterator.remove(); + } + } + } + } + public void fetchAvatar(Account account, Avatar avatar) { fetchAvatar(account, avatar, null); } @@ -4690,26 +4595,26 @@ public class XmppConnectionService extends Service { } } - private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar); - sendIqPacket(account, packet, (a, result) -> { + private void fetchAvatarPep(final Account account, final Avatar avatar, final UiCallback callback) { + final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar); + sendIqPacket(account, packet, (result) -> { synchronized (mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(a, avatar)); + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); } - final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed "; - if (result.getType() == IqPacket.TYPE.RESULT) { - avatar.image = mIqParser.avatarData(result); + final String ERROR = account.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed "; + if (result.getType() == Iq.Type.RESULT) { + avatar.image = IqParser.avatarData(result); if (avatar.image != null) { if (getFileBackend().save(avatar)) { - if (a.getJid().asBareJid().equals(avatar.owner)) { - if (a.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(a); + if (account.getJid().asBareJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); } - getAvatarService().clear(a); + getAvatarService().clear(account); updateConversationUi(); updateAccountUi(); } else { - final Contact contact = a.getRoster().getContact(avatar.owner); + final Contact contact = account.getRoster().getContact(avatar.owner); contact.setAvatar(avatar); syncRoster(account); getAvatarService().clear(contact); @@ -4719,7 +4624,7 @@ public class XmppConnectionService extends Service { if (callback != null) { callback.success(avatar); } - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully fetched pep avatar for " + avatar.owner); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully fetched pep avatar for " + avatar.owner); return; } } else { @@ -4742,57 +4647,54 @@ public class XmppConnectionService extends Service { } private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - final boolean previouslyOmittedPepFetch; - synchronized (mInProgressAvatarFetches) { - final String KEY = generateFetchKey(account, avatar); - mInProgressAvatarFetches.remove(KEY); - previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); - } - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.findChild("vCard", "vcard-temp"); - Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - String image = photo != null ? photo.findChildContent("BINVAL") : null; - if (image != null) { - avatar.image = image; - if (getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": successfully fetched vCard avatar for " + avatar.owner + " omittedPep=" + previouslyOmittedPepFetch); - if (avatar.owner.isBareJid()) { - if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); - account.setAvatar(avatar.getFilename()); - databaseBackend.updateAccount(account); - getAvatarService().clear(account); - updateAccountUi(); - } else { - final Contact contact = account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar, previouslyOmittedPepFetch); - syncRoster(account); - getAvatarService().clear(contact); - updateRosterUi(UpdateRosterReason.AVATAR); - } - updateConversationUi(); + final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar); + this.sendIqPacket(account, packet, response -> { + final boolean previouslyOmittedPepFetch; + synchronized (mInProgressAvatarFetches) { + final String KEY = generateFetchKey(account, avatar); + mInProgressAvatarFetches.remove(KEY); + previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY); + } + if (response.getType() == Iq.Type.RESULT) { + Element vCard = response.findChild("vCard", "vcard-temp"); + Element photo = vCard != null ? vCard.findChild("PHOTO") : null; + String image = photo != null ? photo.findChildContent("BINVAL") : null; + if (image != null) { + avatar.image = image; + if (getFileBackend().save(avatar)) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + + ": successfully fetched vCard avatar for " + avatar.owner + " omittedPep=" + previouslyOmittedPepFetch); + if (avatar.owner.isBareJid()) { + if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard"); + account.setAvatar(avatar.getFilename()); + databaseBackend.updateAccount(account); + getAvatarService().clear(account); + updateAccountUi(); } else { - Conversation conversation = find(account, avatar.owner.asBareJid()); - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner); - if (user != null) { - if (user.setAvatar(avatar)) { - getAvatarService().clear(user); - updateConversationUi(); - updateMucRosterUi(); - } - if (user.getRealJid() != null) { - Contact contact = account.getRoster().getContact(user.getRealJid()); - contact.setAvatar(avatar); - syncRoster(account); - getAvatarService().clear(contact); - updateRosterUi(UpdateRosterReason.AVATAR); - } + final Contact contact = account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar, previouslyOmittedPepFetch); + syncRoster(account); + getAvatarService().clear(contact); + updateRosterUi(UpdateRosterReason.AVATAR); + } + updateConversationUi(); + } else { + Conversation conversation = find(account, avatar.owner.asBareJid()); + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner); + if (user != null) { + if (user.setAvatar(avatar)) { + getAvatarService().clear(user); + updateConversationUi(); + updateMucRosterUi(); + } + if (user.getRealJid() != null) { + Contact contact = account.getRoster().getContact(user.getRealJid()); + contact.setAvatar(avatar); + syncRoster(account); + getAvatarService().clear(contact); + updateRosterUi(UpdateRosterReason.AVATAR); } } } @@ -4803,36 +4705,32 @@ public class XmppConnectionService extends Service { }); } - public void checkForAvatar(Account account, final UiCallback callback) { - IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - Element pubsub = packet.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); + public void checkForAvatar(final Account account, final UiCallback callback) { + final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null); + this.sendIqPacket(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); } - return; + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); } + return; } } } - callback.error(0, null); } + callback.error(0, null); }); } @@ -4849,10 +4747,10 @@ public class XmppConnectionService extends Service { } public void fetchVcard4(Account account, final Contact contact, final Consumer callback) { - IqPacket packet = this.mIqGenerator.retrieveVcard4(contact.getJid()); - sendIqPacket(account, packet, (a, result) -> { - if (result.getType() == IqPacket.TYPE.RESULT) { - final Element item = mIqParser.getItem(result); + final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid()); + sendIqPacket(account, packet, (result) -> { + if (result.getType() == Iq.Type.RESULT) { + final Element item = IqParser.getItem(result); if (item != null) { final Element vcard4 = item.findChild("vcard", Namespace.VCARD4); if (vcard4 != null) { @@ -4883,7 +4781,7 @@ public class XmppConnectionService extends Service { contact.setOption(Contact.Options.DIRTY_DELETE); Account account = contact.getAccount(); if (account.getStatus() == Account.State.ONLINE) { - IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); Element item = iq.query(Namespace.ROSTER).addChild("item"); item.setAttribute("jid", contact.getJid()); item.setAttribute("subscription", "remove"); @@ -4943,12 +4841,12 @@ public class XmppConnectionService extends Service { if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) { changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null); } - final MessagePacket packet = mMessageGenerator.invite(conversation, contact); + final var packet = mMessageGenerator.invite(conversation, contact); sendMessagePacket(conversation.getAccount(), packet); } public void directInvite(Conversation conversation, Jid jid) { - MessagePacket packet = mMessageGenerator.directInvite(conversation, jid); + final var packet = mMessageGenerator.directInvite(conversation, jid); sendMessagePacket(conversation.getAccount(), packet); } @@ -5292,7 +5190,7 @@ public class XmppConnectionService extends Service { if (sendDisplayedMarker && serverAssist) { final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation); - final MessagePacket packet = mMessageGenerator.confirm(last); + final var packet = mMessageGenerator.confirm(last); packet.addChild(mdsDisplayed); if (!last.isPrivateMessage()) { packet.setTo(packet.getTo().asBareJid()); @@ -5308,7 +5206,7 @@ public class XmppConnectionService extends Service { conversation.getAccount().getJid().asBareJid() + ": sending displayed marker to " + last.getCounterpart().toString()); - final MessagePacket packet = mMessageGenerator.confirm(last); + final var packet = mMessageGenerator.confirm(last); this.sendMessagePacket(account, packet); } } @@ -5362,7 +5260,6 @@ public class XmppConnectionService extends Service { public void updateMemorizingTrustManager() { final MemorizingTrustManager trustManager; - final var appSettings = new AppSettings(this); if (appSettings.isTrustSystemCAStore()) { trustManager = new MemorizingTrustManager(getApplicationContext()); } else { @@ -5415,15 +5312,15 @@ public class XmppConnectionService extends Service { return mucServers; } - public void sendMessagePacket(Account account, MessagePacket packet) { + public void sendMessagePacket(final Account account, final im.conversations.android.xmpp.model.stanza.Message packet) { final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.sendMessagePacket(packet); } } - public void sendPresencePacket(Account account, PresencePacket packet) { - XmppConnection connection = account.getXmppConnection(); + public void sendPresencePacket(final Account account, final im.conversations.android.xmpp.model.stanza.Presence packet) { + final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.sendPresencePacket(packet); } @@ -5431,22 +5328,22 @@ public class XmppConnectionService extends Service { public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) { final XmppConnection connection = account.getXmppConnection(); - if (connection != null) { - IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data); - connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true); + if (connection == null) { + return; } + connection.sendCreateAccountWithCaptchaPacket(id, data); } - public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) { + public void sendIqPacket(final Account account, final Iq packet, final Consumer callback) { sendIqPacket(account, packet, callback, null); } - public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback, Long timeout) { + public void sendIqPacket(final Account account, final Iq packet, final Consumer callback, Long timeout) { final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.sendIqPacket(packet, callback, timeout); } else if (callback != null) { - callback.onIqPacketReceived(account, new IqPacket(IqPacket.TYPE.TIMEOUT)); + callback.accept(Iq.TIMEOUT); } } @@ -5461,7 +5358,7 @@ public class XmppConnectionService extends Service { } else { status = getTargetPresence(); } - final PresencePacket packet = mPresenceGenerator.selfPresence(account, status); + final var packet = mPresenceGenerator.selfPresence(account, status); if (mLastActivity > 0 && includeIdleTimestamp) { long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates packet.addChild("idle", Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since)); @@ -5511,10 +5408,6 @@ public class XmppConnectionService extends Service { return this.mIqGenerator; } - public IqParser getIqParser() { - return this.mIqParser; - } - public JingleConnectionManager getJingleConnectionManager() { return this.mJingleConnectionManager; } @@ -5613,10 +5506,11 @@ public class XmppConnectionService extends Service { public boolean sendBlockRequest(final Blockable blockable, final boolean reportSpam, final String serverMsgId) { if (blockable != null && blockable.getBlockedJid() != null) { + final var account = blockable.getAccount(); final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - a.getBlocklist().add(jid); + this.sendIqPacket(account, getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (response) -> { + if (response.getType() == Iq.Type.RESULT) { + account.getBlocklist().add(jid); updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); } }); @@ -5657,31 +5551,29 @@ public class XmppConnectionService extends Service { public void sendUnblockRequest(final Blockable blockable) { if (blockable != null && blockable.getJid() != null) { + final var account = blockable.getAccount(); final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.getBlocklist().remove(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); - } + this.sendIqPacket(account, getIqGenerator().generateSetUnblockRequest(jid), response -> { + if (response.getType() == Iq.Type.RESULT) { + account.getBlocklist().remove(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); } }); } } - public void publishDisplayName(Account account) { + public void publishDisplayName(final Account account) { String displayName = account.getDisplayName(); - final IqPacket request; + final Iq request; if (TextUtils.isEmpty(displayName)) { request = mIqGenerator.deleteNode(Namespace.NICK); } else { request = mIqGenerator.publishNick(displayName); } mAvatarService.clear(account); - sendIqPacket(account, request, (account1, packet) -> { - if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet); + sendIqPacket(account, request, (packet) -> { + if (packet.getType() == Iq.Type.ERROR) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to modify nick name " + packet); } }); } @@ -5701,15 +5593,15 @@ public class XmppConnectionService extends Service { } public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) { - IqPacket request = new IqPacket(input == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET); + final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET); request.setTo(jid); Element query = request.query("jabber:iq:gateway"); if (input != null) { Element prompt = query.addChild("prompt"); prompt.setContent(input); } - sendIqPacket(account, request, (Account acct, IqPacket packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, request, packet -> { + if (packet.getType() == Iq.Type.RESULT) { callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null); } else { Element error = packet.findChild("error"); @@ -5738,7 +5630,7 @@ public class XmppConnectionService extends Service { } updateConversationUi(true); } else { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(jid); final String node = presence == null ? null : presence.getNode(); final String ver = presence == null ? null : presence.getVer(); @@ -5747,12 +5639,12 @@ public class XmppConnectionService extends Service { query.setAttribute("node", node + "#" + ver); } Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + (key == null ? "" : key.second) + " to " + jid); - sendIqPacket(account, request, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + sendIqPacket(account, request, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) { databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult(a.getRoster(), presence == null ? null : presence.getHash(), presence == null ? null : presence.getVer(), jid.getResource(), discoveryResult); + injectServiceDiscoveryResult(account.getRoster(), presence == null ? null : presence.getHash(), presence == null ? null : presence.getVer(), jid.getResource(), discoveryResult); if (discoveryResult.hasIdentity("gateway", "pstn")) { final Contact contact = account.getRoster().getContact(jid); contact.registerAsPhoneAccount(this); @@ -5761,7 +5653,7 @@ public class XmppConnectionService extends Service { updateConversationUi(true); if (cb != null) cb.run(); } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); } } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid); @@ -5770,8 +5662,8 @@ public class XmppConnectionService extends Service { } } - public void fetchCommands(Account account, final Jid jid, OnIqPacketReceived callback) { - final IqPacket request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands"); + public void fetchCommands(Account account, final Jid jid, Consumer callback) { + final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands"); sendIqPacket(account, request, callback); } @@ -5806,13 +5698,13 @@ public class XmppConnectionService extends Service { } } - public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) { + public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) { final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); - IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.addChild("prefs", version.namespace); - sendIqPacket(account, request, (account1, packet) -> { - Element prefs = packet.findChild("prefs", version.namespace); - if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) { + sendIqPacket(account, request, (packet) -> { + final Element prefs = packet.findChild("prefs", version.namespace); + if (packet.getType() == Iq.Type.RESULT && prefs != null) { callback.onPreferencesFetched(prefs); } else { callback.onPreferencesFetchFailed(); @@ -5912,7 +5804,7 @@ public class XmppConnectionService extends Service { } public void pushMamPreferences(Account account, Element prefs) { - IqPacket set = new IqPacket(IqPacket.TYPE.SET); + final Iq set = new Iq(Iq.Type.SET); set.addChild(prefs); sendIqPacket(account, set, null); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 69a9ccfc912728f665b24abad7077e7cf8bf8965..744b9db650f899ca9e4050bab73144408bd7bac6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -158,28 +158,39 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } }; - private final OnClickListener mChangeConferenceSettings = new OnClickListener() { - @Override - public void onClick(View v) { - final MucOptions mucOptions = mConversation.getMucOptions(); - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this); - MucConfiguration configuration = MucConfiguration.get(ConferenceDetailsActivity.this, mAdvancedMode, mucOptions); - builder.setTitle(configuration.title); - final boolean[] values = configuration.values; - builder.setMultiChoiceItems(configuration.names, values, (dialog, which, isChecked) -> values[which] = isChecked); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - final Bundle options = configuration.toBundle(values); - options.putString("muc#roomconfig_persistentroom", "1"); - options.putString("{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", options.getString("muc#roomconfig_allowinvites")); - xmppConnectionService.pushConferenceConfiguration(mConversation, - options, - ConferenceDetailsActivity.this); - }); - builder.create().show(); - } - }; - + private final OnClickListener mChangeConferenceSettings = + new OnClickListener() { + @Override + public void onClick(View v) { + final MucOptions mucOptions = mConversation.getMucOptions(); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this); + MucConfiguration configuration = + MucConfiguration.get( + ConferenceDetailsActivity.this, mAdvancedMode, mucOptions); + builder.setTitle(configuration.title); + final boolean[] values = configuration.values; + builder.setMultiChoiceItems( + configuration.names, + values, + (dialog, which, isChecked) -> values[which] = isChecked); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + final Bundle options = configuration.toBundle(values); + options.putString("muc#roomconfig_persistentroom", "1"); + if (options.containsKey("muc#roomconfig_allowinvites")) { + options.putString( + "{http://prosody.im/protocol/muc}roomconfig_allowmemberinvites", + options.getString("muc#roomconfig_allowinvites")); + } + xmppConnectionService.pushConferenceConfiguration( + mConversation, options, ConferenceDetailsActivity.this); + }); + builder.create().show(); + } + }; @Override public void onConversationUpdate() { @@ -256,6 +267,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers popupMenu.show(); return true; }); + this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic)); this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked); this.binding.mucEditTitle.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(this); @@ -343,6 +355,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucEditor.setVisibility(View.VISIBLE); this.binding.mucDisplay.setVisibility(View.GONE); this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp); + this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel)); final String name = mucOptions.getName(); this.binding.mucEditTitle.setText(""); final boolean owner = mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER); @@ -417,6 +430,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucEditor.setVisibility(View.GONE); this.binding.mucDisplay.setVisibility(View.VISIBLE); this.binding.editMucNameButton.setImageResource(R.drawable.ic_edit_24dp); + this.binding.editMucNameButton.setContentDescription(getString(R.string.edit_name_and_topic)); } private void onMucInfoUpdated(String subject, String name) { @@ -776,8 +790,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final Bookmark bookmark = mConversation.getBookmark(); if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) { this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp); + this.binding.editMucNameButton.setContentDescription(getString(R.string.save)); } else { this.binding.editMucNameButton.setImageResource(R.drawable.ic_cancel_24dp); + this.binding.editMucNameButton.setContentDescription(getString(R.string.cancel)); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 4d1d441215f2e399550366a24326ac254506163e..985d42cb1a925234fbd3c11ccb73b26b0bc840d4 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -185,7 +185,8 @@ import eu.siacs.conversations.xmpp.jingle.JingleFileTransferConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; import eu.siacs.conversations.xmpp.jingle.RtpCapability; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.stanza.Iq; import org.jetbrains.annotations.NotNull; @@ -3333,13 +3334,13 @@ public class ConversationFragment extends XmppFragment } else { if (!delayShow) conversation.showViewPager(); binding.commandsViewProgressbar.setVisibility(View.VISIBLE); - activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (a, iq) -> { + activity.xmppConnectionService.fetchCommands(conversation.getAccount(), commandJid, (iq) -> { if (activity == null) return; activity.runOnUiThread(() -> { binding.commandsViewProgressbar.setVisibility(View.GONE); commandAdapter.clear(); - if (iq.getType() == IqPacket.TYPE.RESULT) { + if (iq.getType() == Iq.Type.RESULT) { for (Element child : iq.query().getChildren()) { if (!"item".equals(child.getName()) || !Namespace.DISCO_ITEMS.equals(child.getNamespace())) continue; commandAdapter.add(new CommandAdapter.Command0050(child)); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index ad571a91425d7e010c3cdfc009f659f6c55b0e04..5c5da91df921ce72a375102f9192322b073b8619 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -242,7 +242,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (ExceptionHelper.checkForCrash(this)) return; if (offerToSetupDiallerIntegration()) return; if (offerToDownloadStickers()) return; - openBatteryOptimizationDialogIfNeeded(); + if (openBatteryOptimizationDialogIfNeeded()) return; + requestNotificationPermissionIfNeeded(); xmppConnectionService.rescanStickers(); } } @@ -267,7 +268,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio intent.setData(uri); try { startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show(); } }); @@ -360,16 +361,16 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private void notifyFragmentOfBackendConnected(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); - if (fragment instanceof OnBackendConnected) { - ((OnBackendConnected) fragment).onBackendConnected(); + if (fragment instanceof OnBackendConnected callback) { + callback.onBackendConnected(); } } private void refreshFragment(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); - if (fragment instanceof XmppFragment) { - ((XmppFragment) fragment).refresh(); - if (refreshForNewCaps) ((XmppFragment) fragment).refreshForNewCaps(newCapsJids); + if (fragment instanceof XmppFragment xmppFragment) { + xmppFragment.refresh(); + if (refreshForNewCaps) xmppFragment.refreshForNewCaps(newCapsJids); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index 1a53b7d4c508698503ce67cd26b8610eb81220f2..d05819f3a49179e6280e100f41b4e4cddbd9bd16 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -47,6 +47,7 @@ import android.view.ViewGroup; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; @@ -96,42 +97,36 @@ public class ConversationsOverviewFragment extends XmppFragment { private FragmentConversationsOverviewBinding binding; private ConversationAdapter conversationsAdapter; private XmppActivity activity; - private float mSwipeEscapeVelocity = 0f; private final PendingActionHelper pendingActionHelper = new PendingActionHelper(); private final ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,LEFT|RIGHT) { @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - //todo maybe we can manually changing the position of the conversation + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false; } @Override - public float getSwipeEscapeVelocity (float defaultValue) { - return mSwipeEscapeVelocity; + public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, + float dX, float dY, int actionState, boolean isCurrentlyActive) { + if (viewHolder instanceof ConversationAdapter.ConversationViewHolder conversationViewHolder) { + getDefaultUIUtil().onDraw(c,recyclerView,conversationViewHolder.binding.frame,dX,dY,actionState,isCurrentlyActive); + } } @Override - public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, - float dX, float dY, int actionState, boolean isCurrentlyActive) { - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){ - Paint paint = new Paint(); - paint.setColor(MaterialColors.getColor(viewHolder.itemView, com.google.android.material.R.attr.colorSecondaryFixedDim)); - paint.setStyle(Paint.Style.FILL); - c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop() - ,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(), paint); + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { + if (viewHolder instanceof ConversationAdapter.ConversationViewHolder conversationViewHolder) { + getDefaultUIUtil().clearView(conversationViewHolder.binding.frame); } } @Override - public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { - super.clearView(recyclerView, viewHolder); - viewHolder.itemView.setAlpha(1f); + public float getSwipeEscapeVelocity(final float defaultEscapeVelocity) { + return 32 * defaultEscapeVelocity; } @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int direction) { pendingActionHelper.execute(); int position = viewHolder.getLayoutPosition(); try { @@ -291,7 +286,6 @@ public class ConversationsOverviewFragment extends XmppFragment { @Override public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity); this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false); this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity())); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 3a48ca2eb3fbcceed1b2f6c8876f59af9afac194..bd685ea791d8128a137a5cadffca1a45ce484e2c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -43,6 +43,7 @@ import com.google.android.material.color.MaterialColors; 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 com.rarepebble.colorpicker.ColorPickerView; @@ -104,8 +105,14 @@ import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; -public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist, - OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched { +public class EditAccountActivity extends OmemoActivity + implements OnAccountUpdate, + OnUpdateBlocklist, + OnKeyStatusUpdated, + OnCaptchaRequested, + KeyChainAliasCallback, + XmppConnectionService.OnShowErrorToast, + XmppConnectionService.OnMamPreferencesFetched { public static final String EXTRA_OPENED_FROM_NOTIFICATION = "opened_from_notification"; public static final String EXTRA_FORCE_REGISTER = "force_register"; @@ -122,37 +129,44 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private boolean mUsernameMode = false; private boolean mShowOptions = false; private Account mAccount; - private final OnClickListener mCancelButtonClickListener = v -> { - deleteAccountAndReturnIfNecessary(); - finish(); - }; - private final UiCallback mAvatarFetchCallback = new UiCallback() { + private final OnClickListener mCancelButtonClickListener = + v -> { + deleteAccountAndReturnIfNecessary(); + finish(); + }; + private final UiCallback mAvatarFetchCallback = + new UiCallback() { - @Override - public void userInputRequired(final PendingIntent pi, final Avatar avatar) { - finishInitialSetup(avatar); - } + @Override + public void userInputRequired(final PendingIntent pi, final Avatar avatar) { + finishInitialSetup(avatar); + } - @Override - public void success(final Avatar avatar) { - finishInitialSetup(avatar); - } + @Override + public void success(final Avatar avatar) { + finishInitialSetup(avatar); + } - @Override - public void error(final int errorCode, final Avatar avatar) { - finishInitialSetup(avatar); - } - }; - private final OnClickListener mAvatarClickListener = new OnClickListener() { - @Override - public void onClick(final View view) { - if (mAccount != null) { - final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - startActivity(intent); - } - } - }; + @Override + public void error(final int errorCode, final Avatar avatar) { + finishInitialSetup(avatar); + } + }; + private final OnClickListener mAvatarClickListener = + new OnClickListener() { + @Override + public void onClick(final View view) { + if (mAccount != null) { + final Intent intent = + new Intent( + getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra( + EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); + startActivity(intent); + } + } + }; private String messageFingerprint; private boolean mFetchingAvatar = false; private Toast mFetchingMamPrefsToast; @@ -161,215 +175,268 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private XmppUri pendingUri = null; private boolean mUseTor; private ActivityEditAccountBinding binding; - private String newPassword = null; - private final OnClickListener mSaveButtonClickListener = new OnClickListener() { - - @Override - public void onClick(final View v) { - final String password = binding.accountPassword.getText().toString(); - final boolean wasDisabled = mAccount != null && mAccount.getStatus() == Account.State.DISABLED; - final boolean accountInfoEdited = accountInfoEdited(); - - ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground(); - if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) { - mAccount.setColor(previewColor.getColor()); - } + private final OnClickListener mSaveButtonClickListener = + new OnClickListener() { + + @Override + public void onClick(final View v) { + final String password = binding.accountPassword.getText().toString(); + final boolean wasDisabled = + mAccount != null && mAccount.getStatus() == Account.State.DISABLED; + final boolean accountInfoEdited = accountInfoEdited(); + + ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground(); + if (previewColor != null && previewColor.getColor() != mAccount.getColor(isDark())) { + mAccount.setColor(previewColor.getColor()); + } - if (mInitMode && mAccount != null) { - mAccount.setOption(Account.OPTION_DISABLED, false); - } - if (mAccount != null && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT).contains(mAccount.getStatus()) && !accountInfoEdited) { - mAccount.setOption(Account.OPTION_SOFT_DISABLED, false); - mAccount.setOption(Account.OPTION_DISABLED, false); - if (!xmppConnectionService.updateAccount(mAccount)) { - Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); - } - return; - } - final boolean registerNewAccount; - if (mForceRegister != null) { - registerNewAccount = mForceRegister; - } else { - registerNewAccount = binding.accountRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI; - } - if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } + if (mInitMode && mAccount != null) { + mAccount.setOption(Account.OPTION_DISABLED, false); + } + if (mAccount != null + && Arrays.asList(Account.State.DISABLED, Account.State.LOGGED_OUT) + .contains(mAccount.getStatus()) + && !accountInfoEdited) { + mAccount.setOption(Account.OPTION_SOFT_DISABLED, false); + mAccount.setOption(Account.OPTION_DISABLED, false); + if (!xmppConnectionService.updateAccount(mAccount)) { + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_update_account, + Toast.LENGTH_SHORT) + .show(); + } + return; + } + final boolean registerNewAccount; + if (mForceRegister != null) { + registerNewAccount = mForceRegister; + } else { + registerNewAccount = + binding.accountRegisterNew.isChecked() + && !Config.DISALLOW_REGISTRATION_IN_UI; + } + if (mUsernameMode && binding.accountJid.getText().toString().contains("@")) { + binding.accountJidLayout.setError(getString(R.string.invalid_username)); + removeErrorsOnAllBut(binding.accountJidLayout); + binding.accountJid.requestFocus(); + return; + } - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - final boolean startOrbot = mAccount != null && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE; - if (startOrbot) { - if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) { - TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT); - } else { - TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT); - } - return; - } + XmppConnection connection = + mAccount == null ? null : mAccount.getXmppConnection(); + final boolean startOrbot = + mAccount != null + && mAccount.getStatus() == Account.State.TOR_NOT_AVAILABLE; + if (startOrbot) { + if (TorServiceUtils.isOrbotInstalled(EditAccountActivity.this)) { + TorServiceUtils.startOrbot(EditAccountActivity.this, REQUEST_ORBOT); + } else { + TorServiceUtils.downloadOrbot(EditAccountActivity.this, REQUEST_ORBOT); + } + return; + } - if (inNeedOfSaslAccept()) { - mAccount.resetPinnedMechanism(); - if (!xmppConnectionService.updateAccount(mAccount)) { - Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); - } - return; - } + if (inNeedOfSaslAccept()) { + mAccount.resetPinnedMechanism(); + if (!xmppConnectionService.updateAccount(mAccount)) { + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_update_account, + Toast.LENGTH_SHORT) + .show(); + } + return; + } - final boolean openRegistrationUrl = registerNewAccount && !accountInfoEdited && mAccount != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB; - final boolean openPaymentUrl = mAccount != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED; - final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl; - final HttpUrl url = connection != null && redirectionWorthyStatus ? connection.getRedirectionUrl() : null; - if (url != null && !wasDisabled) { - try { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))); - return; - } catch (ActivityNotFoundException e) { - Toast.makeText(EditAccountActivity.this, R.string.application_found_to_open_website, Toast.LENGTH_SHORT).show(); - return; - } - } + final boolean openRegistrationUrl = + registerNewAccount + && !accountInfoEdited + && mAccount != null + && mAccount.getStatus() == Account.State.REGISTRATION_WEB; + final boolean openPaymentUrl = + mAccount != null + && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED; + final boolean redirectionWorthyStatus = openPaymentUrl || openRegistrationUrl; + final HttpUrl url = + connection != null && redirectionWorthyStatus + ? connection.getRedirectionUrl() + : null; + if (url != null && !wasDisabled) { + try { + startActivity( + new Intent(Intent.ACTION_VIEW, Uri.parse(url.toString()))); + return; + } catch (ActivityNotFoundException e) { + Toast.makeText( + EditAccountActivity.this, + R.string.application_found_to_open_website, + Toast.LENGTH_SHORT) + .show(); + return; + } + } - final Jid jid; - try { - if (mUsernameMode) { - jid = Jid.ofEscaped(binding.accountJid.getText().toString(), getUserModeDomain(), null); - } else { - jid = Jid.ofEscaped(binding.accountJid.getText().toString()); - Resolver.checkDomain(jid); - } - } catch (final NullPointerException | IllegalArgumentException e) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); - } - binding.accountJid.requestFocus(); - removeErrorsOnAllBut(binding.accountJidLayout); - return; - } - final String hostname; - int numericPort = 5222; - if (mShowOptions) { - hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText()); - final String port = CharMatcher.whitespace().removeFrom(binding.port.getText()); - if (Resolver.invalidHostname(hostname)) { - binding.hostnameLayout.setError(getString(R.string.not_valid_hostname)); - binding.hostname.requestFocus(); - removeErrorsOnAllBut(binding.hostnameLayout); - return; - } - if (!hostname.isEmpty()) { + final Jid jid; try { - numericPort = Integer.parseInt(port); - if (numericPort < 0 || numericPort > 65535) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); + if (mUsernameMode) { + jid = + Jid.ofEscaped( + binding.accountJid.getText().toString(), + getUserModeDomain(), + null); + } else { + jid = Jid.ofEscaped(binding.accountJid.getText().toString()); + Resolver.checkDomain(jid); + } + } catch (final NullPointerException | IllegalArgumentException e) { + if (mUsernameMode) { + binding.accountJidLayout.setError(getString(R.string.invalid_username)); + } else { + binding.accountJidLayout.setError(getString(R.string.invalid_jid)); + } + binding.accountJid.requestFocus(); + removeErrorsOnAllBut(binding.accountJidLayout); + return; + } + final String hostname; + int numericPort = 5222; + if (mShowOptions) { + hostname = CharMatcher.whitespace().removeFrom(binding.hostname.getText()); + final String port = + CharMatcher.whitespace().removeFrom(binding.port.getText()); + if (Resolver.invalidHostname(hostname)) { + binding.hostnameLayout.setError(getString(R.string.not_valid_hostname)); + binding.hostname.requestFocus(); + removeErrorsOnAllBut(binding.hostnameLayout); return; } + if (!hostname.isEmpty()) { + try { + numericPort = Integer.parseInt(port); + if (numericPort < 0 || numericPort > 65535) { + binding.portLayout.setError( + getString(R.string.not_a_valid_port)); + removeErrorsOnAllBut(binding.portLayout); + binding.port.requestFocus(); + return; + } + + } catch (NumberFormatException e) { + binding.portLayout.setError(getString(R.string.not_a_valid_port)); + removeErrorsOnAllBut(binding.portLayout); + binding.port.requestFocus(); + return; + } + } + } else { + hostname = null; + } - } catch (NumberFormatException e) { - binding.portLayout.setError(getString(R.string.not_a_valid_port)); - removeErrorsOnAllBut(binding.portLayout); - binding.port.requestFocus(); + if (jid.getLocal() == null) { + if (mUsernameMode) { + binding.accountJidLayout.setError(getString(R.string.invalid_username)); + } else { + binding.accountJidLayout.setError(getString(R.string.invalid_jid)); + } + removeErrorsOnAllBut(binding.accountJidLayout); + binding.accountJid.requestFocus(); return; } + if (mAccount != null) { + if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { + mAccount.setOption( + Account.OPTION_MAGIC_CREATE, + mAccount.getPassword().contains(password)); + } + mAccount.setJid(jid); + mAccount.setPort(numericPort); + mAccount.setHostname(hostname); + binding.accountJidLayout.setError(null); + mAccount.setPassword(password); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + if (!xmppConnectionService.updateAccount(mAccount)) { + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_update_account, + Toast.LENGTH_SHORT) + .show(); + return; + } + } else { + if (xmppConnectionService.findAccountByJid(jid) != null) { + binding.accountJidLayout.setError( + getString(R.string.account_already_exists)); + removeErrorsOnAllBut(binding.accountJidLayout); + binding.accountJid.requestFocus(); + return; + } + mAccount = new Account(jid.asBareJid(), password); + mAccount.setPort(numericPort); + mAccount.setHostname(hostname); + mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); + xmppConnectionService.createAccount(mAccount); + } + binding.hostnameLayout.setError(null); + binding.portLayout.setError(null); + if (mAccount.isOnion()) { + Toast.makeText( + EditAccountActivity.this, + R.string.audio_video_disabled_tor, + Toast.LENGTH_LONG) + .show(); + } + if (mAccount.isEnabled() && !registerNewAccount && !mInitMode) { + finish(); + } else { + updateSaveButton(); + updateAccountInformation(true); + } } - } else { - hostname = null; - } - - if (jid.getLocal() == null) { - if (mUsernameMode) { - binding.accountJidLayout.setError(getString(R.string.invalid_username)); - } else { - binding.accountJidLayout.setError(getString(R.string.invalid_jid)); - } - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; - } - if (mAccount != null) { - if (mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { - mAccount.setOption(Account.OPTION_MAGIC_CREATE, mAccount.getPassword().contains(password)); - } - mAccount.setJid(jid); - mAccount.setPort(numericPort); - mAccount.setHostname(hostname); - binding.accountJidLayout.setError(null); - mAccount.setPassword(password); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - if (!xmppConnectionService.updateAccount(mAccount)) { - Toast.makeText(EditAccountActivity.this, R.string.unable_to_update_account, Toast.LENGTH_SHORT).show(); - return; - } - } else { - if (xmppConnectionService.findAccountByJid(jid) != null) { - binding.accountJidLayout.setError(getString(R.string.account_already_exists)); - removeErrorsOnAllBut(binding.accountJidLayout); - binding.accountJid.requestFocus(); - return; + }; + private final TextWatcher mTextWatcher = + new TextWatcher() { + + @Override + public void onTextChanged( + final CharSequence s, final int start, final int before, final int count) { + updatePortLayout(); + updateSaveButton(); } - mAccount = new Account(jid.asBareJid(), password); - mAccount.setPort(numericPort); - mAccount.setHostname(hostname); - mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); - xmppConnectionService.createAccount(mAccount); - } - binding.hostnameLayout.setError(null); - binding.portLayout.setError(null); - if (mAccount.isOnion()) { - Toast.makeText(EditAccountActivity.this, R.string.audio_video_disabled_tor, Toast.LENGTH_LONG).show(); - } - if (mAccount.isEnabled() - && !registerNewAccount - && !mInitMode) { - finish(); - } else { - updateSaveButton(); - updateAccountInformation(true); - } - } - }; - private final TextWatcher mTextWatcher = new TextWatcher() { - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - updatePortLayout(); - updateSaveButton(); - } - - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { - } - - @Override - public void afterTextChanged(final Editable s) { - - } - }; - private final View.OnFocusChangeListener mEditTextFocusListener = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - EditText et = (EditText) view; - if (b) { - int resId = mUsernameMode ? R.string.username : R.string.account_settings_example_jabber_id; - if (view.getId() == R.id.hostname) { - resId = mUseTor ? R.string.hostname_or_onion : R.string.hostname_example; + @Override + public void beforeTextChanged( + final CharSequence s, final int start, final int count, final int after) {} + + @Override + public void afterTextChanged(final Editable s) {} + }; + private final View.OnFocusChangeListener mEditTextFocusListener = + new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean b) { + EditText et = (EditText) view; + if (b) { + int resId = + mUsernameMode + ? R.string.username + : R.string.account_settings_example_jabber_id; + if (view.getId() == R.id.hostname) { + resId = + mUseTor + ? R.string.hostname_or_onion + : R.string.hostname_example; + } + final int res = resId; + new Handler().postDelayed(() -> et.setHint(res), 200); + } else { + et.setHint(null); + } } - final int res = resId; - new Handler().postDelayed(() -> et.setHint(res), 200); - } else { - et.setHint(null); - } - } - }; + }; - private static void setAvailabilityRadioButton(Presence.Status status, DialogPresenceBinding binding) { + private static void setAvailabilityRadioButton( + Presence.Status status, DialogPresenceBinding binding) { if (status == null) { binding.online.setChecked(true); return; @@ -403,9 +470,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat public void refreshUiReal() { invalidateOptionsMenu(); - if (mAccount != null - && mAccount.getStatus() != Account.State.ONLINE - && mFetchingAvatar) { + if (mAccount != null && mAccount.getStatus() != Account.State.ONLINE && mFetchingAvatar) { Intent intent = new Intent(this, StartConversationActivity.class); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); @@ -435,30 +500,41 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void deleteAccountAndReturnIfNecessary() { - if (mInitMode && mAccount != null && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) { + if (mInitMode + && mAccount != null + && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY)) { xmppConnectionService.deleteAccount(mAccount); } - final boolean magicCreate = mAccount != null && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); + final boolean magicCreate = + mAccount != null + && mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) + && !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); final Jid jid = mAccount == null ? null : mAccount.getJid(); - if (SignupUtils.isSupportTokenRegistry() && jid != null && magicCreate && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) { + if (SignupUtils.isSupportTokenRegistry() + && jid != null + && magicCreate + && !jid.getDomain().equals(Config.MAGIC_CREATE_DOMAIN)) { final Jid preset; if (mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME)) { preset = jid.asBareJid(); } else { preset = jid.getDomain(); } - final Intent intent = SignupUtils.getTokenRegistrationIntent(this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); + final Intent intent = + SignupUtils.getTokenRegistrationIntent( + this, preset, mAccount.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN)); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); return; } - - final List accounts = xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); + final List accounts = + xmppConnectionService == null ? null : xmppConnectionService.getAccounts(); if (accounts != null && accounts.size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - Intent intent = SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); + Intent intent = + SignupUtils.getSignUpIntent(this, mForceRegister != null && mForceRegister); StartConversationActivity.addInviteUri(intent, getIntent()); startActivity(intent); } @@ -470,27 +546,38 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } protected void finishInitialSetup(final Avatar avatar) { - runOnUiThread(() -> { - SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this); - final Intent intent; - final XmppConnection connection = mAccount.getXmppConnection(); - final boolean wasFirstAccount = xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1; - if (avatar != null || (connection != null && !connection.getFeatures().pep())) { - intent = new Intent(getApplicationContext(), StartConversationActivity.class); - intent.putExtra("init", true); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - } else { - intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); - intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); - intent.putExtra("setup", true); - } - if (wasFirstAccount) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - } - StartConversationActivity.addInviteUri(intent, getIntent()); - startActivity(intent); - finish(); - }); + runOnUiThread( + () -> { + SoftKeyboardUtils.hideSoftKeyboard(EditAccountActivity.this); + final Intent intent; + final XmppConnection connection = mAccount.getXmppConnection(); + final boolean wasFirstAccount = + xmppConnectionService != null + && xmppConnectionService.getAccounts().size() == 1; + if (avatar != null || (connection != null && !connection.getFeatures().pep())) { + intent = + new Intent( + getApplicationContext(), StartConversationActivity.class); + intent.putExtra("init", true); + intent.putExtra( + EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); + } else { + intent = + new Intent( + getApplicationContext(), + PublishProfilePictureActivity.class); + intent.putExtra( + EXTRA_ACCOUNT, mAccount.getJid().asBareJid().toEscapedString()); + intent.putExtra("setup", true); + } + if (wasFirstAccount) { + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + } + StartConversationActivity.addInviteUri(intent, getIntent()); + startActivity(intent); + finish(); + }); } @Override @@ -514,8 +601,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (requestCode == REQUEST_UNLOCK) { if (resultCode == RESULT_OK) { openChangePassword(true); - } else { - this.newPassword = null; } } } @@ -526,7 +611,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } protected void processFingerprintVerification(XmppUri uri, boolean showWarningToast) { - if (mAccount != null && mAccount.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) { + if (mAccount != null + && mAccount.getJid().asBareJid().equals(uri.getJid()) + && uri.hasFingerprints()) { if (xmppConnectionService.verifyFingerprints(mAccount, uri.getFingerprints())) { Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show(); updateAccountInformation(false); @@ -553,10 +640,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.saveButton.setText(R.string.save); this.binding.saveButton.setEnabled(true); } else if (mAccount != null - && (mAccount.getStatus() == Account.State.CONNECTING || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL || mFetchingAvatar)) { + && (mAccount.getStatus() == Account.State.CONNECTING + || mAccount.getStatus() == Account.State.REGISTRATION_SUCCESSFUL + || mFetchingAvatar)) { this.binding.saveButton.setEnabled(false); this.binding.saveButton.setText(R.string.account_status_connecting); - } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED && !mInitMode) { + } else if (mAccount != null + && mAccount.getStatus() == Account.State.DISABLED + && !mInitMode) { this.binding.saveButton.setEnabled(true); this.binding.saveButton.setText(R.string.enable); } else if (torNeedsInstall(mAccount)) { @@ -574,8 +665,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.saveButton.setEnabled(false); } } else { - XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - HttpUrl url = connection != null && mAccount.getStatus() == Account.State.PAYMENT_REQUIRED ? connection.getRedirectionUrl() : null; + XmppConnection connection = + mAccount == null ? null : mAccount.getXmppConnection(); + HttpUrl url = + connection != null + && mAccount.getStatus() + == Account.State.PAYMENT_REQUIRED + ? connection.getRedirectionUrl() + : null; if (url != null) { this.binding.saveButton.setText(R.string.open_website); } else if (inNeedOfSaslAccept()) { @@ -586,8 +683,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } } else { XmppConnection connection = mAccount == null ? null : mAccount.getXmppConnection(); - HttpUrl url = connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB ? connection.getRedirectionUrl() : null; - if (url != null && this.binding.accountRegisterNew.isChecked() && !accountInfoEdited) { + HttpUrl url = + connection != null && mAccount.getStatus() == Account.State.REGISTRATION_WEB + ? connection.getRedirectionUrl() + : null; + if (url != null + && this.binding.accountRegisterNew.isChecked() + && !accountInfoEdited) { this.binding.saveButton.setText(R.string.open_website); } else { this.binding.saveButton.setText(R.string.next); @@ -597,7 +699,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private boolean torNeedsInstall(final Account account) { - return account != null && account.getStatus() == Account.State.TOR_NOT_AVAILABLE && !TorServiceUtils.isOrbotInstalled(this); + return account != null + && account.getStatus() == Account.State.TOR_NOT_AVAILABLE + && !TorServiceUtils.isOrbotInstalled(this); } private boolean torNeedsStart(final Account account) { @@ -609,11 +713,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat return false; } ColorDrawable previewColor = (ColorDrawable) binding.colorPreview.getBackground(); - return jidEdited() || - !this.mAccount.getPassword().equals(this.binding.accountPassword.getText().toString()) || - !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) || - this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor()) || - !String.valueOf(this.mAccount.getPort()).equals(this.binding.port.getText().toString()); + return jidEdited() + || !this.mAccount + .getPassword() + .equals(this.binding.accountPassword.getText().toString()) + || !this.mAccount.getHostname().equals(this.binding.hostname.getText().toString()) + || this.mAccount.getColor(isDark()) != (previewColor == null ? 0 : previewColor.getColor()) + || !String.valueOf(this.mAccount.getPort()) + .equals(this.binding.port.getText().toString()); } protected boolean jidEdited() { @@ -660,7 +767,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (savedInstanceState != null && savedInstanceState.getBoolean("showMoreTable")) { changeMoreTableVisibility(true); } - final OnCheckedChangeListener OnCheckedShowConfirmPassword = (buttonView, isChecked) -> updateSaveButton(); + final OnCheckedChangeListener OnCheckedShowConfirmPassword = + (buttonView, isChecked) -> updateSaveButton(); this.binding.accountRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); if (Config.DISALLOW_REGISTRATION_IN_UI) { this.binding.accountRegisterNew.setVisibility(View.GONE); @@ -703,18 +811,23 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void onEditYourNameClicked(View view) { - quickEdit(mAccount.getDisplayName(), R.string.your_name, value -> { - final String displayName = value.trim(); - updateDisplayName(displayName); - mAccount.setDisplayName(displayName); - xmppConnectionService.publishDisplayName(mAccount); - refreshAvatar(); - return null; - }, true); + quickEdit( + mAccount.getDisplayName(), + R.string.your_name, + value -> { + final String displayName = value.trim(); + updateDisplayName(displayName); + mAccount.setDisplayName(displayName); + xmppConnectionService.publishDisplayName(mAccount); + refreshAvatar(); + return null; + }, + true); } private void refreshAvatar() { - AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar( + mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); } @Override @@ -789,9 +902,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } } boolean init = intent.getBooleanExtra("init", false); - boolean openedFromNotification = intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false); + boolean openedFromNotification = + intent.getBooleanExtra(EXTRA_OPENED_FROM_NOTIFICATION, false); Log.d(Config.LOGTAG, "extras " + intent.getExtras()); - this.mForceRegister = intent.hasExtra(EXTRA_FORCE_REGISTER) ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) : null; + this.mForceRegister = + intent.hasExtra(EXTRA_FORCE_REGISTER) + ? intent.getBooleanExtra(EXTRA_FORCE_REGISTER, false) + : null; Log.d(Config.LOGTAG, "force register=" + mForceRegister); this.mInitMode = init || this.jidToEdit == null; this.messageFingerprint = intent.getStringExtra("fingerprint"); @@ -801,7 +918,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat configureActionBar(getSupportActionBar(), !openedFromNotification); } else { this.binding.avater.setVisibility(View.GONE); - configureActionBar(getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null)); + configureActionBar( + getSupportActionBar(), !(init && Config.MAGIC_CREATE_DOMAIN == null)); if (mForceRegister != null) { if (mForceRegister) { setTitle(R.string.register_new_account); @@ -833,13 +951,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat TextView warning = view.findViewById(R.id.warning); warning.setText(R.string.verifying_omemo_keys_trusted_source_account); builder.setView(view); - builder.setPositiveButton(R.string.continue_btn, (dialog, which) -> { - if (isTrustedSource.isChecked()) { - processFingerprintVerification(xmppUri, false); - } else { - finish(); - } - }); + builder.setPositiveButton( + R.string.continue_btn, + (dialog, which) -> { + if (isTrustedSource.isChecked()) { + processFingerprintVerification(xmppUri, false); + } else { + finish(); + } + }); builder.setNegativeButton(R.string.cancel, (dialog, which) -> finish()); final var dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); @@ -863,9 +983,11 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat @Override public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) { if (mAccount != null) { - savedInstanceState.putString("account", mAccount.getJid().asBareJid().toEscapedString()); + savedInstanceState.putString( + "account", mAccount.getJid().asBareJid().toEscapedString()); savedInstanceState.putBoolean("initMode", mInitMode); - savedInstanceState.putBoolean("showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE); + savedInstanceState.putBoolean( + "showMoreTable", binding.serverInfoMore.getVisibility() == View.VISIBLE); } super.onSaveInstanceState(savedInstanceState); } @@ -874,7 +996,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat boolean init = true; if (mSavedInstanceAccount != null) { try { - this.mAccount = xmppConnectionService.findAccountByJid(Jid.ofEscaped(mSavedInstanceAccount)); + this.mAccount = + xmppConnectionService.findAccountByJid( + Jid.ofEscaped(mSavedInstanceAccount)); this.mInitMode = mSavedInstanceInit; init = false; } catch (IllegalArgumentException e) { @@ -887,7 +1011,9 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (mAccount != null) { this.mInitMode |= this.mAccount.isOptionSet(Account.OPTION_REGISTER); - this.mUsernameMode |= mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && mAccount.isOptionSet(Account.OPTION_REGISTER); + this.mUsernameMode |= + mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) + && mAccount.isOptionSet(Account.OPTION_REGISTER); if (mPendingFingerprintVerificationUri != null) { processFingerprintVerification(mPendingFingerprintVerificationUri, false); mPendingFingerprintVerificationUri = null; @@ -895,16 +1021,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat updateAccountInformation(init); } - - if (Config.MAGIC_CREATE_DOMAIN == null && this.xmppConnectionService.getAccounts().size() == 0) { + if (Config.MAGIC_CREATE_DOMAIN == null + && this.xmppConnectionService.getAccounts().size() == 0) { this.binding.cancelButton.setEnabled(false); } if (mUsernameMode) { this.binding.accountJidLayout.setHint(getString(R.string.username_hint)); } else { - final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, - R.layout.item_autocomplete, - xmppConnectionService.getKnownHosts()); + final KnownHostsAdapter mKnownHostsAdapter = + new KnownHostsAdapter( + this, + R.layout.item_autocomplete, + xmppConnectionService.getKnownHosts()); this.binding.accountJid.setAdapter(mKnownHostsAdapter); } @@ -952,7 +1080,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat shareLink(false); break; case R.id.action_change_password_on_server: - gotoChangePassword(null); + gotoChangePassword(); break; case R.id.action_delete_account: deleteAccount(); @@ -971,13 +1099,18 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } private void deleteAccount() { - this.deleteAccount(mAccount,()->{ - finish(); - }); + this.deleteAccount( + mAccount, + () -> { + finish(); + }); } private boolean inNeedOfSaslAccept() { - return mAccount != null && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK && mAccount.getPinnedMechanismPriority() >= 0 && !accountInfoEdited(); + return mAccount != null + && mAccount.getLastErrorStatus() == Account.State.DOWNGRADE_ATTACK + && mAccount.getPinnedMechanismPriority() >= 0 + && !accountInfoEdited(); } private void shareBarcode() { @@ -988,12 +1121,12 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat startActivity(Intent.createChooser(intent, getText(R.string.share_with))); } - private void changeMoreTableVisibility(boolean visible) { + private void changeMoreTableVisibility(final boolean visible) { binding.serverInfoMore.setVisibility(visible ? View.VISIBLE : View.GONE); + binding.serverInfoLoginMechanism.setVisibility(visible ? View.VISIBLE : View.GONE); } - private void gotoChangePassword(String newPassword) { - this.newPassword = newPassword; + private void gotoChangePassword() { KeyguardManager keyguardManager = (KeyguardManager) this.getSystemService(Context.KEYGUARD_SERVICE); Intent credentialsIntent = keyguardManager.createConfirmDeviceCredentialIntent("Unlock required", "Please unlock in order to change your password"); if (credentialsIntent == null) { @@ -1007,10 +1140,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); changePasswordIntent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toEscapedString()); changePasswordIntent.putExtra("did_unlock", didUnlock); - if (newPassword != null) { - changePasswordIntent.putExtra("password", newPassword); - } - this.newPassword = null; startActivity(changePasswordIntent); } @@ -1020,9 +1149,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private void changePresence() { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - boolean manualStatus = sharedPreferences.getBoolean(AppSettings.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence)); + boolean manualStatus = + sharedPreferences.getBoolean( + AppSettings.MANUALLY_CHANGE_PRESENCE, + getResources().getBoolean(R.bool.manually_change_presence)); final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - final DialogPresenceBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false); + final DialogPresenceBinding binding = + DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_presence, null, false); String current = mAccount.getPresenceStatusMessage(); if (current != null && !current.trim().isEmpty()) { binding.statusMessage.append(current); @@ -1030,47 +1163,66 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat setAvailabilityRadioButton(mAccount.getPresenceStatus(), binding); binding.show.setVisibility(manualStatus ? View.VISIBLE : View.GONE); List templates = xmppConnectionService.getPresenceTemplates(mAccount); - PresenceTemplateAdapter presenceTemplateAdapter = new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates); + PresenceTemplateAdapter presenceTemplateAdapter = + new PresenceTemplateAdapter(this, R.layout.item_autocomplete, templates); binding.statusMessage.setAdapter(presenceTemplateAdapter); - binding.statusMessage.setOnItemClickListener((parent, view, position, id) -> { - PresenceTemplate template = (PresenceTemplate) parent.getItemAtPosition(position); - setAvailabilityRadioButton(template.getStatus(), binding); - }); + binding.statusMessage.setOnItemClickListener( + (parent, view, position, id) -> { + PresenceTemplate template = + (PresenceTemplate) parent.getItemAtPosition(position); + setAvailabilityRadioButton(template.getStatus(), binding); + }); builder.setTitle(R.string.edit_status_message_title); builder.setView(binding.getRoot()); builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - PresenceTemplate template = new PresenceTemplate(getAvailabilityRadioButton(binding), binding.statusMessage.getText().toString().trim()); - if (mAccount.getPgpId() != 0 && hasPgp()) { - generateSignature(null, template); - } else { - xmppConnectionService.changeStatus(mAccount, template, null); - } - }); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + PresenceTemplate template = + new PresenceTemplate( + getAvailabilityRadioButton(binding), + binding.statusMessage.getText().toString().trim()); + if (mAccount.getPgpId() != 0 && hasPgp()) { + generateSignature(null, template); + } else { + xmppConnectionService.changeStatus(mAccount, template, null); + } + }); builder.create().show(); } private void generateSignature(Intent intent, PresenceTemplate template) { - xmppConnectionService.getPgpEngine().generateSignature(intent, mAccount, template.getStatusMessage(), new UiCallback() { - @Override - public void success(String signature) { - xmppConnectionService.changeStatus(mAccount, template, signature); - } - - @Override - public void error(int errorCode, String object) { - - } - - @Override - public void userInputRequired(PendingIntent pi, String object) { - mPendingPresenceTemplate.push(template); - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_CHANGE_STATUS, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); - } catch (final IntentSender.SendIntentException ignored) { - } - } - }); + xmppConnectionService + .getPgpEngine() + .generateSignature( + intent, + mAccount, + template.getStatusMessage(), + new UiCallback() { + @Override + public void success(String signature) { + xmppConnectionService.changeStatus(mAccount, template, signature); + } + + @Override + public void error(int errorCode, String object) {} + + @Override + public void userInputRequired(PendingIntent pi, String object) { + mPendingPresenceTemplate.push(template); + try { + startIntentSenderForResult( + pi.getIntentSender(), + REQUEST_CHANGE_STATUS, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); + } catch (final IntentSender.SendIntentException ignored) { + } + } + }); } @Override @@ -1104,9 +1256,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (init) { this.binding.accountJid.getEditableText().clear(); if (mUsernameMode) { - this.binding.accountJid.getEditableText().append(this.mAccount.getJid().getEscapedLocal()); + this.binding + .accountJid + .getEditableText() + .append(this.mAccount.getJid().getEscapedLocal()); } else { - this.binding.accountJid.getEditableText().append(this.mAccount.getJid().asBareJid().toEscapedString()); + this.binding + .accountJid + .getEditableText() + .append(this.mAccount.getJid().asBareJid().toEscapedString()); } this.binding.accountPassword.getEditableText().clear(); this.binding.accountPassword.getEditableText().append(this.mAccount.getPassword()); @@ -1115,7 +1273,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.port.setText(""); this.binding.port.getEditableText().append(String.valueOf(this.mAccount.getPort())); this.binding.namePort.setVisibility(mShowOptions ? View.VISIBLE : View.GONE); - } final var preferences = getPreferences(); @@ -1143,13 +1300,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); } - final boolean editable = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME) && QuickConversationsService.isConversations(); + final boolean editable = + !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) + && !mAccount.isOptionSet(Account.OPTION_FIXED_USERNAME) + && QuickConversationsService.isConversations(); this.binding.accountJid.setEnabled(editable); this.binding.accountJid.setFocusable(editable); this.binding.accountJid.setFocusableInTouchMode(editable); this.binding.accountJid.setCursorVisible(editable); - final String displayName = mAccount.getDisplayName(); updateDisplayName(displayName); @@ -1162,8 +1321,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat binding.quietHoursBox.setVisibility(View.GONE); } - final boolean togglePassword = mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); - final boolean editPassword = !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) || mAccount.getLastErrorStatus() == Account.State.UNAUTHORIZED; + final boolean togglePassword = + mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) + || !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); + final boolean neverLoggedIn = + !mAccount.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY) + && QuickConversationsService.isConversations(); + final boolean editPassword = mAccount.unauthorized() || neverLoggedIn; this.binding.accountPasswordLayout.setPasswordVisibilityToggleEnabled(togglePassword); @@ -1174,11 +1338,13 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (!mInitMode) { this.binding.avater.setVisibility(View.VISIBLE); - AvatarWorkerTask.loadAvatar(mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar( + mAccount, binding.avater, R.dimen.avatar_on_details_screen_size); } else { this.binding.avater.setVisibility(View.GONE); } - this.binding.accountRegisterNew.setChecked(this.mAccount.isOptionSet(Account.OPTION_REGISTER)); + this.binding.accountRegisterNew.setChecked( + this.mAccount.isOptionSet(Account.OPTION_REGISTER)); if (this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) { if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { ActionBar actionBar = getSupportActionBar(); @@ -1193,13 +1359,14 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.accountRegisterNew.setVisibility(View.GONE); } if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { - Features features = this.mAccount.getXmppConnection().getFeatures(); + final Features features = this.mAccount.getXmppConnection().getFeatures(); this.binding.stats.setVisibility(View.VISIBLE); boolean showBatteryWarning = isOptimizingBattery(); boolean showDataSaverWarning = isAffectedByDataSaver(); showOsOptimizationWarning(showBatteryWarning, showDataSaverWarning); - this.binding.sessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() - .getLastSessionEstablished())); + this.binding.sessionEst.setText( + UIHelper.readableTimeDifferenceFull( + this, this.mAccount.getXmppConnection().getLastSessionEstablished())); if (features.rosterVersioning()) { this.binding.serverInfoRosterVersion.setText(R.string.server_info_available); } else { @@ -1235,6 +1402,17 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { this.binding.serverInfoExternalService.setText(R.string.server_info_unavailable); } + if (features.bind2()) { + this.binding.serverInfoBind2.setText(R.string.server_info_available); + } else { + this.binding.serverInfoBind2.setText(R.string.server_info_unavailable); + } + if (features.sasl2()) { + this.binding.serverInfoSasl2.setText(R.string.server_info_available); + } else { + this.binding.serverInfoSasl2.setText(R.string.server_info_unavailable); + } + this.binding.loginMechanism.setText(Strings.nullToEmpty(features.loginMechanism())); if (features.pep()) { AxolotlService axolotlService = this.mAccount.getAxolotlService(); if (axolotlService != null && axolotlService.isPepBroken()) { @@ -1250,7 +1428,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (features.httpUpload(0)) { final long maxFileSize = features.getMaxHttpUploadSize(); if (maxFileSize > 0) { - this.binding.serverInfoHttpUpload.setText(UIHelper.filesizeToString(maxFileSize)); + this.binding.serverInfoHttpUpload.setText( + UIHelper.filesizeToString(maxFileSize)); } else { this.binding.serverInfoHttpUpload.setText(R.string.server_info_available); } @@ -1258,7 +1437,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.serverInfoHttpUpload.setText(R.string.server_info_unavailable); } - this.binding.pushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE); + this.binding.pushRow.setVisibility( + xmppConnectionService.getPushManagementService().isStub() + ? View.GONE + : View.VISIBLE); if (xmppConnectionService.getPushManagementService().available(mAccount)) { this.binding.serverInfoPush.setText(R.string.server_info_available); @@ -1273,24 +1455,36 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat this.binding.pgpFingerprint.setText(OpenPgpUtils.convertKeyIdToHex(pgpKeyId)); this.binding.pgpFingerprint.setOnClickListener(openPgp); if ("pgp".equals(messageFingerprint)) { - this.binding.pgpFingerprintDesc.setTextColor(MaterialColors.getColor(binding.pgpFingerprintDesc, com.google.android.material.R.attr.colorPrimaryVariant)); + this.binding.pgpFingerprintDesc.setTextColor( + MaterialColors.getColor( + binding.pgpFingerprintDesc, + com.google.android.material.R.attr.colorPrimaryVariant)); } this.binding.pgpFingerprintDesc.setOnClickListener(openPgp); this.binding.actionDeletePgp.setOnClickListener(delete); } else { this.binding.pgpFingerprintBox.setVisibility(View.GONE); } - final String ownAxolotlFingerprint = this.mAccount.getAxolotlService().getOwnFingerprint(); + final String ownAxolotlFingerprint = + this.mAccount.getAxolotlService().getOwnFingerprint(); if (ownAxolotlFingerprint != null && Config.supportOmemo()) { this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE); if (ownAxolotlFingerprint.equals(messageFingerprint)) { - this.binding.ownFingerprintDesc.setTextColor(MaterialColors.getColor(binding.ownFingerprintDesc, com.google.android.material.R.attr.colorPrimaryVariant)); - this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint_selected_message); + this.binding.ownFingerprintDesc.setTextColor( + MaterialColors.getColor( + binding.ownFingerprintDesc, + com.google.android.material.R.attr.colorPrimaryVariant)); + this.binding.ownFingerprintDesc.setText( + R.string.omemo_fingerprint_selected_message); } else { - this.binding.ownFingerprintDesc.setTextColor(MaterialColors.getColor(binding.ownFingerprintDesc, com.google.android.material.R.attr.colorOnSurface)); + this.binding.ownFingerprintDesc.setTextColor( + MaterialColors.getColor( + binding.ownFingerprintDesc, + com.google.android.material.R.attr.colorOnSurface)); this.binding.ownFingerprintDesc.setText(R.string.omemo_fingerprint); } - this.binding.axolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2))); + this.binding.axolotlFingerprint.setText( + CryptoHelper.prettifyFingerprint(ownAxolotlFingerprint.substring(2))); this.binding.showQrCodeButton.setVisibility(View.VISIBLE); this.binding.showQrCodeButton.setOnClickListener(v -> showQrCode()); } else { @@ -1299,7 +1493,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat boolean hasKeys = false; boolean showUnverifiedWarning = false; binding.otherDeviceKeys.removeAllViews(); - for (final XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) { + for (final XmppAxolotlSession session : + mAccount.getAxolotlService().findOwnSessions()) { final FingerprintStatus trust = session.getTrust(); if (!trust.isCompromised()) { boolean highlight = session.getFingerprint().equals(messageFingerprint); @@ -1310,7 +1505,10 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat showUnverifiedWarning = true; } } - if (hasKeys && Config.supportOmemo()) { //TODO: either the button should be visible if we print an active device or the device list should be fed with reactived devices + if (hasKeys + && Config.supportOmemo()) { // TODO: either the button should be visible if we + // print an active device or the device list should + // be fed with reactived devices this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE); Set otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); if (otherDevices == null || otherDevices.isEmpty()) { @@ -1318,7 +1516,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { binding.clearDevices.setVisibility(View.VISIBLE); } - binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); + binding.unverifiedWarning.setVisibility( + showUnverifiedWarning ? View.VISIBLE : View.GONE); binding.scanButton.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); } else { this.binding.otherDeviceKeysCard.setVisibility(View.GONE); @@ -1339,7 +1538,8 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } else { final TextInputLayout errorLayout; if (this.mAccount.errorStatus()) { - if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED || this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) { + if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED + || this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) { errorLayout = this.binding.accountPasswordLayout; } else if (mShowOptions && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND @@ -1365,10 +1565,16 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat private void updateDisplayName(String displayName) { if (TextUtils.isEmpty(displayName)) { this.binding.yourName.setText(R.string.no_name_set_instructions); - this.binding.yourName.setTextColor(MaterialColors.getColor(binding.yourName, com.google.android.material.R.attr.colorOnSurfaceVariant)); + this.binding.yourName.setTextColor( + MaterialColors.getColor( + binding.yourName, + com.google.android.material.R.attr.colorOnSurfaceVariant)); } else { this.binding.yourName.setText(displayName); - this.binding.yourName.setTextColor(MaterialColors.getColor(binding.yourName, com.google.android.material.R.attr.colorOnSurfaceVariant)); + this.binding.yourName.setTextColor( + MaterialColors.getColor( + binding.yourName, + com.google.android.material.R.attr.colorOnSurfaceVariant)); } } @@ -1396,46 +1602,71 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat builder.setTitle(R.string.unpublish_pgp); builder.setMessage(R.string.unpublish_pgp_message); builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.confirm, (dialogInterface, i) -> { - mAccount.setPgpSignId(0); - mAccount.unsetPgpSignature(); - xmppConnectionService.databaseBackend.updateAccount(mAccount); - xmppConnectionService.sendPresence(mAccount); - refreshUiReal(); - }); + builder.setPositiveButton( + R.string.confirm, + (dialogInterface, i) -> { + mAccount.setPgpSignId(0); + mAccount.unsetPgpSignature(); + xmppConnectionService.databaseBackend.updateAccount(mAccount); + xmppConnectionService.sendPresence(mAccount); + refreshUiReal(); + }); builder.create().show(); } - private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) { - this.binding.osOptimization.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE); - if (showDataSaverWarning && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + private void showOsOptimizationWarning( + boolean showBatteryWarning, boolean showDataSaverWarning) { + this.binding.osOptimization.setVisibility( + showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE); + if (showDataSaverWarning + && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { this.binding.osOptimizationHeadline.setText(R.string.data_saver_enabled); - this.binding.osOptimizationBody.setText(getString(R.string.data_saver_enabled_explained, getString(R.string.app_name))); + this.binding.osOptimizationBody.setText( + getString(R.string.data_saver_enabled_explained, getString(R.string.app_name))); this.binding.osOptimizationDisable.setText(R.string.allow); - this.binding.osOptimizationDisable.setOnClickListener(v -> { - Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_DATA_SAVER); - } catch (ActivityNotFoundException e) { - Toast.makeText(EditAccountActivity.this, getString(R.string.device_does_not_support_data_saver, getString(R.string.app_name)), Toast.LENGTH_SHORT).show(); - } - }); + this.binding.osOptimizationDisable.setOnClickListener( + v -> { + Intent intent = + new Intent( + Settings + .ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS); + Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + try { + startActivityForResult(intent, REQUEST_DATA_SAVER); + } catch (ActivityNotFoundException e) { + Toast.makeText( + EditAccountActivity.this, + getString( + R.string.device_does_not_support_data_saver, + getString(R.string.app_name)), + Toast.LENGTH_SHORT) + .show(); + } + }); } else if (showBatteryWarning) { this.binding.osOptimizationDisable.setText(R.string.disable); this.binding.osOptimizationHeadline.setText(R.string.battery_optimizations_enabled); - this.binding.osOptimizationBody.setText(getString(R.string.battery_optimizations_enabled_explained, getString(R.string.app_name))); - this.binding.osOptimizationDisable.setOnClickListener(v -> { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (ActivityNotFoundException e) { - Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show(); - } - }); + this.binding.osOptimizationBody.setText( + getString( + R.string.battery_optimizations_enabled_explained, + getString(R.string.app_name))); + this.binding.osOptimizationDisable.setOnClickListener( + v -> { + Intent intent = + new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + try { + startActivityForResult(intent, REQUEST_BATTERY_OP); + } catch (ActivityNotFoundException e) { + Toast.makeText( + EditAccountActivity.this, + R.string.device_does_not_support_battery_op, + Toast.LENGTH_SHORT) + .show(); + } + }); } } @@ -1445,13 +1676,15 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setMessage(getString(R.string.clear_other_devices_desc)); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.accept), + builder.setPositiveButton( + getString(R.string.accept), (dialog, which) -> mAccount.getAxolotlService().wipeOtherPepDevices()); builder.create().show(); } private void editMamPrefs() { - this.mFetchingMamPrefsToast = Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG); + this.mFetchingMamPrefsToast = + Toast.makeText(this, R.string.fetching_mam_prefs, Toast.LENGTH_LONG); this.mFetchingMamPrefsToast.show(); xmppConnectionService.fetchMamPreferences(mAccount, this); } @@ -1462,86 +1695,110 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat } @Override - public void onCaptchaRequested(final Account account, final String id, final Data data, final Bitmap captcha) { - runOnUiThread(() -> { - if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) { - mCaptchaDialog.dismiss(); - } - if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { - Log.d(Config.LOGTAG,"activity not running when captcha was requested"); - return; - } - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(EditAccountActivity.this); - final View view = getLayoutInflater().inflate(R.layout.captcha, null); - final ImageView imageView = view.findViewById(R.id.captcha); - final EditText input = view.findViewById(R.id.input); - imageView.setImageBitmap(captcha); - - builder.setTitle(getString(R.string.captcha_required)); - builder.setView(view); - - builder.setPositiveButton(getString(R.string.ok), - (dialog, which) -> { - String rc = input.getText().toString(); - data.put("username", account.getUsername()); - data.put("password", account.getPassword()); - data.put("ocr", rc); - data.submit(); - - if (xmppConnectionServiceBound) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, id, data); - } - }); - builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null); - } - }); - - builder.setOnCancelListener(dialog -> { - if (xmppConnectionService != null) { - xmppConnectionService.sendCreateAccountWithCaptchaPacket(account, null, null); - } - }); - mCaptchaDialog = builder.create(); - mCaptchaDialog.show(); - input.requestFocus(); - }); + public void onCaptchaRequested( + final Account account, final String id, final Data data, final Bitmap captcha) { + runOnUiThread( + () -> { + if (mCaptchaDialog != null && mCaptchaDialog.isShowing()) { + mCaptchaDialog.dismiss(); + } + if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + Log.d(Config.LOGTAG, "activity not running when captcha was requested"); + return; + } + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(EditAccountActivity.this); + final View view = getLayoutInflater().inflate(R.layout.captcha, null); + final ImageView imageView = view.findViewById(R.id.captcha); + final EditText input = view.findViewById(R.id.input); + imageView.setImageBitmap(captcha); + + builder.setTitle(getString(R.string.captcha_required)); + builder.setView(view); + + builder.setPositiveButton( + getString(R.string.ok), + (dialog, which) -> { + String rc = input.getText().toString(); + data.put("username", account.getUsername()); + data.put("password", account.getPassword()); + data.put("ocr", rc); + data.submit(); + + if (xmppConnectionServiceBound) { + xmppConnectionService.sendCreateAccountWithCaptchaPacket( + account, id, data); + } + }); + builder.setNegativeButton( + getString(R.string.cancel), + (dialog, which) -> { + if (xmppConnectionService != null) { + xmppConnectionService.sendCreateAccountWithCaptchaPacket( + account, null, null); + } + }); + + builder.setOnCancelListener( + dialog -> { + if (xmppConnectionService != null) { + xmppConnectionService.sendCreateAccountWithCaptchaPacket( + account, null, null); + } + }); + mCaptchaDialog = builder.create(); + mCaptchaDialog.show(); + input.requestFocus(); + }); } public void onShowErrorToast(final int resId) { - runOnUiThread(() -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show()); + runOnUiThread( + () -> Toast.makeText(EditAccountActivity.this, resId, Toast.LENGTH_SHORT).show()); } @Override public void onPreferencesFetched(final Element prefs) { - runOnUiThread(() -> { - if (mFetchingMamPrefsToast != null) { - mFetchingMamPrefsToast.cancel(); - } - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(EditAccountActivity.this); - builder.setTitle(R.string.server_side_mam_prefs); - String defaultAttr = prefs.getAttribute("default"); - final List defaults = Arrays.asList("never", "roster", "always"); - final AtomicInteger choice = new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr))); - builder.setSingleChoiceItems(R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - prefs.setAttribute("default", defaults.get(choice.get())); - xmppConnectionService.pushMamPreferences(mAccount, prefs); - }); - builder.create().show(); - }); + runOnUiThread( + () -> { + if (mFetchingMamPrefsToast != null) { + mFetchingMamPrefsToast.cancel(); + } + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(EditAccountActivity.this); + builder.setTitle(R.string.server_side_mam_prefs); + String defaultAttr = prefs.getAttribute("default"); + final List defaults = Arrays.asList("never", "roster", "always"); + final AtomicInteger choice = + new AtomicInteger(Math.max(0, defaults.indexOf(defaultAttr))); + builder.setSingleChoiceItems( + R.array.mam_prefs, choice.get(), (dialog, which) -> choice.set(which)); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton( + R.string.ok, + (dialog, which) -> { + prefs.setAttribute("default", defaults.get(choice.get())); + xmppConnectionService.pushMamPreferences(mAccount, prefs); + }); + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + builder.create().show(); + } + }); } @Override public void onPreferencesFetchFailed() { - runOnUiThread(() -> { - if (mFetchingMamPrefsToast != null) { - mFetchingMamPrefsToast.cancel(); - } - Toast.makeText(EditAccountActivity.this, R.string.unable_to_fetch_mam_prefs, Toast.LENGTH_LONG).show(); - }); + runOnUiThread( + () -> { + if (mFetchingMamPrefsToast != null) { + mFetchingMamPrefsToast.cancel(); + } + Toast.makeText( + EditAccountActivity.this, + R.string.unable_to_fetch_mam_prefs, + Toast.LENGTH_LONG) + .show(); + }); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java index fdca161d8db00853c874936971ed4922f67659bc..2660d72948f0558e4667b80838fad508f0062c91 100644 --- a/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RecordingActivity.java @@ -111,8 +111,13 @@ public class RecordingActivity extends BaseActivity implements View.OnClickListe private boolean startRecording() { mRecorder = new MediaRecorder(); - mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); final String userChosenCodec = getPreferences().getString("voice_message_codec", ""); + try { + mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + } catch (final RuntimeException e) { + Log.e(Config.LOGTAG,"could not set audio source", e); + return false; + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { mRecorder.setPrivacySensitive(true); } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 19a7074601162a0c1c545f2b0a3124c23b099ddd..0ef3625fdba7d98174a9308d7d5423416e632390 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -34,6 +34,7 @@ import androidx.databinding.DataBindingUtil; import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -363,10 +364,15 @@ public class RtpSessionActivity extends XmppActivity private void acceptContentAdd() { try { - requireRtpConnection() - .acceptContentAdd(requireRtpConnection().getPendingContentAddition().summary); + final ContentAddition pendingContentAddition = + requireRtpConnection().getPendingContentAddition(); + if (pendingContentAddition == null) { + Log.d(Config.LOGTAG, "content offer was gone after granting permission"); + return; + } + requireRtpConnection().acceptContentAdd(pendingContentAddition.summary); } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, Strings.nullToEmpty(e.getMessage()), Toast.LENGTH_SHORT).show(); } } @@ -537,7 +543,12 @@ public class RtpSessionActivity extends XmppActivity final String action = intent.getAction(); Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")"); final Account account = extractAccount(intent); - final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH)); + final var extraWith = intent.getStringExtra(EXTRA_WITH); + final Jid with = Strings.isNullOrEmpty(extraWith) ? null : Jid.ofEscaped(extraWith); + if (with == null || account == null) { + Log.e(Config.LOGTAG, "intent is missing extras (account or with)"); + return; + } final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID); if (sessionId != null) { if (initializeActivityWithRunningRtpSession(account, with, sessionId)) { @@ -1089,16 +1100,21 @@ public class RtpSessionActivity extends XmppActivity final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) { switch (selectedAudioDevice) { case EARPIECE -> { - this.binding.inCallActionRight.setImageResource( - R.drawable.ic_volume_off_24dp); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_24dp); if (numberOfChoices >= 2) { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_earpiece_tap_to_switch_to_speaker)); this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker); } else { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_earpiece)); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } } case WIRED_HEADSET -> { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_wired_headset)); this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); @@ -1106,15 +1122,20 @@ public class RtpSessionActivity extends XmppActivity case SPEAKER_PHONE -> { this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp); if (numberOfChoices >= 2) { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_speaker_tap_to_switch_to_earpiece)); this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece); } else { + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_speaker)); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } } case BLUETOOTH -> { - this.binding.inCallActionRight.setImageResource( - R.drawable.ic_bluetooth_audio_24dp); + this.binding.inCallActionRight.setContentDescription( + getString(R.string.call_is_using_bluetooth)); + this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_24dp); this.binding.inCallActionRight.setOnClickListener(null); this.binding.inCallActionRight.setClickable(false); } @@ -1131,15 +1152,21 @@ public class RtpSessionActivity extends XmppActivity R.drawable.ic_flip_camera_android_24dp); this.binding.inCallActionFarRight.setVisibility(View.VISIBLE); this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera); + this.binding.inCallActionFarRight.setContentDescription( + getString(R.string.flip_camera)); } else { this.binding.inCallActionFarRight.setVisibility(View.GONE); } if (videoEnabled) { this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp); this.binding.inCallActionRight.setOnClickListener(this::disableVideo); + this.binding.inCallActionRight.setContentDescription( + getString(R.string.video_is_enabled_tap_to_disable)); } else { this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp); this.binding.inCallActionRight.setOnClickListener(this::enableVideo); + this.binding.inCallActionRight.setContentDescription( + getString(R.string.video_is_disabled_tap_to_enable)); } } @@ -1168,7 +1195,7 @@ public class RtpSessionActivity extends XmppActivity MainThreadExecutor.getInstance()); } - private void enableVideo(View view) { + private void enableVideo(final View view) { try { requireRtpConnection().setVideoEnabled(true); } catch (final IllegalStateException e) { @@ -1178,14 +1205,19 @@ public class RtpSessionActivity extends XmppActivity updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable()); } - private void disableVideo(View view) { + private void disableVideo(final View view) { final JingleRtpConnection rtpConnection = requireRtpConnection(); final ContentAddition pending = rtpConnection.getPendingContentAddition(); if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) { rtpConnection.retractContentAdd(); return; } - requireRtpConnection().setVideoEnabled(false); + try { + requireRtpConnection().setVideoEnabled(false); + } catch (final IllegalStateException e) { + Toast.makeText(this, R.string.could_not_disable_video, Toast.LENGTH_SHORT).show(); + return; + } updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable()); } @@ -1326,13 +1358,21 @@ public class RtpSessionActivity extends XmppActivity } private void switchToEarpiece(final View view) { - requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE); - acquireProximityWakeLock(); + try { + requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE); + acquireProximityWakeLock(); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } } private void switchToSpeaker(final View view) { - requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); - releaseProximityWakeLock(); + try { + requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); + releaseProximityWakeLock(); + } catch (final IllegalStateException e) { + Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + } } private void retry(final View view) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 6df65ba8a9409759c3f8a01bd860af92ad203251..efd32b162ca22b8828588ffc012d2a3b32238756 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -36,7 +36,6 @@ import android.widget.AutoCompleteTextView; import android.widget.CheckBox; import android.widget.EditText; import android.widget.ListView; -import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -47,7 +46,7 @@ import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; -import androidx.core.content.ContextCompat; +import androidx.core.app.ActivityCompat; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; @@ -63,21 +62,11 @@ import com.cheogram.android.FinishOnboarding; import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.textfield.TextInputLayout; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.leinardi.android.speeddial.SpeedDialActionItem; import com.leinardi.android.speeddial.SpeedDialView; -import java.util.Arrays; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; - import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -109,11 +98,29 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener { +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class StartConversationActivity extends XmppActivity + implements XmppConnectionService.OnConversationUpdate, + OnRosterUpdate, + OnUpdateBlocklist, + CreatePrivateGroupChatDialog.CreateConferenceDialogListener, + JoinConferenceDialog.JoinConferenceDialogListener, + SwipeRefreshLayout.OnRefreshListener, + CreatePublicChannelDialog.CreatePublicChannelDialogListener { - private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent"; + private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = + "contact_list_integration_consent"; public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri"; @@ -135,126 +142,138 @@ public class StartConversationActivity extends XmppActivity implements XmppConne private final AtomicBoolean mOpenedFab = new AtomicBoolean(false); private boolean mHideOfflineContacts = false; private boolean createdByViewIntent = false; - private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - mSearchEditText.post(() -> { - updateSearchViewHint(); - mSearchEditText.requestFocus(); - if (oneShotKeyboardSuppress.compareAndSet(true, false)) { - return; - } - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - if (imm != null) { - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); + private final MenuItem.OnActionExpandListener mOnActionExpandListener = + new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(@NonNull final MenuItem item) { + mSearchEditText.post( + () -> { + updateSearchViewHint(); + mSearchEditText.requestFocus(); + if (oneShotKeyboardSuppress.compareAndSet(true, false)) { + return; + } + InputMethodManager imm = + (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput( + mSearchEditText, InputMethodManager.SHOW_IMPLICIT); + } + }); + if (binding.speedDial.isOpen()) { + binding.speedDial.close(); + } + return true; } - }); - if (binding.speedDial.isOpen()) { - binding.speedDial.close(); - } - return true; - } - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); - mSearchEditText.setText(""); - filter(null); - navigateBack(); - return true; - } - }; - private final TextWatcher mSearchTextWatcher = new TextWatcher() { + @Override + public boolean onMenuItemActionCollapse(@NonNull final MenuItem item) { + SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); + mSearchEditText.setText(""); + filter(null); + navigateBack(); + return true; + } + }; + private final TextWatcher mSearchTextWatcher = + new TextWatcher() { - @Override - public void afterTextChanged(Editable editable) { - filter(editable.toString()); - } + @Override + public void afterTextChanged(Editable editable) { + filter(editable.toString()); + } - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - } - }; + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + }; private MenuItem mMenuSearchView; - private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { - @Override - public void onTagClicked(String tag) { - if (mMenuSearchView != null) { - mMenuSearchView.expandActionView(); - mSearchEditText.setText(""); - mSearchEditText.append(tag); - filter(tag); - } - } - }; + private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = + new ListItemAdapter.OnTagClickedListener() { + @Override + public void onTagClicked(String tag) { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(""); + mSearchEditText.append(tag); + filter(tag); + } + } + }; private Pair mPostponedActivityResult; private Toast mToast; - private final UiCallback mAdhocConferenceCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - runOnUiThread(() -> { - hideToast(); - switchToConversation(conversation); - }); - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(() -> replaceToast(getString(errorCode))); - } + private final UiCallback mAdhocConferenceCallback = + new UiCallback<>() { + @Override + public void success(final Conversation conversation) { + runOnUiThread( + () -> { + hideToast(); + switchToConversation(conversation); + }); + } - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { + @Override + public void error(final int errorCode, Conversation object) { + runOnUiThread(() -> replaceToast(getString(errorCode))); + } - } - }; + @Override + public void userInputRequired(PendingIntent pi, Conversation object) {} + }; private ActivityStartConversationBinding binding; - private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() { - @Override - public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { - int pos = binding.startConversationViewPager.getCurrentItem(); - if (pos == 0) { - if (contacts.size() == 1) { - openConversation(contacts.get(0)); - return true; - } else if (contacts.size() == 0 && conferences.size() == 1) { - openConversationsForBookmark((Bookmark) conferences.get(0)); - return true; - } - } else { - if (conferences.size() == 1) { - openConversationsForBookmark((Bookmark) conferences.get(0)); - return true; - } else if (conferences.size() == 0 && contacts.size() == 1) { - openConversation(contacts.get(0)); + private final TextView.OnEditorActionListener mSearchDone = + new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + int pos = binding.startConversationViewPager.getCurrentItem(); + if (pos == 0) { + if (contacts.size() == 1) { + openConversation(contacts.get(0)); + return true; + } else if (contacts.isEmpty() && conferences.size() == 1) { + openConversationsForBookmark((Bookmark) conferences.get(0)); + return true; + } + } else { + if (conferences.size() == 1) { + openConversationsForBookmark((Bookmark) conferences.get(0)); + return true; + } else if (conferences.isEmpty() && contacts.size() == 1) { + openConversation(contacts.get(0)); + return true; + } + } + SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); + mListPagerAdapter.requestFocus(pos); return true; } - } - SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this); - mListPagerAdapter.requestFocus(pos); - return true; - } - }; + }; - public static void populateAccountSpinner(final Context context, final List accounts, final AutoCompleteTextView spinner) { + public static void populateAccountSpinner( + final Context context, + final List accounts, + final AutoCompleteTextView spinner) { if (accounts.isEmpty()) { - ArrayAdapter adapter = new ArrayAdapter<>(context, - R.layout.item_autocomplete, - Collections.singletonList(context.getString(R.string.no_accounts))); + ArrayAdapter adapter = + new ArrayAdapter<>( + context, + R.layout.item_autocomplete, + Collections.singletonList(context.getString(R.string.no_accounts))); adapter.setDropDownViewResource(R.layout.item_autocomplete); spinner.setAdapter(adapter); spinner.setEnabled(false); } else { - final ArrayAdapter adapter = new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts); + final ArrayAdapter adapter = + new ArrayAdapter<>(context, R.layout.item_autocomplete, accounts); adapter.setDropDownViewResource(R.layout.item_autocomplete); spinner.setAdapter(adapter); spinner.setEnabled(true); - spinner.setText(Iterables.getFirst(accounts,null),false); + spinner.setText(Iterables.getFirst(accounts, null), false); } } @@ -271,7 +290,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private static boolean isViewIntent(final Intent i) { - return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(EXTRA_INVITE_URI)); + return i != null + && (Intent.ACTION_VIEW.equals(i.getAction()) + || Intent.ACTION_SENDTO.equals(i.getAction()) + || i.hasExtra(EXTRA_INVITE_URI)); } protected void hideToast() { @@ -301,12 +323,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu); binding.tabLayout.setupWithViewPager(binding.startConversationViewPager); - binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { - @Override - public void onPageSelected(int position) { - updateSearchViewHint(); - } - }); + binding.startConversationViewPager.addOnPageChangeListener( + new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + updateSearchViewHint(); + } + }); mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager()); binding.startConversationViewPager.setAdapter(mListPagerAdapter); @@ -316,9 +339,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final SharedPreferences preferences = getPreferences(); - this.mHideOfflineContacts = QuickConversationsService.isConversations() && preferences.getBoolean("hide_offline", false); + this.mHideOfflineContacts = + QuickConversationsService.isConversations() + && preferences.getBoolean("hide_offline", false); - final boolean startSearching = preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching)); + final boolean startSearching = + preferences.getBoolean( + "start_searching", getResources().getBoolean(R.bool.start_searching)); final Intent intent; if (savedInstanceState == null) { @@ -343,36 +370,42 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } else if (startSearching && mInitialSearchValue.peek() == null) { mInitialSearchValue.push(""); } - mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false)); - mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false)); - binding.speedDial.setOnActionSelectedListener(actionItem -> { - final String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null; - final String prefilled; - if (isValidJid(searchString)) { - prefilled = Jid.ofEscaped(searchString).toEscapedString(); - } else { - prefilled = null; - } - switch (actionItem.getId()) { - case R.id.discover_public_channels: - if (QuickConversationsService.isPlayStoreFlavor()) { - throw new IllegalStateException("Channel discovery is not available on Google Play flavor"); + mRequestedContactsPermission.set( + savedInstanceState != null + && savedInstanceState.getBoolean("requested_contacts_permission", false)); + mOpenedFab.set( + savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false)); + binding.speedDial.setOnActionSelectedListener( + actionItem -> { + final String searchString = + mSearchEditText != null ? mSearchEditText.getText().toString() : null; + final String prefilled; + if (isValidJid(searchString)) { + prefilled = Jid.ofEscaped(searchString).toEscapedString(); } else { - startActivity(new Intent(this, ChannelDiscoveryActivity.class)); + prefilled = null; } - break; - case R.id.create_private_group_chat: - showCreatePrivateGroupChatDialog(); - break; - case R.id.create_public_channel: - showPublicChannelDialog(); - break; - case R.id.create_contact: - showCreateContactDialog(prefilled, null); - break; - } - return false; - }); + switch (actionItem.getId()) { + case R.id.discover_public_channels: + if (QuickConversationsService.isPlayStoreFlavor()) { + throw new IllegalStateException( + "Channel discovery is not available on Google Play flavor"); + } else { + startActivity(new Intent(this, ChannelDiscoveryActivity.class)); + } + break; + case R.id.create_private_group_chat: + showCreatePrivateGroupChatDialog(); + break; + case R.id.create_public_channel: + showPublicChannelDialog(); + break; + case R.id.create_contact: + showCreateContactDialog(prefilled, null); + break; + } + return false; + }); } private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) { @@ -382,16 +415,29 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final Menu menu = popupMenu.getMenu(); for (int i = 0; i < menu.size(); i++) { final MenuItem menuItem = menu.getItem(i); - if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) { + if (QuickConversationsService.isPlayStoreFlavor() + && menuItem.getItemId() == R.id.discover_public_channels) { continue; } - final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon()) - .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null) - .setFabImageTintColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorOnSurface)) - .setFabBackgroundColor(MaterialColors.getColor(speedDialView, com.google.android.material.R.attr.colorSurfaceContainerHighest)) - .create(); + final SpeedDialActionItem actionItem = + new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon()) + .setLabel( + menuItem.getTitle() != null + ? menuItem.getTitle().toString() + : null) + .setFabImageTintColor( + MaterialColors.getColor( + speedDialView, + com.google.android.material.R.attr.colorOnSurface)) + .setFabBackgroundColor( + MaterialColors.getColor( + speedDialView, + com.google.android.material.R.attr + .colorSurfaceContainerHighest)) + .create(); speedDialView.addActionItem(actionItem); } + speedDialView.setContentDescription(getString(R.string.add_contact_or_create_or_join_group_chat)); } public static boolean isValidJid(String input) { @@ -406,12 +452,16 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override public void onSaveInstanceState(Bundle savedInstanceState) { Intent pendingIntent = pendingViewIntent.peek(); - savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent()); - savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get()); + savedInstanceState.putParcelable( + "intent", pendingIntent != null ? pendingIntent : getIntent()); + savedInstanceState.putBoolean( + "requested_contacts_permission", mRequestedContactsPermission.get()); savedInstanceState.putBoolean("opened_fab", mOpenedFab.get()); savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent); if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) { - savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null); + savedInstanceState.putString( + "search", + mSearchEditText != null ? mSearchEditText.getText().toString() : null); } super.onSaveInstanceState(savedInstanceState); } @@ -419,11 +469,24 @@ public class StartConversationActivity extends XmppActivity implements XmppConne @Override public void onStart() { super.onStart(); - if (pendingViewIntent.peek() == null) { - askForContactsPermissions(); - } mConferenceAdapter.refreshSettings(); mContactsAdapter.refreshSettings(); + if (pendingViewIntent.peek() == null) { + if (askForContactsPermissions()) { + return; + } + requestNotificationPermissionIfNeeded(); + } + } + + private void requestNotificationPermissionIfNeeded() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] {Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_POST_NOTIFICATION); + } } @Override @@ -450,7 +513,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void openConversationForContact(Contact contact) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); SoftKeyboardUtils.hideSoftKeyboard(this); switchToConversation(conversation); } @@ -475,9 +540,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + Uri.encode(address, "@/+") + "?join"); shareIntent.setType("text/plain"); try { - context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with))); + context.startActivity( + Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with))); } catch (ActivityNotFoundException e) { - Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT) + .show(); } } @@ -487,7 +554,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show(); return; } - final Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true); + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + bookmark.getAccount(), jid, true, true, true); bookmark.setConversation(conversation); if (!bookmark.autojoin()) { bookmark.setAutojoin(true); @@ -514,11 +583,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.action_delete_contact); - builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString())); - builder.setPositiveButton(R.string.delete, (dialog, which) -> { - xmppConnectionService.deleteContactOnServer(contact); - filter(mSearchEditText.getText().toString()); - }); + builder.setMessage( + JidDialog.style( + this, R.string.remove_contact_text, contact.getJid().toEscapedString())); + builder.setPositiveButton( + R.string.delete, + (dialog, which) -> { + xmppConnectionService.deleteContactOnServer(contact); + filter(mSearchEditText.getText().toString()); + }); builder.create().show(); } @@ -530,21 +603,28 @@ public class StartConversationActivity extends XmppActivity implements XmppConne builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_bookmark); if (hasConversation) { - builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_and_close, bookmark.getJid().toEscapedString())); + builder.setMessage( + JidDialog.style( + this, + R.string.remove_bookmark_and_close, + bookmark.getJid().toEscapedString())); } else { - builder.setMessage(JidDialog.style(this, R.string.remove_bookmark, bookmark.getJid().toEscapedString())); - } - builder.setPositiveButton(hasConversation ? R.string.delete_and_close : R.string.delete, (dialog, which) -> { - bookmark.setConversation(null); - final Account account = bookmark.getAccount(); - xmppConnectionService.deleteBookmark(account, bookmark); - if (conversation != null) { - xmppConnectionService.archiveConversation(conversation); - } - filter(mSearchEditText.getText().toString()); - }); + builder.setMessage( + JidDialog.style( + this, R.string.remove_bookmark, bookmark.getJid().toEscapedString())); + } + builder.setPositiveButton( + hasConversation ? R.string.delete_and_close : R.string.delete, + (dialog, which) -> { + bookmark.setConversation(null); + final Account account = bookmark.getAccount(); + xmppConnectionService.deleteBookmark(account, bookmark); + if (conversation != null) { + xmppConnectionService.archiveConversation(conversation); + } + filter(mSearchEditText.getText().toString()); + }); builder.create().show(); - } @SuppressLint("InflateParams") @@ -641,7 +721,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.remove(prev); } ft.addToBackStack(null); - JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts); + JoinConferenceDialog joinConferenceFragment = + JoinConferenceDialog.newInstance(prefilledJid, invite.getParameter("password"), mActivatedAccounts); joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); } @@ -652,7 +733,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.remove(prev); } ft.addToBackStack(null); - CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts); + CreatePrivateGroupChatDialog createConferenceFragment = + CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts); createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG); } @@ -663,11 +745,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne ft.remove(prev); } ft.addToBackStack(null); - CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts); + CreatePublicChannelDialog dialog = + CreatePublicChannelDialog.newInstance(mActivatedAccounts); dialog.show(ft, FRAGMENT_TAG_DIALOG); } - public static Account getSelectedAccount(final Context context, final AutoCompleteTextView spinner) { + public static Account getSelectedAccount( + final Context context, final AutoCompleteTextView spinner) { if (spinner == null || !spinner.isEnabled()) { return null; } @@ -689,7 +773,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void switchToConversation(Contact contact) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); switchToConversation(conversation); } @@ -698,7 +784,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) { - Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true); + Conversation conversation = + xmppConnectionService.findOrCreateConversation( + contact.getAccount(), contact.getJid(), false, true); switchToConversation(conversation, body, false, null, false, true, postInit); } @@ -828,7 +916,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne this.mPostponedActivityResult = null; if (requestCode == REQUEST_CREATE_CONFERENCE) { Account account = extractAccount(intent); - final String name = intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME); + final String name = + intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME); final List jids = ChooseContactActivity.extractJabberIds(intent); if (account != null && jids.size() > 0) { // This hardcodes cheogram.com and is in general a terrible hack @@ -863,104 +952,109 @@ public class StartConversationActivity extends XmppActivity implements XmppConne super.onActivityResult(requestCode, requestCode, intent); } - private void askForContactsPermissions() { - if (QuickConversationsService.isContactListIntegration(this)) { - if (checkSelfPermission(Manifest.permission.READ_CONTACTS) - != PackageManager.PERMISSION_GRANTED) { - if (mRequestedContactsPermission.compareAndSet(false, true)) { - final String consent = - PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) - .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null); - final boolean requiresConsent = - (QuickConversationsService.isQuicksy() - || QuickConversationsService.isPlayStoreFlavor()) - && !"agreed".equals(consent); - if (requiresConsent && "declined".equals(consent)) { - Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined"); - return; - } - if (requiresConsent - || shouldShowRequestPermissionRationale( - Manifest.permission.READ_CONTACTS)) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - final AtomicBoolean requestPermission = new AtomicBoolean(false); - if (QuickConversationsService.isQuicksy()) { - builder.setTitle(R.string.quicksy_wants_your_consent); - builder.setMessage( - Html.fromHtml( - getString(R.string.sync_with_contacts_quicksy_static))); - } else { - builder.setTitle(R.string.sync_with_contacts); - builder.setMessage( - getString( - R.string.sync_with_contacts_long, - getString(R.string.app_name))); - } - @StringRes int confirmButtonText; - if (requiresConsent) { - confirmButtonText = R.string.agree_and_continue; - } else { - confirmButtonText = R.string.next; - } - builder.setPositiveButton( - confirmButtonText, - (dialog, which) -> { - if (requiresConsent) { - PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()) - .edit() - .putString( - PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed") - .apply(); - } - if (requestPermission.compareAndSet(false, true)) { - requestPermissions( - new String[] {Manifest.permission.READ_CONTACTS}, - REQUEST_SYNC_CONTACTS); - } - }); - if (requiresConsent) { - builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()) - .edit() - .putString( - PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined") - .apply()); - } else { - builder.setOnDismissListener( - dialog -> { - if (requestPermission.compareAndSet(false, true)) { - requestPermissions( - new String[] { - Manifest.permission.READ_CONTACTS - }, - REQUEST_SYNC_CONTACTS); - } - }); - } - builder.setCancelable(requiresConsent); - final AlertDialog dialog = builder.create(); - dialog.setCanceledOnTouchOutside(requiresConsent); - dialog.setOnShowListener( - dialogInterface -> { - final TextView tv = dialog.findViewById(android.R.id.message); - if (tv != null) { - tv.setMovementMethod(LinkMovementMethod.getInstance()); - } - }); - dialog.show(); - } else { - requestPermissions( - new String[] {Manifest.permission.READ_CONTACTS}, - REQUEST_SYNC_CONTACTS); - } + private boolean askForContactsPermissions() { + if (!QuickConversationsService.isContactListIntegration(this)) { + return false; + } + if (checkSelfPermission(Manifest.permission.READ_CONTACTS) + == PackageManager.PERMISSION_GRANTED) { + return false; + } + if (mRequestedContactsPermission.compareAndSet(false, true)) { + final ImmutableList.Builder permissionBuilder = new ImmutableList.Builder<>(); + permissionBuilder.add(Manifest.permission.READ_CONTACTS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionBuilder.add(Manifest.permission.POST_NOTIFICATIONS); + } + final String[] permission = permissionBuilder.build().toArray(new String[0]); + final String consent = + PreferenceManager.getDefaultSharedPreferences(getApplicationContext()) + .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null); + final boolean requiresConsent = + (QuickConversationsService.isQuicksy() + || QuickConversationsService.isPlayStoreFlavor()) + && !"agreed".equals(consent); + if (requiresConsent && "declined".equals(consent)) { + Log.d( + Config.LOGTAG, + "not asking for contacts permission because consent has been declined"); + return false; + } + if (requiresConsent + || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + final AtomicBoolean requestPermission = new AtomicBoolean(false); + if (QuickConversationsService.isQuicksy()) { + builder.setTitle(R.string.quicksy_wants_your_consent); + builder.setMessage( + Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static))); + } else { + builder.setTitle(R.string.sync_with_contacts); + builder.setMessage( + getString( + R.string.sync_with_contacts_long, + getString(R.string.app_name))); + } + @StringRes int confirmButtonText; + if (requiresConsent) { + confirmButtonText = R.string.agree_and_continue; + } else { + confirmButtonText = R.string.next; + } + builder.setPositiveButton( + confirmButtonText, + (dialog, which) -> { + if (requiresConsent) { + PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()) + .edit() + .putString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed") + .apply(); + } + if (requestPermission.compareAndSet(false, true)) { + requestPermissions(permission, REQUEST_SYNC_CONTACTS); + } + }); + if (requiresConsent) { + builder.setNegativeButton( + R.string.decline, + (dialog, which) -> + PreferenceManager.getDefaultSharedPreferences( + getApplicationContext()) + .edit() + .putString( + PREF_KEY_CONTACT_INTEGRATION_CONSENT, + "declined") + .apply()); + } else { + builder.setOnDismissListener( + dialog -> { + if (requestPermission.compareAndSet(false, true)) { + requestPermissions(permission, REQUEST_SYNC_CONTACTS); + } + }); } + builder.setCancelable(requiresConsent); + final AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(requiresConsent); + dialog.setOnShowListener( + dialogInterface -> { + final TextView tv = dialog.findViewById(android.R.id.message); + if (tv != null) { + tv.setMovementMethod(LinkMovementMethod.getInstance()); + } + }); + dialog.show(); + } else { + requestPermissions(permission, REQUEST_SYNC_CONTACTS); } } + return true; } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -980,10 +1074,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (actionBar == null) { return; } - boolean openConversations = !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null); + boolean openConversations = + !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null); actionBar.setDisplayHomeAsUpEnabled(openConversations); actionBar.setDisplayHomeAsUpEnabled(openConversations); - } @Override @@ -995,7 +1089,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne xmppConnectionService.getQuickConversationsService().considerSyncBackground(false); } if (mPostponedActivityResult != null) { - onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); + onActivityResult( + mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second); this.mPostponedActivityResult = null; } this.mActivatedAccounts.clear(); @@ -1064,7 +1159,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (QuickConversationsService.isQuicksy()) { setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing()); } - if (QuickConversationsService.isConversations() && AccountUtils.hasEnabledAccounts(xmppConnectionService) && this.contacts.size() == 0 && this.conferences.size() == 0 && mOpenedFab.compareAndSet(false, true)) { + if (QuickConversationsService.isConversations() + && AccountUtils.hasEnabledAccounts(xmppConnectionService) + && this.contacts.size() == 0 + && this.conferences.size() == 0 + && mOpenedFab.compareAndSet(false, true)) { binding.speedDial.open(); } } @@ -1087,7 +1186,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne case Intent.ACTION_VIEW: Uri uri = intent.getData(); if (uri != null) { - Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false)); + Invite invite = + new Invite(intent.getData(), intent.getBooleanExtra("scanned", false)); invite.account = intent.getStringExtra(EXTRA_ACCOUNT); invite.forceDialog = intent.getBooleanExtra("force_dialog", false); return invite.invite(); @@ -1099,7 +1199,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private boolean handleJid(Invite invite) { - final List contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account); + List contacts = + xmppConnectionService.findContacts(invite.getJid(), invite.account); final Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid(), invite.account); if (invite.isAction(XmppUri.ACTION_JOIN) || (contacts.isEmpty() && muc != null)) { if (muc != null && !invite.forceDialog) { @@ -1121,8 +1222,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne displayVerificationWarningDialog(contact, invite); } else { if (invite.hasFingerprints()) { - if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) { - Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show(); + if (xmppConnectionService.verifyFingerprints( + contact, invite.getFingerprints())) { + Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT) + .show(); } } if (invite.account != null) { @@ -1150,15 +1253,23 @@ public class StartConversationActivity extends XmppActivity implements XmppConne View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null); final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source); TextView warning = view.findViewById(R.id.warning); - warning.setText(JidDialog.style(this, R.string.verifying_omemo_keys_trusted_source, contact.getJid().asBareJid().toEscapedString(), contact.getDisplayName())); + warning.setText( + JidDialog.style( + this, + R.string.verifying_omemo_keys_trusted_source, + contact.getJid().asBareJid().toEscapedString(), + contact.getDisplayName())); builder.setView(view); - builder.setPositiveButton(R.string.confirm, (dialog, which) -> { - if (isTrustedSource.isChecked() && invite.hasFingerprints()) { - xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints()); - } - switchToConversationDoNotAppend(contact, invite.getBody()); - }); - builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish()); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + if (isTrustedSource.isChecked() && invite.hasFingerprints()) { + xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints()); + } + switchToConversationDoNotAppend(contact, invite.getBody()); + }); + builder.setNegativeButton( + R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish()); AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish()); @@ -1181,10 +1292,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (account.isEnabled()) { for (Contact contact : account.getRoster().getContacts()) { Presence.Status s = contact.getShownStatus(); - if (contact.showInContactList() && contact.match(this, needle) + if (contact.showInContactList() + && contact.match(this, needle) && (!this.mHideOfflineContacts - || (needle != null && !needle.trim().isEmpty()) - || s.compareTo(Presence.Status.OFFLINE) < 0)) { + || (needle != null && !needle.trim().isEmpty()) + || s.compareTo(Presence.Status.OFFLINE) < 0)) { this.contacts.add(contact); tags.addAll(contact.getTags(this)); } @@ -1276,7 +1388,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } private void navigateBack() { - if (!createdByViewIntent && xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) { + if (!createdByViewIntent + && xmppConnectionService != null + && !xmppConnectionService.isConversationsListEmpty(null)) { Intent intent = new Intent(this, ConversationsActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); startActivity(intent); @@ -1297,13 +1411,20 @@ public class StartConversationActivity extends XmppActivity implements XmppConne intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false); intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true); intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim()); - intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + intent.putExtra( + ChooseContactActivity.EXTRA_ACCOUNT, + account.getJid().asBareJid().toEscapedString()); intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants); startActivityForResult(intent, REQUEST_CREATE_CONFERENCE); } @Override - public void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout layout, AutoCompleteTextView jid, String password) { + public void onJoinDialogPositiveClick( + final Dialog dialog, + final AutoCompleteTextView spinner, + final TextInputLayout layout, + final AutoCompleteTextView jid, + final String password) { if (!xmppConnectionServiceBound) { return; } @@ -1338,9 +1459,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne bookmark.setNick(nick); } xmppConnectionService.createBookmark(account, bookmark); - final Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, conferenceJid, true, true, null, true, password); - + final Conversation conversation = + xmppConnectionService.findOrCreateConversation( + account, conferenceJid, true, true, null, true, password); bookmark.setConversation(conversation); switchToConversation(conversation); } @@ -1360,7 +1481,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } } - private void setRefreshing(boolean refreshing) { MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0); if (fragment != null) { @@ -1372,29 +1492,32 @@ public class StartConversationActivity extends XmppActivity implements XmppConne public void onCreatePublicChannel(Account account, String name, Jid address) { mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG); mToast.show(); - xmppConnectionService.createPublicChannel(account, name, address, new UiCallback() { - @Override - public void success(Conversation conversation) { - runOnUiThread(() -> { - hideToast(); - switchToConversation(conversation); - }); + xmppConnectionService.createPublicChannel( + account, + name, + address, + new UiCallback() { + @Override + public void success(Conversation conversation) { + runOnUiThread( + () -> { + hideToast(); + switchToConversation(conversation); + }); + } - } + @Override + public void error(int errorCode, Conversation conversation) { + runOnUiThread( + () -> { + replaceToast(getString(errorCode)); + switchToConversation(conversation); + }); + } - @Override - public void error(int errorCode, Conversation conversation) { - runOnUiThread(() -> { - replaceToast(getString(errorCode)); - switchToConversation(conversation); + @Override + public void userInputRequired(PendingIntent pi, Conversation object) {} }); - } - - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { - - } - }); } public static class MyListFragment extends SwipeRefreshListFragment { @@ -1406,7 +1529,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void onListItemClick(final ListView l, final View v, final int position, final long id) { + public void onListItemClick( + final ListView l, final View v, final int position, final long id) { if (mOnItemClickListener != null) { mOnItemClickListener.onItemClick(l, v, position, id); } @@ -1426,7 +1550,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void onCreateContextMenu(@NonNull final ContextMenu menu, @NonNull final View v, final ContextMenuInfo menuInfo) { + public void onCreateContextMenu( + @NonNull final ContextMenu menu, + @NonNull final View v, + final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); final StartConversationActivity activity = (StartConversationActivity) getActivity(); if (activity == null) { @@ -1460,7 +1587,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (contact.isSelf()) { showContactDetailsItem.setVisible(false); } - deleteContactMenuItem.setVisible(contact.showInRoster() && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER)); + deleteContactMenuItem.setVisible( + contact.showInRoster() + && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER)); final XmppConnection xmpp = contact.getAccount().getXmppConnection(); if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) { if (contact.isBlocked()) { @@ -1519,7 +1648,8 @@ public class StartConversationActivity extends XmppActivity implements XmppConne } @Override - public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + public void destroyItem( + @NonNull ViewGroup container, int position, @NonNull Object object) { FragmentTransaction trans = fragmentManager.beginTransaction(); trans.remove(fragments[position]); trans.commit(); @@ -1535,7 +1665,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne try { trans.commit(); } catch (IllegalStateException e) { - //ignore + // ignore } return fragment; } @@ -1569,11 +1699,13 @@ public class StartConversationActivity extends XmppActivity implements XmppConne if (position == 1) { listFragment.setListAdapter(mConferenceAdapter); listFragment.setContextMenu(R.menu.conference_context); - listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p)); + listFragment.setOnListItemClickListener( + (arg0, arg1, p, arg3) -> openConversationForBookmark(p)); } else { listFragment.setListAdapter(mContactsAdapter); listFragment.setContextMenu(R.menu.contact_context); - listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p)); + listFragment.setOnListItemClickListener( + (arg0, arg1, p, arg3) -> openConversationForContact(p)); if (QuickConversationsService.isQuicksy()) { listFragment.setOnRefreshListener(StartConversationActivity.this); } @@ -1597,7 +1729,6 @@ public class StartConversationActivity extends XmppActivity implements XmppConne boolean forceDialog = false; - Invite(final String uri) { super(uri); } @@ -1608,7 +1739,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne boolean invite() { if (!isValidJid()) { - Toast.makeText(StartConversationActivity.this, R.string.invalid_jid, Toast.LENGTH_SHORT).show(); + Toast.makeText( + StartConversationActivity.this, + R.string.invalid_jid, + Toast.LENGTH_SHORT) + .show(); return false; } if (getJid() != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java index 637a7f55f39b7e33c6eac31e0babb5187a8aa8d4..724454067ea48d6f1983e7acf0f5eb2cfc566131 100644 --- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java @@ -68,9 +68,7 @@ public class UriHandlerActivity extends BaseActivity { } public static void scan(final Activity activity, final boolean provisioning) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M - || ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) - == PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { final Intent intent = new Intent(activity, UriHandlerActivity.class); intent.setAction(UriHandlerActivity.ACTION_SCAN_QR_CODE); if (provisioning) { @@ -114,6 +112,7 @@ public class UriHandlerActivity extends BaseActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_uri_handler); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); } @Override @@ -187,7 +186,7 @@ public class UriHandlerActivity extends BaseActivity { startActivity(intent); return true; } - if (accounts.size() == 0 + if (accounts.isEmpty() && xmppUri.isAction(XmppUri.ACTION_ROSTER) && "y" .equalsIgnoreCase( @@ -203,7 +202,7 @@ public class UriHandlerActivity extends BaseActivity { return false; } - if (accounts.size() == 0) { + if (accounts.isEmpty()) { if (xmppUri.isValidJid()) { intent = SignupUtils.getSignUpIntent(this); intent.putExtra(StartConversationActivity.EXTRA_INVITE_URI, xmppUri.toString()); @@ -259,14 +258,14 @@ public class UriHandlerActivity extends BaseActivity { private void checkForLinkHeader(final HttpUrl url) { Log.d(Config.LOGTAG, "checking for link header on " + url); this.call = - HttpConnectionManager.OK_HTTP_CLIENT.newCall( + HttpConnectionManager.okHttpClient(this).newCall( new Request.Builder().url(url).head().build()); this.call.enqueue( new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d(Config.LOGTAG, "unable to check HTTP url", e); - showError(R.string.no_xmpp_adddress_found); + showErrorOnUiThread(R.string.no_xmpp_adddress_found); } @Override @@ -277,7 +276,7 @@ public class UriHandlerActivity extends BaseActivity { return; } } - showError(R.string.no_xmpp_adddress_found); + showErrorOnUiThread(R.string.no_xmpp_adddress_found); } }); } @@ -301,6 +300,10 @@ public class UriHandlerActivity extends BaseActivity { this.binding.error.setVisibility(View.VISIBLE); } + private void showErrorOnUiThread(@StringRes int error) { + runOnUiThread(()-> showError(error)); + } + private static Class findShareViaAccountClass() { try { return Class.forName("eu.siacs.conversations.ui.ShareViaAccountActivity"); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 1c09a74fbb1ba8170b3a03fc7bdcf45c3ff19d50..4c4dbbbd86072a602fee67c0dca0bb6f753d938e 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -516,14 +516,9 @@ public abstract class XmppActivity extends ActionBarActivity { } protected boolean isOptimizingBattery() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); - return pm != null - && !pm.isIgnoringBatteryOptimizations(getPackageName()); - } else { - return false; - } - } + final PowerManager pm = getSystemService(PowerManager.class); + return !pm.isIgnoringBatteryOptimizations(getPackageName()); +} protected boolean isAffectedByDataSaver() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/:w b/src/main/java/eu/siacs/conversations/ui/adapter/:w new file mode 100644 index 0000000000000000000000000000000000000000..b033660fd7480ce0b845e4d65ed89f8cc6efc978 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/:w @@ -0,0 +1,1701 @@ +package eu.siacs.conversations.ui.adapter; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.content.res.ColorStateList; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.Spanned; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.style.ImageSpan; +import android.text.style.ClickableSpan; +import android.text.format.DateUtils; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StyleSpan; +import android.text.style.URLSpan; +import android.util.DisplayMetrics; +import android.util.LruCache; +import android.view.accessibility.AccessibilityEvent; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.AttrRes; +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; +import androidx.core.widget.ImageViewCompat; + +import com.google.android.material.imageview.ShapeableImageView; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.ShapeAppearanceModel; + +import com.cheogram.android.BobTransfer; +import com.cheogram.android.MessageTextActionModeCallback; +import com.cheogram.android.SwipeDetector; +import com.cheogram.android.WebxdcPage; +import com.cheogram.android.WebxdcUpdate; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.color.MaterialColors; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import com.lelloman.identicon.view.GithubIdenticonView; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.ipfs.cid.Cid; + +import me.saket.bettermovementmethod.BetterLinkMovementMethod; + +import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Conversational; +import eu.siacs.conversations.entities.DownloadableFile; +import eu.siacs.conversations.entities.Message.FileParams; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.entities.RtpSessionStatus; +import eu.siacs.conversations.entities.Transferable; +import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.MessageArchiveService; +import eu.siacs.conversations.services.NotificationService; +import eu.siacs.conversations.ui.Activities; +import eu.siacs.conversations.ui.ConversationFragment; +import eu.siacs.conversations.ui.ConversationsActivity; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.ui.service.AudioPlayer; +import eu.siacs.conversations.ui.text.DividerSpan; +import eu.siacs.conversations.ui.text.QuoteSpan; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.ui.util.AvatarWorkerTask; +import eu.siacs.conversations.ui.util.MyLinkify; +import eu.siacs.conversations.ui.util.QuoteHelper; +import eu.siacs.conversations.ui.util.ShareUtil; +import eu.siacs.conversations.ui.util.ViewUtil; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.Emoticons; +import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.utils.MessageUtils; +import eu.siacs.conversations.utils.StylingHelper; +import eu.siacs.conversations.utils.TimeFrameUtils; +import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.mam.MamReference; +import eu.siacs.conversations.xml.Element; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MessageAdapter extends ArrayAdapter { + + public static final String DATE_SEPARATOR_BODY = "DATE_SEPARATOR"; + private static final int SENT = 0; + private static final int RECEIVED = 1; + private static final int STATUS = 2; + private static final int DATE_SEPARATOR = 3; + private static final int RTP_SESSION = 4; + private final XmppActivity activity; + private final AudioPlayer audioPlayer; + private List highlightedTerm = null; + private final DisplayMetrics metrics; + private ConversationFragment mConversationFragment = null; + private OnContactPictureClicked mOnContactPictureClickedListener; + private OnContactPictureClicked mOnMessageBoxClickedListener; + private OnContactPictureClicked mOnMessageBoxSwipedListener; + private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + private OnInlineImageLongClicked mOnInlineImageLongClickedListener; + private boolean mUseGreenBackground = false; + private BubbleDesign bubbleDesign = new BubbleDesign(false, false); + private final boolean mForceNames; + private final Map lastWebxdcUpdate = new HashMap<>(); + private String selectionUuid = null; + + public MessageAdapter( + final XmppActivity activity, final List messages, final boolean forceNames) { + super(activity, 0, messages); + this.audioPlayer = new AudioPlayer(this); + this.activity = activity; + metrics = getContext().getResources().getDisplayMetrics(); + updatePreferences(); + this.mForceNames = forceNames; + } + + public MessageAdapter(final XmppActivity activity, final List messages) { + this(activity, messages, false); + } + + private static void resetClickListener(View... views) { + for (View view : views) { + if (view != null) view.setOnClickListener(null); + } + } + + public void flagScreenOn() { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + public void flagScreenOff() { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + public void setVolumeControl(final int stream) { + activity.setVolumeControlStream(stream); + } + + public void setOnContactPictureClicked(OnContactPictureClicked listener) { + this.mOnContactPictureClickedListener = listener; + } + + public void setOnMessageBoxClicked(OnContactPictureClicked listener) { + this.mOnMessageBoxClickedListener = listener; + } + + public void setOnMessageBoxSwiped(OnContactPictureClicked listener) { + this.mOnMessageBoxSwipedListener = listener; + } + + public void setConversationFragment(ConversationFragment frag) { + mConversationFragment = frag; + } + + public void quoteText(String text) { + if (mConversationFragment != null) mConversationFragment.quoteText(text); + } + + public boolean hasSelection() { + return selectionUuid != null; + } + + public Activity getActivity() { + return activity; + } + + public void setOnContactPictureLongClicked(OnContactPictureLongClicked listener) { + this.mOnContactPictureLongClickedListener = listener; + } + + public void setOnInlineImageLongClicked(OnInlineImageLongClicked listener) { + this.mOnInlineImageLongClickedListener = listener; + } + + @Override + public int getViewTypeCount() { + return 5; + } + + private int getItemViewType(Message message) { + if (message.getType() == Message.TYPE_STATUS) { + if (DATE_SEPARATOR_BODY.equals(message.getBody())) { + return DATE_SEPARATOR; + } else { + return STATUS; + } + } else if (message.getType() == Message.TYPE_RTP_SESSION) { + return RTP_SESSION; + } else if (message.getStatus() <= Message.STATUS_RECEIVED) { + return RECEIVED; + } else { + return SENT; + } + } + + @Override + public int getItemViewType(int position) { + return this.getItemViewType(getItem(position)); + } + + private void displayStatus( + final ViewHolder viewHolder, + final Message message, + final int type, + final BubbleColor bubbleColor) { + final int mergedStatus = message.getMergedStatus(); + final boolean error; + if (viewHolder.indicatorReceived != null) { + viewHolder.indicatorReceived.setVisibility(View.GONE); + } + final Transferable transferable = message.getTransferable(); + final boolean multiReceived = + message.getConversation().getMode() == Conversation.MODE_MULTI + && mergedStatus <= Message.STATUS_RECEIVED; + final String fileSize; + if (message.isFileOrImage() + || transferable != null + || MessageUtils.unInitiatedButKnownSize(message)) { + final FileParams params = message.getFileParams(); + fileSize = params.size != null ? UIHelper.filesizeToString(params.size) : null; + if (message.getStatus() == Message.STATUS_SEND_FAILED + || (transferable != null + && (transferable.getStatus() == Transferable.STATUS_FAILED + || transferable.getStatus() + == Transferable.STATUS_CANCELLED))) { + error = true; + } else { + error = message.getStatus() == Message.STATUS_SEND_FAILED; + } + } else { + fileSize = null; + error = message.getStatus() == Message.STATUS_SEND_FAILED; + } + if (type == SENT) { + final @DrawableRes Integer receivedIndicator = + getMessageStatusAsDrawable(message, mergedStatus); + if (receivedIndicator == null) { + viewHolder.indicatorReceived.setVisibility(View.INVISIBLE); + } else { + viewHolder.indicatorReceived.setImageResource(receivedIndicator); + if (mergedStatus == Message.STATUS_SEND_FAILED) { + setImageTintError(viewHolder.indicatorReceived); + } else { + setImageTint(viewHolder.indicatorReceived, bubbleColor); + } + viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + } + } + final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus); + + if (error && type == SENT) { + viewHolder.time.setTextColor( + MaterialColors.getColor( + viewHolder.time, com.google.android.material.R.attr.colorError)); + } else { + setTextColor(viewHolder.time, bubbleColor); + } + setTextColor(viewHolder.subject, bubbleColor); + if (message.getEncryption() == Message.ENCRYPTION_NONE) { + viewHolder.indicator.setVisibility(View.GONE); + } else { + boolean verified = false; + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + final FingerprintStatus status = + message.getConversation() + .getAccount() + .getAxolotlService() + .getFingerprintTrust(message.getFingerprint()); + if (status != null && status.isVerified()) { + verified = true; + } + } + if (verified) { + viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp); + } else { + viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp); + } + if (error && type == SENT) { + setImageTintError(viewHolder.indicator); + } else { + setImageTint(viewHolder.indicator, bubbleColor); + } + viewHolder.indicator.setVisibility(View.VISIBLE); + } + + if (viewHolder.edit_indicator != null) { + if (message.edited()) { + viewHolder.edit_indicator.setVisibility(View.VISIBLE); + if (error && type == SENT) { + setImageTintError(viewHolder.edit_indicator); + } else { + setImageTint(viewHolder.edit_indicator, bubbleColor); + } + } else { + viewHolder.edit_indicator.setVisibility(View.GONE); + } + } + + final String formattedTime = + UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + final String bodyLanguage = message.getBodyLanguage(); + final ImmutableList.Builder timeInfoBuilder = new ImmutableList.Builder<>(); + if (message.getStatus() <= Message.STATUS_RECEIVED) { + timeInfoBuilder.add(formattedTime); + if (fileSize != null) { + timeInfoBuilder.add(fileSize); + } + if (mForceNames || multiReceived || (message.getTrueCounterpart() != null && message.getContact() != null)) { + final String displayName = UIHelper.getMessageDisplayName(message); + if (displayName != null) { + timeInfoBuilder.add(displayName); + } + } + if (bodyLanguage != null) { + timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + } + } else { + if (bodyLanguage != null) { + timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + } + if (fileSize != null) { + timeInfoBuilder.add(fileSize); + } + // for space reasons we display only 'additional status info' (send progress or concrete + // failure reason) or the time + if (additionalStatusInfo != null) { + timeInfoBuilder.add(additionalStatusInfo); + } else { + timeInfoBuilder.add(formattedTime); + } + } + final var timeInfo = timeInfoBuilder.build(); + viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo)); + } + + public static @DrawableRes Integer getMessageStatusAsDrawable( + final Message message, final int status) { + final var transferable = message.getTransferable(); + return switch (status) { + case Message.STATUS_WAITING -> R.drawable.ic_more_horiz_24dp; + case Message.STATUS_UNSEND -> transferable == null ? null : R.drawable.ic_upload_24dp; + case Message.STATUS_SEND -> R.drawable.ic_done_24dp; + case Message.STATUS_SEND_RECEIVED, Message.STATUS_SEND_DISPLAYED -> R.drawable + .ic_done_all_24dp; + case Message.STATUS_SEND_FAILED -> { + final String errorMessage = message.getErrorMessage(); + if (Message.ERROR_MESSAGE_CANCELLED.equals(errorMessage)) { + yield R.drawable.ic_cancel_24dp; + } else { + yield R.drawable.ic_error_24dp; + } + } + case Message.STATUS_OFFERED -> R.drawable.ic_p2p_24dp; + default -> null; + }; + } + + @Nullable + private String getAdditionalStatusInfo(final Message message, final int mergedStatus) { + final String additionalStatusInfo; + if (mergedStatus == Message.STATUS_SEND_FAILED) { + final String errorMessage = Strings.nullToEmpty(message.getErrorMessage()); + final String[] errorParts = errorMessage.split("\\u001f", 2); + if (errorParts.length == 2 && errorParts[0].equals("file-too-large")) { + additionalStatusInfo = getContext().getString(R.string.file_too_large); + } else { + additionalStatusInfo = null; + } + } else if (mergedStatus == Message.STATUS_UNSEND) { + final var transferable = message.getTransferable(); + if (transferable == null) { + return null; + } + return getContext().getString(R.string.sending_file, transferable.getProgress()); + } else { + additionalStatusInfo = null; + } + return additionalStatusInfo; + } + + private void displayInfoMessage( + ViewHolder viewHolder, CharSequence text, final BubbleColor bubbleColor) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText(text); + viewHolder.messageBody.setTextColor( + bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)); + viewHolder.messageBody.setTextIsSelectable(false); + } + + private void displayEmojiMessage( + final ViewHolder viewHolder, final SpannableStringBuilder body, final BubbleColor bubbleColor) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + setTextColor(viewHolder.messageBody, bubbleColor); + ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class); + float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 3.0f : 2.0f; + body.setSpan( + new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(body); + } + + private void applyQuoteSpan( + final TextView textView, + Editable body, + int start, + int end, + final BubbleColor bubbleColor, + final boolean makeEdits) { + if (makeEdits && start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { + body.insert(start++, "\n"); + body.setSpan( + new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + end++; + } + if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { + body.insert(end, "\n"); + body.setSpan( + new DividerSpan(false), + end, + end + ("\n".equals(body.subSequence(end + 1, end + 2).toString()) ? 2 : 1), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + body.setSpan( + new QuoteSpan(bubbleToOnSurfaceVariant(textView, bubbleColor), metrics), + start, + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + public boolean handleTextQuotes(final TextView textView, final Editable body) { + return handleTextQuotes(textView, body, true); + } + + public boolean handleTextQuotes(final TextView textView, final Editable body, final boolean deleteMarkers) { + final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE; + return handleTextQuotes(textView, body, bubbleColor, deleteMarkers); + } + + /** + * Applies QuoteSpan to group of lines which starts with > or » characters. Appends likebreaks + * and applies DividerSpan to them to show a padding between quote and text. + */ + public boolean handleTextQuotes( + final TextView textView, + final Editable body, + final BubbleColor bubbleColor, + final boolean deleteMarkers) { + boolean startsWithQuote = false; + int quoteDepth = 1; + while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) { + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + int skipped = 0; + for (int i = 0; i <= body.length(); i++) { + if (!deleteMarkers && QuoteHelper.isRelativeSizeSpanned(body, i)) { + skipped++; + continue; + } + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i - skipped; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor, deleteMarkers); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + if (deleteMarkers) { + i -= lineTextStart - lineStart; + body.delete(lineStart, lineTextStart); + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + } else { + body.setSpan(new RelativeSizeSpan(i - (lineTextStart - lineStart) == lineStart ? 1 : 0), lineStart, lineTextStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | StylingHelper.XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); + } + lineStart = -1; + lineTextStart = -1; + } + } + previous = current; + skipped = 0; + } + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor, deleteMarkers); + } + quoteDepth++; + } + return startsWithQuote; + } + + private SpannableStringBuilder getSpannableBody(final Message message) { + Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null); + return message.getMergedBody((cid) -> { + try { + DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); + if (f == null || !f.canRead()) { + if (!message.trusted() && !message.getConversation().canInferPresence()) return null; + + try { + new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); + } catch (final NoSuchAlgorithmException | URISyntaxException e) { } + return null; + } + + Drawable d = activity.xmppConnectionService.getFileBackend().getThumbnail(f, activity.getResources(), (int) (metrics.density * 288), true); + if (d == null) { + new ThumbnailTask().execute(f); + } + return d; + } catch (final IOException e) { + return null; + } + }, fallbackImg); + } + + private void displayTextMessage( + final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.VISIBLE); + setTextColor(viewHolder.messageBody, bubbleColor); + setTextSize(viewHolder.messageBody, this.bubbleDesign.largeFont); + + final ViewGroup.LayoutParams layoutParams = viewHolder.messageBody.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + viewHolder.messageBody.setLayoutParams(layoutParams); + + viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + + if (message.getBody() != null && !message.getBody().equals("")) { + viewHolder.messageBody.setTextIsSelectable(true); + viewHolder.messageBody.setVisibility(View.VISIBLE); + final String nick = UIHelper.getMessageDisplayName(message); + SpannableStringBuilder body = getSpannableBody(message); + final var processMarkup = body.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0; + boolean hasMeCommand = message.hasMeCommand(); + if (hasMeCommand) { + body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); + } + if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { + body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); + body.append("\u2026"); + } + Message.MergeSeparator[] mergeSeparators = + body.getSpans(0, body.length(), Message.MergeSeparator.class); + for (Message.MergeSeparator mergeSeparator : mergeSeparators) { + int start = body.getSpanStart(mergeSeparator); + int end = body.getSpanEnd(mergeSeparator); + body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + for (final android.text.style.QuoteSpan quote : body.getSpans(0, body.length(), android.text.style.QuoteSpan.class)) { + int start = body.getSpanStart(quote); + int end = body.getSpanEnd(quote); + body.removeSpan(quote); + applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true); + } + boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true) : false; + if (!message.isPrivateMessage()) { + if (hasMeCommand) { + body.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + 0, + nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } else { + String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = + activity.getString( + R.string.private_message_to, + Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + body.insert(0, privateMarker); + int privateMarkerIndex = privateMarker.length(); + if (startsWithQuote) { + body.insert(privateMarkerIndex, "\n\n"); + body.setSpan( + new DividerSpan(false), + privateMarkerIndex, + privateMarkerIndex + 2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + body.insert(privateMarkerIndex, " "); + } + body.setSpan( + new ForegroundColorSpan( + bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), + 0, + privateMarkerIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan( + new StyleSpan(Typeface.BOLD), + 0, + privateMarkerIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + if (hasMeCommand) { + body.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + privateMarkerIndex + 1, + privateMarkerIndex + 1 + nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + if (message.getConversation().getMode() == Conversation.MODE_MULTI + && message.getStatus() == Message.STATUS_RECEIVED) { + if (message.getConversation() instanceof Conversation conversation) { + Pattern pattern = + NotificationService.generateNickHighlightPattern( + conversation.getMucOptions().getActualNick()); + Matcher matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan( + new StyleSpan(Typeface.BOLD), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + pattern = NotificationService.generateNickHighlightPattern(conversation.getMucOptions().getActualName()); + matcher = pattern.matcher(body); + while (matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + Matcher matcher = Emoticons.getEmojiPattern(body).matcher(body); + while (matcher.find()) { + if (matcher.start() < matcher.end()) { + body.setSpan( + new RelativeSizeSpan(1.2f), + matcher.start(), + matcher.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + if (processMarkup) StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); + MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid()); + if (highlightedTerm != null) { + StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm); + } + + viewHolder.messageBody.setAutoLinkMask(0); + viewHolder.messageBody.setText(body); + BetterLinkMovementMethod method = new BetterLinkMovementMethod() { + @Override + protected void dispatchUrlLongClick(TextView tv, ClickableSpan span) { + if (span instanceof URLSpan || mOnInlineImageLongClickedListener == null) { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + super.dispatchUrlLongClick(tv, span); + return; + } + + Spannable body = (Spannable) tv.getText(); + ImageSpan[] imageSpans = body.getSpans(body.getSpanStart(span), body.getSpanEnd(span), ImageSpan.class); + if (imageSpans.length > 0) { + Uri uri = Uri.parse(imageSpans[0].getSource()); + Cid cid = BobTransfer.cid(uri); + if (cid == null) return; + if (mOnInlineImageLongClickedListener.onInlineImageLongClicked(cid)) { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + } + } + } + }; + method.setOnLinkLongClickListener((tv, url) -> { + tv.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); + ShareUtil.copyLinkToClipboard(activity, url); + return true; + }); + viewHolder.messageBody.setMovementMethod(method); + } else { + viewHolder.messageBody.setText(""); + viewHolder.messageBody.setTextIsSelectable(false); + toggleWhisperInfo(viewHolder, message, bubbleColor); + } + } + + private void displayDownloadableMessage( + ViewHolder viewHolder, + final Message message, + String text, + final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; + if (thumbs != null && !thumbs.isEmpty()) { + for (Element thumb : thumbs) { + Uri uri = Uri.parse(thumb.getAttribute("uri")); + if (uri.getScheme().equals("data")) { + String[] parts = uri.getSchemeSpecificPart().split(",", 2); + parts = parts[0].split(";"); + if (!parts[0].equals("image/blurhash") && !parts[0].equals("image/thumbhash") && !parts[0].equals("image/jpeg") && !parts[0].equals("image/png") && !parts[0].equals("image/webp") && !parts[0].equals("image/gif")) continue; + } else if (uri.getScheme().equals("cid")) { + Cid cid = BobTransfer.cid(uri); + if (cid == null) continue; + DownloadableFile f = activity.xmppConnectionService.getFileForCid(cid); + if (f == null || !f.canRead()) { + if (!message.trusted() && !message.getConversation().canInferPresence()) continue; + + try { + new BobTransfer(BobTransfer.uri(cid), message.getConversation().getAccount(), message.getCounterpart(), activity.xmppConnectionService).start(); + } catch (final NoSuchAlgorithmException | URISyntaxException e) { } + continue; + } + } else { + continue; + } + + int width = message.getFileParams().width; + if (width < 1 && thumb.getAttribute("width") != null) width = Integer.parseInt(thumb.getAttribute("width")); + if (width < 1) width = 1920; + + int height = message.getFileParams().height; + if (height < 1 && thumb.getAttribute("height") != null) height = Integer.parseInt(thumb.getAttribute("height")); + if (height < 1) height = 1080; + + viewHolder.image.setVisibility(View.VISIBLE); + imagePreviewLayout(width, height, viewHolder.image, true, type, viewHolder); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); + + break; + } + } + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(text); + final var attachment = Attachment.of(message); + final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); + viewHolder.download_button.setIconResource(imageResource); + viewHolder.download_button.setOnClickListener( + v -> ConversationFragment.downloadFile(activity, message)); + } + + private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + Cid webxdcCid = message.getFileParams().getCids().get(0); + WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message, activity.xmppConnectionService); + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText("Open " + webxdc.getName()); + viewHolder.download_button.setOnClickListener(v -> { + Conversation conversation = (Conversation) message.getConversation(); + if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { + conversation.startWebxdc(webxdc); + } + }); + + final WebxdcUpdate lastUpdate; + synchronized(lastWebxdcUpdate) { lastUpdate = lastWebxdcUpdate.get(message.getUuid()); } + if (lastUpdate == null) { + new Thread(() -> { + final WebxdcUpdate update = activity.xmppConnectionService.findLastWebxdcUpdate(message); + if (update != null) { + synchronized(lastWebxdcUpdate) { lastWebxdcUpdate.put(message.getUuid(), update); } + activity.xmppConnectionService.updateConversationUi(); + } + }).start(); + } else { + if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) { + viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody.setText( + (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") + + (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary()) + ); + } + } + + final LruCache cache = activity.xmppConnectionService.getDrawableCache(); + final Drawable d = cache.get("webxdc:icon:" + webxdcCid); + if (d == null) { + new Thread(() -> { + Drawable icon = webxdc.getIcon(); + if (icon != null) { + cache.put("webxdc:icon:" + webxdcCid, icon); + activity.xmppConnectionService.updateConversationUi(); + } + }).start(); + } else { + viewHolder.image.setVisibility(View.VISIBLE); + viewHolder.image.setImageDrawable(d); + } + } + + private void displayOpenableMessage( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText( + activity.getString( + R.string.open_x_file, + UIHelper.getFileDescriptionString(activity, message))); + final var attachment = Attachment.of(message); + final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); + viewHolder.download_button.setIconResource(imageResource); + viewHolder.download_button.setOnClickListener(v -> openDownloadable(message)); + } + + private void displayLocationMessage( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(R.string.show_location); + final var attachment = Attachment.of(message); + final @DrawableRes int imageResource = MediaAdapter.getImageDrawable(attachment); + viewHolder.download_button.setIconResource(imageResource); + viewHolder.download_button.setOnClickListener(v -> showLocation(message)); + } + + private void displayAudioMessage( + ViewHolder viewHolder, Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.image.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.GONE); + final RelativeLayout audioPlayer = viewHolder.audioPlayer; + audioPlayer.setVisibility(View.VISIBLE); + AudioPlayer.ViewHolder.get(audioPlayer).setBubbleColor(bubbleColor); + this.audioPlayer.init(audioPlayer, message); + } + + private void displayMediaPreviewMessage( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + displayTextMessage(viewHolder, message, bubbleColor, type); + viewHolder.download_button.setVisibility(View.GONE); + viewHolder.audioPlayer.setVisibility(View.GONE); + viewHolder.image.setVisibility(View.VISIBLE); + final FileParams params = message.getFileParams(); + imagePreviewLayout(params.width, params.height, viewHolder.image, viewHolder.messageBody.getVisibility() != View.GONE, type, viewHolder); + activity.loadBitmap(message, viewHolder.image); + viewHolder.image.setOnClickListener(v -> openDownloadable(message)); + } + + private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean withOther, int type, ViewHolder viewHolder) { + final float target = activity.getResources().getDimension(R.dimen.image_preview_width); + final int scaledW; + final int scaledH; + if (Math.max(h, w) * metrics.density <= target) { + scaledW = (int) (w * metrics.density); + scaledH = (int) (h * metrics.density); + } else if (Math.max(h, w) <= target) { + scaledW = w; + scaledH = h; + } else if (w <= h) { + scaledW = (int) (w / ((double) h / target)); + scaledH = (int) target; + } else { + scaledW = (int) target; + scaledH = (int) (h / ((double) w / target)); + } + final var small = withOther ? scaledW < target : scaledW < 110 * metrics.density; + final LinearLayout.LayoutParams layoutParams = + new LinearLayout.LayoutParams(scaledW, scaledH); + image.setLayoutParams(layoutParams); + + final var bubbleRadius = activity.getResources().getDimension(R.dimen.bubble_radius); + var shape = new ShapeAppearanceModel.Builder().setTopRightCorner(CornerFamily.ROUNDED, bubbleRadius); + if (type == SENT) { + shape = shape.setTopLeftCorner(CornerFamily.ROUNDED, bubbleRadius); + } + if (small) { + final var imageRadius = activity.getResources().getDimension(R.dimen.image_radius); + shape = shape.setAllCorners(CornerFamily.ROUNDED, imageRadius); + image.setPadding(0, (int)(8 * metrics.density), 0, 0); + } else { + image.setPadding(0, 0, 0, 0); + } + image.setShapeAppearanceModel(shape.build()); + + if (!small) { + final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody.getLayoutParams(); + blayoutParams.width = (int) (target - (22 * metrics.density)); + viewHolder.messageBody.setLayoutParams(blayoutParams); + } + } + + private void toggleWhisperInfo( + ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { + if (message.isPrivateMessage()) { + final String privateMarker; + if (message.getStatus() <= Message.STATUS_RECEIVED) { + privateMarker = activity.getString(R.string.private_message); + } else { + Jid cp = message.getCounterpart(); + privateMarker = + activity.getString( + R.string.private_message_to, + Strings.nullToEmpty(cp == null ? null : cp.getResource())); + } + final SpannableString body = new SpannableString(privateMarker); + body.setSpan( + new ForegroundColorSpan( + bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), + 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan( + new StyleSpan(Typeface.BOLD), + 0, + privateMarker.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + viewHolder.messageBody.setText(body); + viewHolder.messageBody.setVisibility(View.VISIBLE); + } else { + viewHolder.messageBody.setVisibility(View.GONE); + } + } + + private void loadMoreMessages(Conversation conversation) { + conversation.setLastClearHistory(0, null); + activity.xmppConnectionService.updateConversation(conversation); + conversation.setHasMessagesLeftOnServer(true); + conversation.setFirstMamReference(null); + long timestamp = conversation.getLastMessageTransmitted().getTimestamp(); + if (timestamp == 0) { + timestamp = System.currentTimeMillis(); + } + conversation.messagesLoaded.set(true); + MessageArchiveService.Query query = + activity.xmppConnectionService + .getMessageArchiveService() + .query(conversation, new MamReference(0), timestamp, false); + if (query != null) { + Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG) + .show(); + } else { + Toast.makeText( + activity, + R.string.not_fetching_history_retention_period, + Toast.LENGTH_SHORT) + .show(); + } + } + + @Override + public View getView(final int position, View view, final @NonNull ViewGroup parent) { + final Message message = getItem(position); + final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; + final boolean isInValidSession = + message.isValidInSession() && (!omemoEncryption || message.isTrusted()); + final Conversational conversation = message.getConversation(); + final Account account = conversation.getAccount(); + final List commands = message.getCommands(); + final int type = getItemViewType(position); + ViewHolder viewHolder; + if (view == null) { + viewHolder = new ViewHolder(); + switch (type) { + case DATE_SEPARATOR: + view = + activity.getLayoutInflater() + .inflate(R.layout.item_message_date_bubble, parent, false); + viewHolder.status_message = view.findViewById(R.id.message_body); + viewHolder.message_box = view.findViewById(R.id.message_box); + break; + case RTP_SESSION: + view = + activity.getLayoutInflater() + .inflate(R.layout.item_message_rtp_session, parent, false); + viewHolder.status_message = view.findViewById(R.id.message_body); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + break; + case SENT: + view = activity.getLayoutInflater().inflate(R.layout.item_message_sent, parent, false); + viewHolder.status_line = view.findViewById(R.id.status_line); + viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.subject = view.findViewById(R.id.message_subject); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); + break; + case RECEIVED: + view = activity.getLayoutInflater().inflate(R.layout.item_message_received, parent, false); + viewHolder.status_line = view.findViewById(R.id.status_line); + viewHolder.message_box_inner = view.findViewById(R.id.message_box_inner); + viewHolder.message_box = view.findViewById(R.id.message_box); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.download_button = view.findViewById(R.id.download_button); + viewHolder.indicator = view.findViewById(R.id.security_indicator); + viewHolder.edit_indicator = view.findViewById(R.id.edit_indicator); + viewHolder.image = view.findViewById(R.id.message_image); + viewHolder.messageBody = view.findViewById(R.id.message_body); + viewHolder.time = view.findViewById(R.id.message_time); + viewHolder.subject = view.findViewById(R.id.message_subject); + viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); + viewHolder.encryption = view.findViewById(R.id.message_encryption); + viewHolder.audioPlayer = view.findViewById(R.id.audio_player); + viewHolder.commands_list = view.findViewById(R.id.commands_list); + viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); + break; + case STATUS: + view = + activity.getLayoutInflater() + .inflate(R.layout.item_message_status, parent, false); + viewHolder.contact_picture = view.findViewById(R.id.message_photo); + viewHolder.status_message = view.findViewById(R.id.status_message); + viewHolder.load_more_messages = view.findViewById(R.id.load_more_messages); + break; + default: + throw new AssertionError("Unknown view type"); + } + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + if (viewHolder == null) { + return view; + } + } + + if (viewHolder.messageBody != null) { + viewHolder.messageBody.setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody)); + } + + if (viewHolder.thread_identicon != null) { + viewHolder.thread_identicon.setVisibility(View.GONE); + final Element thread = message.getThread(); + if (thread != null) { + final String threadId = thread.getContent(); + if (threadId != null) { + viewHolder.thread_identicon.setVisibility(View.VISIBLE); + viewHolder.thread_identicon.setColor(UIHelper.getColorForName(threadId)); + viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId)); + } + } + } + + final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black); + final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final BubbleColor bubbleColor; + if (type == RECEIVED) { + if (isInValidSession) { + bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE; + } else { + bubbleColor = BubbleColor.WARNING; + } + } else { + if (!colorfulBackground && black) { + bubbleColor = BubbleColor.SECONDARY; + } else { + bubbleColor = colorfulBackground ? BubbleColor.TERTIARY : BubbleColor.SURFACE_HIGH; + } + } + + if (type == DATE_SEPARATOR) { + if (UIHelper.today(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.today); + } else if (UIHelper.yesterday(message.getTimeSent())) { + viewHolder.status_message.setText(R.string.yesterday); + } else { + viewHolder.status_message.setText( + DateUtils.formatDateTime( + activity, + message.getTimeSent(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); + } + if (colorfulBackground) { + setBackgroundTint(viewHolder.message_box, BubbleColor.PRIMARY); + setTextColor(viewHolder.status_message, BubbleColor.PRIMARY); + } else { + setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH); + setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH); + } + return view; + } else if (type == RTP_SESSION) { + final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; + final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); + final long duration = rtpSessionStatus.duration; + final String callTime = UIHelper.readableTimeDifferenceFull(activity, message.getTimeSent()); + if (received) { + if (duration > 0) { + viewHolder.status_message.setText( + activity.getString( + R.string.incoming_call_duration_timestamp, + TimeFrameUtils.resolve(activity, duration), + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } else if (rtpSessionStatus.successful) { + viewHolder.status_message.setText(activity.getString(R.string.incoming_call_timestamp, callTime)); + } else { + viewHolder.status_message.setText( + activity.getString( + R.string.missed_call_timestamp, + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } + } else { + if (duration > 0) { + viewHolder.status_message.setText( + activity.getString( + R.string.outgoing_call_duration_timestamp, + TimeFrameUtils.resolve(activity, duration), + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } else { + viewHolder.status_message.setText( + activity.getString( + R.string.outgoing_call_timestamp, + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } + } + if (colorfulBackground) { + setBackgroundTint(viewHolder.message_box, BubbleColor.SECONDARY); + setTextColor(viewHolder.status_message, BubbleColor.SECONDARY); + setImageTint(viewHolder.indicatorReceived, BubbleColor.SECONDARY); + } else { + setBackgroundTint(viewHolder.message_box, BubbleColor.SURFACE_HIGH); + setTextColor(viewHolder.status_message, BubbleColor.SURFACE_HIGH); + setImageTint(viewHolder.indicatorReceived, BubbleColor.SURFACE_HIGH); + } + viewHolder.indicatorReceived.setImageResource( + RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful)); + return view; + } else if (type == STATUS) { + if ("LOAD_MORE".equals(message.getBody())) { + viewHolder.status_message.setVisibility(View.GONE); + viewHolder.contact_picture.setVisibility(View.GONE); + viewHolder.load_more_messages.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setOnClickListener( + v -> loadMoreMessages((Conversation) message.getConversation())); + } else { + viewHolder.status_message.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setVisibility(View.GONE); + viewHolder.status_message.setText(message.getBody()); + boolean showAvatar; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + showAvatar = true; + AvatarWorkerTask.loadAvatar( + message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else if (message.getCounterpart() != null + || message.getTrueCounterpart() != null + || (message.getCounterparts() != null + && message.getCounterparts().size() > 0)) { + showAvatar = true; + AvatarWorkerTask.loadAvatar( + message, viewHolder.contact_picture, R.dimen.avatar_on_status_message); + } else { + showAvatar = false; + } + if (showAvatar) { + viewHolder.contact_picture.setAlpha(0.5f); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + } else { + viewHolder.contact_picture.setVisibility(View.GONE); + } + } + return view; + } else { + // viewHolder.message_box.setClipToOutline(true); This eats the bubble tails on A14 for some reason + AvatarWorkerTask.loadAvatar(message, viewHolder.contact_picture, R.dimen.avatar); + } + + resetClickListener(viewHolder.message_box, viewHolder.messageBody); + + viewHolder.message_box.setOnClickListener(v -> { + if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { + MessageAdapter.this.mOnMessageBoxClickedListener + .onContactPictureClicked(message); + } + }); + SwipeDetector swipeDetector = new SwipeDetector((action) -> { + if (action == SwipeDetector.Action.LR && MessageAdapter.this.mOnMessageBoxSwipedListener != null) { + MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message); + } + }); + viewHolder.message_box.setOnTouchListener(swipeDetector); + viewHolder.image.setOnTouchListener(swipeDetector); + viewHolder.time.setOnTouchListener(swipeDetector); + + // Treat touch-up as click so we don't have to touch twice + // (touch twice is because it's waiting to see if you double-touch for text selection) + viewHolder.messageBody.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP) { + if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { + MessageAdapter.this.mOnMessageBoxClickedListener + .onContactPictureClicked(message); + } + } + + swipeDetector.onTouch(v, event); + + return false; + }); + viewHolder.messageBody.setOnClickListener(v -> { + if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { + MessageAdapter.this.mOnMessageBoxClickedListener + .onContactPictureClicked(message); + } + }); + viewHolder.contact_picture.setOnClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); + } + + }); + viewHolder.contact_picture.setOnLongClickListener(v -> { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(v, message); + return true; + } else { + return false; + } + }); + viewHolder.messageBody.setAccessibilityDelegate(null); + + boolean footerWrap = false; + + final Transferable transferable = message.getTransferable(); + final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); + + final boolean muted = message.getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && activity.xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), message.getOccupantId(), null, null)); + if (muted) { + // Muted MUC participant + displayInfoMessage(viewHolder, "Muted", bubbleColor); + } else if (unInitiatedButKnownSize || message.isDeleted() || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING)) { + if (unInitiatedButKnownSize || (message.isDeleted() && message.getModerated() == null) || transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type); + } else if (transferable != null && transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor, type); + } else { + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor); + } + } else if (message.isFileOrImage() + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) { + if (message.getFileParams().width > 0 && message.getFileParams().height > 0) { + displayMediaPreviewMessage(viewHolder, message, bubbleColor, type); + if (!black && viewHolder.image.getLayoutParams().width > metrics.density * 110) { + footerWrap = true; + } + } else if (message.getFileParams().runtime > 0) { + displayAudioMessage(viewHolder, message, bubbleColor, type); + } else if ("application/xdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) { + displayWebxdcMessage(viewHolder, message, bubbleColor, type); + } else { + displayOpenableMessage(viewHolder, message, bubbleColor, type); + } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (account.isPgpDecryptionServiceConnected()) { + if (conversation instanceof Conversation + && !account.hasPendingPgpIntent((Conversation) conversation)) { + displayInfoMessage( + viewHolder, + activity.getString(R.string.message_decrypting), + bubbleColor); + } else { + displayInfoMessage( + viewHolder, activity.getString(R.string.pgp_message), bubbleColor); + } + } else { + displayInfoMessage( + viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor); + viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); + viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); + } + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + displayInfoMessage( + viewHolder, activity.getString(R.string.decryption_failed), bubbleColor); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE) { + displayInfoMessage( + viewHolder, + activity.getString(R.string.not_encrypted_for_this_device), + bubbleColor); + } else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL_FAILED) { + displayInfoMessage( + viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor); + } else { + if (message.isGeoUri()) { + displayLocationMessage(viewHolder, message, bubbleColor, type); + } else if (message.treatAsDownloadable()) { + try { + final URI uri = message.getOob(); + displayDownloadableMessage(viewHolder, + message, + activity.getString( + R.string.check_x_filesize_on_host, + UIHelper.getFileDescriptionString(activity, message), + uri.getHost()), + bubbleColor, type); + } catch (Exception e) { + displayDownloadableMessage( + viewHolder, + message, + activity.getString( + R.string.check_x_filesize, + UIHelper.getFileDescriptionString(activity, message)), + bubbleColor, type); + } + } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { + displayEmojiMessage(viewHolder, getSpannableBody(message), bubbleColor); + } else { + displayTextMessage(viewHolder, message, bubbleColor, message.getType()); + } + } + + viewHolder.message_box_inner.setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0); + LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.status_line.getLayoutParams(); + statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; + viewHolder.status_line.setLayoutParams(statusParams); + + setBackgroundTint(viewHolder.message_box, bubbleColor); + setTextColor(viewHolder.messageBody, bubbleColor); + viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor)); + + if (type == RECEIVED) { + if (!muted && commands != null && conversation instanceof Conversation) { + CommandButtonAdapter adapter = new CommandButtonAdapter(activity); + adapter.addAll(commands); + viewHolder.commands_list.setAdapter(adapter); + viewHolder.commands_list.setVisibility(View.VISIBLE); + viewHolder.commands_list.setOnItemClickListener((p, v, pos, id) -> { + final Element command = adapter.getItem(pos); + activity.startCommand(conversation.getAccount(), command.getAttributeAsJid("jid"), command.getAttribute("node")); + }); + } else { + // It's unclear if we can set this to null... + ListAdapter adapter = viewHolder.commands_list.getAdapter(); + if (adapter instanceof ArrayAdapter) { + ((ArrayAdapter) adapter).clear(); + } + viewHolder.commands_list.setVisibility(View.GONE); + viewHolder.commands_list.setOnItemClickListener(null); + } + + setTextColor(viewHolder.encryption, bubbleColor); + + if (isInValidSession) { + viewHolder.encryption.setVisibility(View.GONE); + } else { + viewHolder.encryption.setVisibility(View.VISIBLE); + if (omemoEncryption && !message.isTrusted()) { + viewHolder.encryption.setText(R.string.not_trusted); + } else { + viewHolder.encryption.setText( + CryptoHelper.encryptionTypeToText(message.getEncryption())); + } + } + } + + if (type == RECEIVED || type == SENT) { + String subject = message.getSubject(); + if (subject == null && message.getThread() != null) { + final var thread = ((Conversation) message.getConversation()).getThread(message.getThread().getContent()); + if (thread != null) subject = thread.getSubject(); + } + if (muted || subject == null) { + viewHolder.subject.setVisibility(View.GONE); + } else { + viewHolder.subject.setVisibility(View.VISIBLE); + viewHolder.subject.setText(subject); + } + } + + displayStatus(viewHolder, message, type, bubbleColor); + + viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public void sendAccessibilityEvent(View host, int eventType) { + super.sendAccessibilityEvent(host, eventType); + if (eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) { + if (viewHolder.messageBody.hasSelection()) { + selectionUuid = message.getUuid(); + } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) { + selectionUuid = null; + } + } + } + }); + + return view; + } + + private void promptOpenKeychainInstall(View view) { + activity.showInstallPgpDialog(); + } + + public FileBackend getFileBackend() { + return activity.xmppConnectionService.getFileBackend(); + } + + public void stopAudioPlayer() { + audioPlayer.stop(); + } + + public void unregisterListenerInAudioPlayer() { + audioPlayer.unregisterListener(); + } + + public void startStopPending() { + audioPlayer.startStopPending(); + } + + public void openDownloadable(Message message) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + && ContextCompat.checkSelfPermission( + activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ConversationFragment.registerPendingMessage(activity, message); + ActivityCompat.requestPermissions( + activity, + new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, + ConversationsActivity.REQUEST_OPEN_MESSAGE); + return; + } + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); + ViewUtil.view(activity, file); + } + + private void showLocation(Message message) { + for (Intent intent : GeoHelper.createGeoIntentsFromMessage(activity, message)) { + if (intent.resolveActivity(getContext().getPackageManager()) != null) { + getContext().startActivity(intent); + return; + } + } + Toast.makeText( + activity, + R.string.no_application_found_to_display_location, + Toast.LENGTH_SHORT) + .show(); + } + + public void updatePreferences() { + final AppSettings appSettings = new AppSettings(activity); + this.bubbleDesign = + new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont()); + } + + public void setHighlightedTerm(List terms) { + this.highlightedTerm = terms == null ? null : StylingHelper.filterHighlightedWords(terms); + } + + public interface OnContactPictureClicked { + void onContactPictureClicked(Message message); + } + + public interface OnContactPictureLongClicked { + void onContactPictureLongClicked(View v, Message message); + } + + public interface OnInlineImageLongClicked { + boolean onInlineImageLongClicked(Cid cid); + } + + private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) { + view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor)); + } + + private static ColorStateList bubbleToColorStateList( + final View view, final BubbleColor bubbleColor) { + final @AttrRes int colorAttributeResId = + switch (bubbleColor) { + case SURFACE -> Activities.isNightMode(view.getContext()) + ? com.google.android.material.R.attr.colorSurfaceContainerHigh + : com.google.android.material.R.attr.colorSurfaceContainerLow; + case SURFACE_HIGH -> Activities.isNightMode(view.getContext()) + ? com.google.android.material.R.attr.colorSurfaceContainerHighest + : com.google.android.material.R.attr.colorSurfaceContainerHigh; + case PRIMARY -> com.google.android.material.R.attr.colorPrimaryContainer; + case SECONDARY -> com.google.android.material.R.attr.colorSecondaryContainer; + case TERTIARY -> com.google.android.material.R.attr.colorTertiaryContainer; + case WARNING -> com.google.android.material.R.attr.colorErrorContainer; + }; + return ColorStateList.valueOf(MaterialColors.getColor(view, colorAttributeResId)); + } + + public static void setImageTint(final ImageView imageView, final BubbleColor bubbleColor) { + ImageViewCompat.setImageTintList( + imageView, bubbleToOnSurfaceColorStateList(imageView, bubbleColor)); + } + + public static void setImageTintError(final ImageView imageView) { + ImageViewCompat.setImageTintList( + imageView, + ColorStateList.valueOf( + MaterialColors.getColor( + imageView, com.google.android.material.R.attr.colorError))); + } + + public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) { + final var color = bubbleToOnSurfaceColor(textView, bubbleColor); + textView.setTextColor(color); + if (BubbleColor.SURFACES.contains(bubbleColor)) { + textView.setLinkTextColor( + MaterialColors.getColor( + textView, com.google.android.material.R.attr.colorPrimary)); + } else { + textView.setLinkTextColor(color); + } + } + + private static void setTextSize(final TextView textView, final boolean largeFont) { + if (largeFont) { + textView.setTextAppearance( + com.google.android.material.R.style.TextAppearance_Material3_TitleLarge); + } else { + textView.setTextAppearance( + com.google.android.material.R.style.TextAppearance_Material3_BodyMedium); + } + } + + private static @ColorInt int bubbleToOnSurfaceVariant( + final View view, final BubbleColor bubbleColor) { + final @AttrRes int colorAttributeResId; + if (BubbleColor.SURFACES.contains(bubbleColor)) { + colorAttributeResId = com.google.android.material.R.attr.colorOnSurfaceVariant; + } else { + colorAttributeResId = bubbleToOnSurface(bubbleColor); + } + return MaterialColors.getColor(view, colorAttributeResId); + } + + private static @ColorInt int bubbleToOnSurfaceColor( + final View view, final BubbleColor bubbleColor) { + return MaterialColors.getColor(view, bubbleToOnSurface(bubbleColor)); + } + + public static ColorStateList bubbleToOnSurfaceColorStateList( + final View view, final BubbleColor bubbleColor) { + return ColorStateList.valueOf(bubbleToOnSurfaceColor(view, bubbleColor)); + } + + private static @AttrRes int bubbleToOnSurface(final BubbleColor bubbleColor) { + return switch (bubbleColor) { + case SURFACE, SURFACE_HIGH -> com.google.android.material.R.attr.colorOnSurface; + case PRIMARY -> com.google.android.material.R.attr.colorOnPrimaryContainer; + case SECONDARY -> com.google.android.material.R.attr.colorOnSecondaryContainer; + case TERTIARY -> com.google.android.material.R.attr.colorOnTertiaryContainer; + case WARNING -> com.google.android.material.R.attr.colorOnErrorContainer; + }; + } + + public enum BubbleColor { + SURFACE, + SURFACE_HIGH, + PRIMARY, + SECONDARY, + TERTIARY, + WARNING; + + private static final Collection SURFACES = + Arrays.asList(BubbleColor.SURFACE, BubbleColor.SURFACE_HIGH); + } + + private static class BubbleDesign { + public final boolean colorfulChatBubbles; + public final boolean largeFont; + + private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) { + this.colorfulChatBubbles = colorfulChatBubbles; + this.largeFont = largeFont; + } + } + + private static class ViewHolder { + + public MaterialButton load_more_messages; + public ImageView edit_indicator; + public RelativeLayout audioPlayer; + protected View status_line; + protected LinearLayout message_box; + protected View message_box_inner; + protected MaterialButton download_button; + protected ShapeableImageView image; + protected ImageView indicator; + protected ImageView indicatorReceived; + protected TextView time; + protected TextView subject; + protected TextView messageBody; + protected ImageView contact_picture; + protected TextView status_message; + protected TextView encryption; + protected ListView commands_list; + protected GithubIdenticonView thread_identicon; + } + + class ThumbnailTask extends AsyncTask { + @Override + protected Drawable[] doInBackground(DownloadableFile... params) { + if (isCancelled()) return null; + + Drawable[] d = new Drawable[params.length]; + for (int i = 0; i < params.length; i++) { + try { + d[i] = activity.xmppConnectionService.getFileBackend().getThumbnail(params[i], activity.getResources(), (int) (metrics.density * 288), false); + } catch (final IOException e) { + d[i] = null; + } + } + + return d; + } + + @Override + protected void onPostExecute(final Drawable[] d) { + if (isCancelled()) return; + activity.xmppConnectionService.updateConversationUi(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index d6930c24e7a003f684e70ccdd97298fe3075c99b..b97746f222f52c2226bddb91bdf4dba0920c700a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -279,8 +279,8 @@ public class ConversationAdapter void onConversationClick(View view, Conversation conversation); } - static class ConversationViewHolder extends RecyclerView.ViewHolder { - private final ItemConversationBinding binding; + public static class ConversationViewHolder extends RecyclerView.ViewHolder { + public final ItemConversationBinding binding; private ConversationViewHolder(final ItemConversationBinding binding) { super(binding.getRoot()); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java index 7efaff3b02c50ca52537d252cce94fbe77f7c20b..c8f8417af3496f0efc70f6e2b7596aec8cc4252d 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -45,6 +45,14 @@ public class MediaAdapter extends RecyclerView.Adapter ARCHIVE_MIMES = + Arrays.asList( + "application/x-7z-compressed", + "application/zip", + "application/rar", + "application/x-gtar", + "application/x-tar"); public static final List CODE_MIMES = Arrays.asList("text/html", "text/xml"); private final ArrayList attachments = new ArrayList<>(); @@ -95,7 +103,7 @@ public class MediaAdapter extends RecyclerView.Adapter Toast.makeText(requireActivity(), "Blocked media will be displayed again", Toast.LENGTH_LONG).show()); return true; }); + + final ListPreference autoAcceptFileSize = findPreference("auto_accept_file_size"); + if (autoAcceptFileSize == null) { + throw new IllegalStateException("The preference resource file is missing preferences"); + } + setValues( + autoAcceptFileSize, + R.array.file_size_values, + value -> { + if (value <= 0) { + return getString(R.string.never); + } else { + return UIHelper.filesizeToString(value); + } + }); } protected void downloadStickers() { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java index f78577656d619357219dd94068dd67d968946464..739598a69009561a7f6a6bec4076fc6530d31c32 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java @@ -71,16 +71,10 @@ public class BackupSettingsFragment extends XmppPreferenceFragment { R.string.pref_create_backup_summary, FileBackend.getBackupDirectory(requireContext()).getAbsolutePath())); createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked); - final int[] choices = getResources().getIntArray(R.array.recurring_backup_values); - final CharSequence[] entries = new CharSequence[choices.length]; - final CharSequence[] entryValues = new CharSequence[choices.length]; - for (int i = 0; i < choices.length; ++i) { - entryValues[i] = String.valueOf(choices[i]); - entries[i] = timeframeValueToName(requireContext(), choices[i]); - } - recurringBackup.setEntries(entries); - recurringBackup.setEntryValues(entryValues); - recurringBackup.setSummaryProvider(new TimeframeSummaryProvider()); + setValues( + recurringBackup, + R.array.recurring_backup_values, + value -> timeframeValueToName(requireContext(), value)); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java index 9a526c75932103cfa64327b1569bc3166281d9b2..582ddec0b6ae0c939af71a8089915625e8aa3866 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java @@ -196,7 +196,12 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment { uri = appSettings().getRingtone(); } Log.i(Config.LOGTAG, "current ringtone: " + uri); - this.pickRingtoneLauncher.launch(uri); + try { + this.pickRingtoneLauncher.launch(uri); + } catch (final ActivityNotFoundException e) { + Toast.makeText(requireActivity(), R.string.no_application_found, Toast.LENGTH_LONG) + .show(); + } } private AppSettings appSettings() { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java index 0ccf2679e17d3069093148d73ba31af3dba127fc..2fa1bc3bee2e624d66a63af5991e8729e2d0dec4 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/SecuritySettingsFragment.java @@ -4,6 +4,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.widget.Toast; +import androidx.annotation.ArrayRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -11,7 +12,9 @@ import androidx.preference.ListPreference; import androidx.preference.Preference; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Function; import com.google.common.base.Strings; +import com.google.common.primitives.Ints; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.R; @@ -21,6 +24,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager; import java.security.KeyStoreException; import java.util.ArrayList; import java.util.Collections; +import java.util.concurrent.Callable; public class SecuritySettingsFragment extends XmppPreferenceFragment { @@ -36,19 +40,12 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment { throw new IllegalStateException("The preference resource file is missing preferences"); } omemo.setSummaryProvider(new OmemoSummaryProvider()); - final int[] choices = getResources().getIntArray(R.array.automatic_message_deletion_values); - final CharSequence[] entries = new CharSequence[choices.length]; - final CharSequence[] entryValues = new CharSequence[choices.length]; - for (int i = 0; i < choices.length; ++i) { - entryValues[i] = String.valueOf(choices[i]); - entries[i] = timeframeValueToName(requireContext(), choices[i]); - } - automaticMessageDeletion.setEntries(entries); - automaticMessageDeletion.setEntryValues(entryValues); - automaticMessageDeletion.setSummaryProvider(new TimeframeSummaryProvider()); + setValues( + automaticMessageDeletion, + R.array.automatic_message_deletion_values, + value -> timeframeValueToName(requireContext(), value)); } - @Override protected void onSharedPreferenceChanged(@NonNull String key) { super.onSharedPreferenceChanged(key); @@ -151,7 +148,6 @@ public class SecuritySettingsFragment extends XmppPreferenceFragment { .show(); } - private static class OmemoSummaryProvider implements Preference.SummaryProvider { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java index 183f2c4d23245d94ef2c71f8d899ea464ae06ac2..e026f25732c6ddeddbc3f01d064bf9de50a8f5d8 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/XmppPreferenceFragment.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.util.Log; +import androidx.annotation.ArrayRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.ListPreference; @@ -13,6 +14,7 @@ import androidx.preference.Preference; import com.rarepebble.colorpicker.ColorPreference; +import com.google.common.base.Function; import com.google.common.base.Strings; import com.google.common.primitives.Ints; @@ -111,14 +113,29 @@ public abstract class XmppPreferenceFragment extends PreferenceFragmentCompat { } } - protected static class TimeframeSummaryProvider - implements Preference.SummaryProvider { - - @Nullable - @Override - public CharSequence provideSummary(@NonNull ListPreference preference) { - final Integer value = Ints.tryParse(Strings.nullToEmpty(preference.getValue())); - return timeframeValueToName(preference.getContext(), value == null ? 0 : value); + protected void setValues( + final ListPreference listPreference, + @ArrayRes int resId, + final Function valueToName) { + final int[] choices = getResources().getIntArray(resId); + final CharSequence[] entries = new CharSequence[choices.length]; + final CharSequence[] entryValues = new CharSequence[choices.length]; + for (int i = 0; i < choices.length; ++i) { + final int value = choices[i]; + entryValues[i] = String.valueOf(choices[i]); + entries[i] = valueToName.apply(value); } + listPreference.setEntries(entries); + listPreference.setEntryValues(entryValues); + listPreference.setSummaryProvider( + new Preference.SummaryProvider() { + @Nullable + @Override + public CharSequence provideSummary(@NonNull ListPreference preference) { + final Integer value = + Ints.tryParse(Strings.nullToEmpty(preference.getValue())); + return valueToName.apply(value == null ? 0 : value); + } + }); } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java b/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java index 3772a851e5574e2b9e9db1ddc09ffc63ccfa0916..f46b9ea28119c82e23d0346888730b0553e2ca84 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MucConfiguration.java @@ -10,13 +10,13 @@ import eu.siacs.conversations.entities.MucOptions; public class MucConfiguration { - public final @StringRes - int title; + public final @StringRes int title; public final String[] names; public final boolean[] values; public final Option[] options; - private MucConfiguration(@StringRes int title, String[] names, boolean[] values, Option[] options) { + private MucConfiguration( + @StringRes int title, String[] names, boolean[] values, Option[] options) { this.title = title; this.names = names; this.values = values; @@ -25,52 +25,62 @@ public class MucConfiguration { public static MucConfiguration get(Context context, boolean advanced, MucOptions mucOptions) { if (mucOptions.isPrivateAndNonAnonymous()) { - String[] names = new String[]{ - context.getString(R.string.allow_participants_to_edit_subject), - context.getString(R.string.allow_participants_to_invite_others) - }; - boolean[] values = new boolean[]{ - mucOptions.participantsCanChangeSubject(), - mucOptions.allowInvites() - }; - final Option[] options = new Option[]{ - new Option("muc#roomconfig_changesubject"), - new Option("muc#roomconfig_allowinvites") - }; + String[] names = + new String[] { + context.getString(R.string.allow_participants_to_edit_subject), + context.getString(R.string.allow_participants_to_invite_others) + }; + boolean[] values = + new boolean[] { + mucOptions.participantsCanChangeSubject(), mucOptions.allowInvites() + }; + final Option[] options = + new Option[] { + new Option("muc#roomconfig_changesubject"), + new Option("muc#roomconfig_allowinvites") + }; return new MucConfiguration(R.string.conference_options, names, values, options); } else { final String[] names; final boolean[] values; final Option[] options; if (advanced) { - names = new String[]{ - context.getString(R.string.non_anonymous), - context.getString(R.string.allow_participants_to_edit_subject), - context.getString(R.string.moderated) - }; - values = new boolean[]{ - mucOptions.nonanonymous(), - mucOptions.participantsCanChangeSubject(), - mucOptions.moderated() - }; - options = new Option[]{ - new Option("muc#roomconfig_whois", "anyone", "moderators"), - new Option("muc#roomconfig_changesubject"), - new Option("muc#roomconfig_moderatedroom") - }; + names = + new String[] { + context.getString(R.string.non_anonymous), + context.getString(R.string.allow_participants_to_edit_subject), + context.getString(R.string.moderated), + context.getString(R.string.allow_private_messages) + }; + values = + new boolean[] { + mucOptions.nonanonymous(), + mucOptions.participantsCanChangeSubject(), + mucOptions.moderated(), + mucOptions.allowPmRaw() + }; + options = + new Option[] { + new Option("muc#roomconfig_whois", "anyone", "moderators"), + new Option("muc#roomconfig_changesubject"), + new Option("muc#roomconfig_moderatedroom"), + new Option("muc#roomconfig_allowpm", "anyone", "moderators"), + }; } else { - names = new String[]{ - context.getString(R.string.non_anonymous), - context.getString(R.string.allow_participants_to_edit_subject), - }; - values = new boolean[]{ - mucOptions.nonanonymous(), - mucOptions.participantsCanChangeSubject() - }; - options = new Option[]{ - new Option("muc#roomconfig_whois", "anyone", "moderators"), - new Option("muc#roomconfig_changesubject") - }; + names = + new String[] { + context.getString(R.string.non_anonymous), + context.getString(R.string.allow_participants_to_edit_subject), + }; + values = + new boolean[] { + mucOptions.nonanonymous(), mucOptions.participantsCanChangeSubject() + }; + options = + new Option[] { + new Option("muc#roomconfig_whois", "anyone", "moderators"), + new Option("muc#roomconfig_changesubject") + }; } return new MucConfiguration(R.string.channel_options, names, values, options); } @@ -108,9 +118,9 @@ public class MucConfiguration { public Bundle toBundle(boolean[] values) { Bundle bundle = new Bundle(); - for(int i = 0; i < values.length; ++i) { + for (int i = 0; i < values.length; ++i) { final Option option = options[i]; - bundle.putString(option.name,option.values[values[i] ? 0 : 1]); + bundle.putString(option.name, option.values[values[i] ? 0 : 1]); } return bundle; } @@ -121,13 +131,12 @@ public class MucConfiguration { private Option(String name) { this.name = name; - this.values = new String[]{"1","0"}; + this.values = new String[] {"1", "0"}; } private Option(String name, String on, String off) { this.name = name; - this.values = new String[]{on,off}; + this.values = new String[] {on, off}; } } - } diff --git a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java index fe70241f90faf1d4117b02d5c0a3b8819d024f3c..4b2c2957f62aaa1e1a2e6e814312a4bb7b136843 100644 --- a/src/main/java/eu/siacs/conversations/utils/AccountUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/AccountUtils.java @@ -38,23 +38,27 @@ public class AccountUtils { return false; } - public static String publicDeviceId(final Account account) { + public static String publicDeviceId(final Account account, final long installationId) { final UUID uuid; try { uuid = UUID.fromString(account.getUuid()); } catch (final IllegalArgumentException e) { return account.getUuid(); } + return createUuid4(uuid.getMostSignificantBits(), installationId).toString(); + } + + public static UUID createUuid4(long mostSigBits, long leastSigBits) { final byte[] bytes = Bytes.concat( - Longs.toByteArray(uuid.getLeastSignificantBits()), - Longs.toByteArray(uuid.getLeastSignificantBits())); + Longs.toByteArray(mostSigBits), + Longs.toByteArray(leastSigBits)); bytes[6] &= 0x0f; /* clear version */ bytes[6] |= 0x40; /* set to version 4 */ bytes[8] &= 0x3f; /* clear variant */ bytes[8] |= 0x80; /* set to IETF variant */ final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - return new UUID(byteBuffer.getLong(), byteBuffer.getLong()).toString(); + return new UUID(byteBuffer.getLong(), byteBuffer.getLong()); } public static List getEnabledAccounts(final XmppConnectionService service) { diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java index 8a77e04bb884b4441e5b9d40a9e819a0873bee9c..c585ad38fd8293cbc24742d9b583791e3b8095f2 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHandler.java @@ -1,36 +1,59 @@ package eu.siacs.conversations.utils; import android.content.Context; +import android.os.Build; import androidx.annotation.NonNull; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +import eu.siacs.conversations.BuildConfig; +import eu.siacs.conversations.services.NotificationService; + +import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; -import java.io.Writer; import java.lang.Thread.UncaughtExceptionHandler; - -import eu.siacs.conversations.services.NotificationService; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; public class ExceptionHandler implements UncaughtExceptionHandler { - private final UncaughtExceptionHandler defaultHandler; - private final Context context; - - ExceptionHandler(final Context context) { - this.context = context; - this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); - } - - @Override - public void uncaughtException(@NonNull Thread thread, final Throwable throwable) { - NotificationService.cancelIncomingCallNotification(context); - final Writer stringWriter = new StringWriter(); - final PrintWriter printWriter = new PrintWriter(stringWriter); - throwable.printStackTrace(printWriter); - final String stacktrace = stringWriter.toString(); - printWriter.close(); - ExceptionHelper.writeToStacktraceFile(context, stacktrace); - this.defaultHandler.uncaughtException(thread, throwable); - } - + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ENGLISH); + + private final UncaughtExceptionHandler defaultHandler; + private final Context context; + + ExceptionHandler(final Context context) { + this.context = context; + this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + @Override + public void uncaughtException(@NonNull Thread thread, final Throwable throwable) { + NotificationService.cancelIncomingCallNotification(context); + final String stacktrace; + try (final StringWriter stringWriter = new StringWriter(); + final PrintWriter printWriter = new PrintWriter(stringWriter)) { + throwable.printStackTrace(printWriter); + stacktrace = stringWriter.toString(); + } catch (final IOException e) { + return; + } + final List report = + ImmutableList.of( + String.format( + "Version: %s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME), + String.format("Manufacturer: %s", Strings.nullToEmpty(Build.MANUFACTURER)), + String.format("Device: %s", Strings.nullToEmpty(Build.DEVICE)), + String.format("Timestamp: %s", DATE_FORMAT.format(new Date())), + stacktrace); + ExceptionHelper.writeToStacktraceFile(context, Joiner.on("\n").join(report)); + this.defaultHandler.uncaughtException(thread, throwable); + } } diff --git a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java index ded53567ad94ae5aab26b1163a308ea0266e8d8b..6e4f397bbe9391c3809d6debc811fc9e8a3524a9 100644 --- a/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/ExceptionHelper.java @@ -1,12 +1,12 @@ package eu.siacs.conversations.utils; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; import android.util.Log; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Charsets; +import com.google.common.io.CharSink; +import com.google.common.io.Files; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; @@ -17,20 +17,16 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity; -import java.io.BufferedReader; -import java.io.FileInputStream; +import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.ClassNotFoundException; import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Locale; public class ExceptionHelper { private static final String FILENAME = "stacktrace.txt"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH); public static void init(final Context context) { if (Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler) { @@ -45,72 +41,65 @@ public class ExceptionHelper { return false; } catch (final ClassNotFoundException e) { } + final XmppConnectionService service = + activity == null ? null : activity.xmppConnectionService; + if (service == null) { + return false; + } + final AppSettings appSettings = new AppSettings(activity); + if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) { + return false; + } + final Account account = AccountUtils.getFirstEnabled(service); + if (account == null) { + return false; + } + final var file = new File(activity.getCacheDir(), FILENAME); + if (!file.exists()) { + return false; + } + final String report; try { - final XmppConnectionService service = activity == null ? null : activity.xmppConnectionService; - if (service == null) { - return false; - } - final AppSettings appSettings = new AppSettings(activity); - if (!appSettings.isSendCrashReports() || Config.BUG_REPORTS == null) { - return false; - } - final Account account = AccountUtils.getFirstEnabled(service); - if (account == null) { - return false; - } - final FileInputStream file = activity.openFileInput(FILENAME); - final InputStreamReader inputStreamReader = new InputStreamReader(file); - final BufferedReader stacktrace = new BufferedReader(inputStreamReader); - final StringBuilder report = new StringBuilder(); - final PackageManager pm = activity.getPackageManager(); - final PackageInfo packageInfo; - try { - packageInfo = pm.getPackageInfo(activity.getPackageName(), PackageManager.GET_SIGNATURES); - final String versionName = packageInfo.versionName; - final int versionCode = packageInfo.versionCode; - final int version = versionCode > 10000 ? (versionCode / 100) : versionCode; - report.append(String.format(Locale.ROOT, "Version: %s(%d)", versionName, version)).append('\n'); - report.append("Last Update: ").append(DATE_FORMAT.format(new Date(packageInfo.lastUpdateTime))).append('\n'); - Signature[] signatures = packageInfo.signatures; - if (signatures != null && signatures.length >= 1) { - report.append("SHA-1: ").append(CryptoHelper.getFingerprintCert(packageInfo.signatures[0].toByteArray())).append('\n'); - } - report.append('\n'); - } catch (final Exception e) { - return false; - } - String line; - while ((line = stacktrace.readLine()) != null) { - report.append(line); - report.append('\n'); - } - file.close(); - activity.deleteFile(FILENAME); - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); - builder.setTitle(activity.getString(R.string.crash_report_title, activity.getString(R.string.app_name))); - builder.setMessage(activity.getString(R.string.crash_report_message, activity.getString(R.string.app_name))); - builder.setPositiveButton(activity.getText(R.string.send_now), (dialog, which) -> { - - Log.d(Config.LOGTAG, "using account=" + account.getJid().asBareJid() + " to send in stack trace"); - Conversation conversation = service.findOrCreateConversation(account, Config.BUG_REPORTS, false, true); - Message message = new Message(conversation, report.toString(), Message.ENCRYPTION_NONE); - service.sendMessage(message); - }); - builder.setNegativeButton(activity.getText(R.string.send_never), (dialog, which) -> appSettings.setSendCrashReports(false)); - builder.create().show(); - return true; - } catch (final IOException ignored) { + report = Files.asCharSource(file, Charsets.UTF_8).read(); + } catch (final IOException e) { return false; } + if (file.delete()) { + Log.d(Config.LOGTAG, "deleted crash report file"); + } + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); + builder.setTitle( + activity.getString( + R.string.crash_report_title, activity.getString(R.string.app_name))); + builder.setMessage( + activity.getString( + R.string.crash_report_message, activity.getString(R.string.app_name))); + builder.setPositiveButton( + activity.getText(R.string.send_now), + (dialog, which) -> { + Log.d( + Config.LOGTAG, + "using account=" + + account.getJid().asBareJid() + + " to send in stack trace"); + Conversation conversation = + service.findOrCreateConversation( + account, Config.BUG_REPORTS, false, true); + Message message = new Message(conversation, report, Message.ENCRYPTION_NONE); + service.sendMessage(message); + }); + builder.setNegativeButton( + activity.getText(R.string.send_never), + (dialog, which) -> appSettings.setSendCrashReports(false)); + builder.create().show(); + return true; } - static void writeToStacktraceFile(Context context, String msg) { + static void writeToStacktraceFile(final Context context, final String msg) { try { - OutputStream os = context.openFileOutput(FILENAME, Context.MODE_PRIVATE); - os.write(msg.getBytes()); - os.flush(); - os.close(); - } catch (IOException ignored) { + Files.asCharSink(new File(context.getCacheDir(), FILENAME), Charsets.UTF_8).write(msg); + } catch (IOException e) { + Log.w(Config.LOGTAG, "could not write stack trace to file", e); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 4c53df6692433e2ed20b93f0a236d859b6cba133..90dfe137aca81cf872c6e924fa3d5b1a28c784ee 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -141,6 +141,7 @@ public final class MimeUtils { add("application/vnd.sun.xml.writer.global", "sxg"); add("application/vnd.sun.xml.writer.template", "stw"); add("application/vnd.visio", "vsd"); + add("application/x-7z-compressed","7z"); add("application/x-abiword", "abw"); add("application/x-apple-diskimage", "dmg"); add("application/x-bcpio", "bcpio"); diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 236a9b41c6ac78f85b1434cf9ffb54de605b60f9..00fffea9ba416c174a7eb25fa957935f1a0e32ea 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -6,24 +6,56 @@ 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.Strings; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Ordering; import com.google.common.net.InetAddresses; import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.Conversations; +import eu.siacs.conversations.xmpp.Jid; + +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsname.InvalidDnsNameException; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.A; +import org.minidns.record.AAAA; +import org.minidns.record.CNAME; +import org.minidns.record.Data; +import org.minidns.record.InternetAddressRR; +import org.minidns.record.Record; +import org.minidns.record.SRV; import java.io.IOException; -import java.lang.reflect.Field; import java.net.Inet4Address; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.Jid; -//import de.gultsch.minidns.AndroidDNSClient; import org.minidns.AbstractDnsClient; import org.minidns.DnsCache; import org.minidns.DnsClient; @@ -44,13 +76,35 @@ import org.minidns.record.Data; import org.minidns.record.InternetAddressRR; import org.minidns.record.Record; import org.minidns.record.SRV; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.xmpp.Jid; public class Resolver { + private static final Comparator RESULT_COMPARATOR = + (left, right) -> { + if (left.priority == right.priority) { + if (left.directTls == right.directTls) { + if (left.ip == null && right.ip == null) { + return 0; + } else if (left.ip != null && right.ip != null) { + if (left.ip instanceof Inet4Address + && right.ip instanceof Inet4Address) { + return 0; + } else { + return left.ip instanceof Inet4Address ? -1 : 1; + } + } else { + return left.ip != null ? -1 : 1; + } + } else { + return left.directTls ? -1 : 1; + } + } else { + return left.priority - right.priority; + } + }; + + private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12); + public static final int DEFAULT_PORT_XMPP = 5222; private static final String DIRECT_TLS_SERVICE = "_xmpps-client"; @@ -203,7 +257,7 @@ public class Resolver { try { DnsName.from(hostname); return false; - } catch (IllegalArgumentException e) { + } catch (final InvalidDnsNameException | IllegalArgumentException e) { return true; } } @@ -224,206 +278,234 @@ public class Resolver { } } - public static boolean useDirectTls(final int port) { return port == 443 || port == 5223; } public static List resolve(final String domain) { - final List ipResults = fromIpAddress(domain); - if (ipResults.size() > 0) { + final List ipResults = fromIpAddress(domain); + if (!ipResults.isEmpty()) { return ipResults; } - final List results = new ArrayList<>(); - final List fallbackResults = new ArrayList<>(); - final Thread[] threads = new Thread[3]; - threads[0] = new Thread(() -> { - try { - final List list = resolveSrv(domain, true); - synchronized (results) { - results.addAll(list); - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (direct TLS)", throwable); - } - } - }); - threads[1] = new Thread(() -> { - try { - final List list = resolveSrv(domain, false); - synchronized (results) { - results.addAll(list); - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving SRV record (STARTTLS)", throwable); - } - } - }); - threads[2] = new Thread(() -> { - List list = resolveNoSrvRecords(DnsName.from(domain), true); - synchronized (fallbackResults) { - fallbackResults.addAll(list); - } - }); - for (final Thread thread : threads) { - thread.start(); - } + + final var startTls = resolveSrvAsFuture(domain, false); + final var directTls = resolveSrvAsFuture(domain, true); + + final var combined = merge(ImmutableList.of(startTls, directTls)); + + final var combinedWithFallback = + Futures.transformAsync( + combined, + results -> { + if (results.isEmpty()) { + return resolveNoSrvAsFuture(DnsName.from(domain), true); + } else { + return Futures.immediateFuture(results); + } + }, + MoreExecutors.directExecutor()); + final var orderedFuture = + Futures.transform( + combinedWithFallback, + all -> Ordering.from(RESULT_COMPARATOR).immutableSortedCopy(all), + MoreExecutors.directExecutor()); try { - threads[0].join(); - threads[1].join(); - if (results.size() > 0) { - threads[2].interrupt(); - synchronized (results) { - Collections.sort(results); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results); - return results; - } - } else { - threads[2].join(); - synchronized (fallbackResults) { - Collections.sort(fallbackResults); - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults); - return fallbackResults; - } - } - } catch (InterruptedException e) { - for (Thread thread : threads) { - thread.interrupt(); - } + final var ordered = orderedFuture.get(); + Log.d(Config.LOGTAG, "Resolver (" + ordered.size() + "): " + ordered); + return ordered; + } catch (final ExecutionException e) { + Log.d(Config.LOGTAG, "error resolving DNS", e); + return Collections.emptyList(); + } catch (final InterruptedException e) { + Log.d(Config.LOGTAG, "DNS resolution interrupted"); return Collections.emptyList(); } } - private static List fromIpAddress(String domain) { - if (!IP.matches(domain)) { - return Collections.emptyList(); - } - try { - Result result = new Result(); - result.ip = InetAddress.getByName(domain); + private static List fromIpAddress(final String domain) { + if (IP.matches(domain)) { + final InetAddress inetAddress; + try { + inetAddress = InetAddress.getByName(domain); + } catch (final UnknownHostException e) { + return Collections.emptyList(); + } + final Result result = new Result(); + result.ip = inetAddress; result.port = DEFAULT_PORT_XMPP; - result.authenticated = true; return Collections.singletonList(result); - } catch (UnknownHostException e) { + } else { return Collections.emptyList(); } } - private static List resolveSrv(String domain, final boolean directTls) throws IOException { - final String dnsNameS = (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain; - DnsName dnsName = DnsName.from(dnsNameS); - ResolverResult result = resolveWithFallback(dnsName, SRV.class); - final List results = new ArrayList<>(); - final List threads = new ArrayList<>(); - for (SRV record : result.getAnswersOrEmptySet()) { - if (record.name.length() == 0 && record.priority == 0) { - continue; - } - final boolean authentic = result.isAuthenticData() || record.target.toString().equals(knownSRV.get(dnsNameS)); - threads.add(new Thread(() -> { - final List ipv4s = resolveIp(record, A.class, authentic, directTls); - if (ipv4s.size() == 0) { - Result resolverResult = Result.fromRecord(record, directTls); - resolverResult.authenticated = result.isAuthenticData(); - ipv4s.add(resolverResult); - } - synchronized (results) { - results.addAll(ipv4s); - } + private static ListenableFuture> resolveSrvAsFuture( + final String domain, final boolean directTls) { + final DnsName dnsName = + DnsName.from( + (directTls ? DIRECT_TLS_SERVICE : STARTTLS_SERVICE) + "._tcp." + domain); + final var resultFuture = resolveAsFuture(dnsName, SRV.class); + return Futures.transformAsync( + resultFuture, + result -> resolveIpsAsFuture(result, directTls), + MoreExecutors.directExecutor()); + } - })); - threads.add(new Thread(() -> { - final List ipv6s = resolveIp(record, AAAA.class, authentic, directTls); - synchronized (results) { - results.addAll(ipv6s); - } - })); - } - for (Thread thread : threads) { - thread.start(); - } - for (Thread thread : threads) { - try { - thread.join(); - } catch (InterruptedException e) { - return Collections.emptyList(); + @NonNull + private static ListenableFuture> resolveIpsAsFuture( + final ResolverResult srvResolverResult, final boolean directTls) { + final ImmutableList.Builder>> futuresBuilder = + new ImmutableList.Builder<>(); + for (final SRV record : srvResolverResult.getAnswersOrEmptySet()) { + if (record.target.length() == 0 && record.priority == 0) { + continue; } + final var ipv4sRaw = + resolveIpsAsFuture( + record, A.class, srvResolverResult.isAuthenticData(), directTls); + final var ipv4s = + Futures.transform( + ipv4sRaw, + results -> { + if (results.isEmpty()) { + final Result resolverResult = + Result.fromRecord(record, directTls); + resolverResult.authenticated = + srvResolverResult.isAuthenticData(); + return Collections.singletonList(resolverResult); + } else { + return results; + } + }, + MoreExecutors.directExecutor()); + final var ipv6s = + resolveIpsAsFuture( + record, AAAA.class, srvResolverResult.isAuthenticData(), directTls); + futuresBuilder.add(ipv4s); + futuresBuilder.add(ipv6s); } - return results; + final ImmutableList>> futures = futuresBuilder.build(); + return merge(futures); } - private static List resolveIp(SRV srv, Class type, boolean authenticated, boolean directTls) { - List list = new ArrayList<>(); - try { - ResolverResult results = resolveWithFallback(srv.target, type); - for (D record : results.getAnswersOrEmptySet()) { - Result resolverResult = Result.fromRecord(srv, directTls); - resolverResult.authenticated = results.isAuthenticData() && authenticated; - resolverResult.ip = record.getInetAddress(); - list.add(resolverResult); - } - } catch (Throwable t) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " " + t.getMessage()); - } - return list; + private static ListenableFuture> merge( + final Collection>> futures) { + return Futures.transform( + Futures.successfulAsList(futures), + lists -> { + final var builder = new ImmutableList.Builder(); + for (final Collection list : lists) { + if (list == null) { + continue; + } + builder.addAll(list); + } + return builder.build(); + }, + MoreExecutors.directExecutor()); } - private static List resolveNoSrvRecords(DnsName dnsName, boolean withCnames) { - final List results = new ArrayList<>(); - try { - ResolverResult aResult = resolveWithFallback(dnsName, A.class); - for (A a : aResult.getAnswersOrEmptySet()) { - Result r = Result.createDefault(dnsName, a.getInetAddress()); - r.authenticated = aResult.isAuthenticData(); - results.add(r); - } - ResolverResult aaaaResult = resolveWithFallback(dnsName, AAAA.class); - for (AAAA aaaa : aaaaResult.getAnswersOrEmptySet()) { - Result r = Result.createDefault(dnsName, aaaa.getInetAddress()); - r.authenticated = aaaaResult.isAuthenticData(); - results.add(r); - } - if (results.size() == 0 && withCnames) { - ResolverResult cnameResult = resolveWithFallback(dnsName, CNAME.class); - for (CNAME cname : cnameResult.getAnswersOrEmptySet()) { - for (Result r : resolveNoSrvRecords(cname.name, false)) { - r.authenticated = r.authenticated && cnameResult.isAuthenticData(); - results.add(r); + private static > + ListenableFuture> resolveIpsAsFuture( + final SRV srv, Class type, boolean authenticated, boolean directTls) { + final var resultFuture = resolveAsFuture(srv.target, type); + return Futures.transform( + resultFuture, + result -> { + final var builder = new ImmutableList.Builder(); + for (D record : result.getAnswersOrEmptySet()) { + Result resolverResult = Result.fromRecord(srv, directTls); + resolverResult.authenticated = + result.isAuthenticData() + && authenticated; // TODO technically it does not matter if + // the IP + // was authenticated + resolverResult.ip = record.getInetAddress(); + builder.add(resolverResult); } - } - } - } catch (final Throwable throwable) { - if (!(Throwables.getRootCause(throwable) instanceof InterruptedException)) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + "error resolving fallback records", throwable); - } - } - results.add(Result.createDefault(dnsName)); - return results; + return builder.build(); + }, + MoreExecutors.directExecutor()); } - private static ResolverResult resolveWithFallback(DnsName dnsName, Class type) throws IOException { - final Question question = new Question(dnsName, Record.TYPE.getType(type)); - if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) { - try { - ResolverResult result = DnssecResolverApi.INSTANCE.resolve(question); - if (result.wasSuccessful() && !result.isAuthenticData()) { - Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons()); - } - return result; - } catch (DnssecValidationFailedException e) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e); - } catch (IOException e) { - throw e; - } catch (Throwable throwable) { - Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable); - } + private static ListenableFuture> resolveNoSrvAsFuture( + final DnsName dnsName, boolean cName) { + final ImmutableList.Builder>> futuresBuilder = + new ImmutableList.Builder<>(); + ListenableFuture> aRecordResults = + Futures.transform( + resolveAsFuture(dnsName, A.class), + result -> + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + a -> Result.createDefault(dnsName, a.getInetAddress(), result.isAuthenticData())), + MoreExecutors.directExecutor()); + futuresBuilder.add(aRecordResults); + ListenableFuture> aaaaRecordResults = + Futures.transform( + resolveAsFuture(dnsName, AAAA.class), + result -> + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + aaaa -> + Result.createDefault( + dnsName, aaaa.getInetAddress(), result.isAuthenticData())), + MoreExecutors.directExecutor()); + futuresBuilder.add(aaaaRecordResults); + if (cName) { + ListenableFuture> cNameRecordResults = + Futures.transformAsync( + resolveAsFuture(dnsName, CNAME.class), + result -> { + Collection>> test = + Lists.transform( + ImmutableList.copyOf(result.getAnswersOrEmptySet()), + cname -> resolveNoSrvAsFuture(cname.target, false)); + return merge(test); + }, + MoreExecutors.directExecutor()); + futuresBuilder.add(cNameRecordResults); } - return ResolverApi.INSTANCE.resolve(question); + final ImmutableList>> futures = futuresBuilder.build(); + final var noSrvFallbacks = merge(futures); + return Futures.transform( + noSrvFallbacks, + results -> { + if (results.isEmpty()) { + return Collections.singletonList(Result.createDefault(dnsName)); + } else { + return results; + } + }, + MoreExecutors.directExecutor()); + } + + private static ListenableFuture> resolveAsFuture( + final DnsName dnsName, final Class type) { + return Futures.submit( + () -> { + final Question question = new Question(dnsName, Record.TYPE.getType(type)); + if (!DNSSECLESS_TLDS.contains(dnsName.getLabels()[0].toString())) { + try { + ResolverResult result = DnssecResolverApi.INSTANCE.resolve(question); + if (result.wasSuccessful() && !result.isAuthenticData()) { + Log.d(Config.LOGTAG, "DNSSEC validation failed for " + type.getSimpleName() + " : " + result.getUnverifiedReasons()); + } + return result; + } catch (DnssecValidationFailedException e) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", e); + } catch (IOException e) { + throw e; + } catch (Throwable throwable) { + Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": error resolving " + type.getSimpleName() + " with DNSSEC. Trying DNS instead.", throwable); + } + } + return ResolverApi.INSTANCE.resolve(question); + }, + DNS_QUERY_EXECUTOR); } - public static class Result implements Comparable { + public static class Result { public static final String DOMAIN = "domain"; public static final String IP = "ip"; public static final String HOSTNAME = "hostname"; @@ -438,40 +520,42 @@ public class Resolver { private boolean authenticated = false; private int priority; - static Result fromRecord(SRV srv, boolean directTls) { - Result result = new Result(); + static Result fromRecord(final SRV srv, final boolean directTls) { + final Result result = new Result(); result.port = srv.port; - result.hostname = srv.name; + result.hostname = srv.target; result.directTls = directTls; result.priority = srv.priority; return result; } - static Result createDefault(DnsName hostname, InetAddress ip) { + static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) { Result result = new Result(); result.port = DEFAULT_PORT_XMPP; result.hostname = hostname; result.ip = ip; + result.authenticated = authenticated; return result; } - static Result createDefault(DnsName hostname) { - return createDefault(hostname, null); + static Result createDefault(final DnsName hostname) { + return createDefault(hostname, null, false); } - public static Result fromCursor(Cursor cursor) { + public static Result fromCursor(final Cursor cursor) { final Result result = new Result(); try { - result.ip = InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndex(IP))); - } catch (UnknownHostException e) { + result.ip = + InetAddress.getByAddress(cursor.getBlob(cursor.getColumnIndexOrThrow(IP))); + } catch (final UnknownHostException e) { result.ip = null; } - final String hostname = cursor.getString(cursor.getColumnIndex(HOSTNAME)); + final String hostname = cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)); result.hostname = hostname == null ? null : DnsName.from(hostname); - result.port = cursor.getInt(cursor.getColumnIndex(PORT)); - result.priority = cursor.getInt(cursor.getColumnIndex(PRIORITY)); - result.authenticated = cursor.getInt(cursor.getColumnIndex(AUTHENTICATED)) > 0; - result.directTls = cursor.getInt(cursor.getColumnIndex(DIRECT_TLS)) > 0; + result.port = cursor.getInt(cursor.getColumnIndexOrThrow(PORT)); + result.priority = cursor.getInt(cursor.getColumnIndexOrThrow(PRIORITY)); + result.authenticated = cursor.getInt(cursor.getColumnIndexOrThrow(AUTHENTICATED)) > 0; + result.directTls = cursor.getInt(cursor.getColumnIndexOrThrow(DIRECT_TLS)) > 0; return result; } @@ -479,26 +563,18 @@ public class Resolver { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Result result = (Result) o; - - if (port != result.port) return false; - if (directTls != result.directTls) return false; - if (authenticated != result.authenticated) return false; - if (priority != result.priority) return false; - if (ip != null ? !ip.equals(result.ip) : result.ip != null) return false; - return hostname != null ? hostname.equals(result.hostname) : result.hostname == null; + return port == result.port + && directTls == result.directTls + && authenticated == result.authenticated + && priority == result.priority + && Objects.equal(ip, result.ip) + && Objects.equal(hostname, result.hostname); } @Override public int hashCode() { - int result = ip != null ? ip.hashCode() : 0; - result = 31 * result + (hostname != null ? hostname.hashCode() : 0); - result = 31 * result + port; - result = 31 * result + (directTls ? 1 : 0); - result = 31 * result + (authenticated ? 1 : 0); - result = 31 * result + priority; - return result; + return Objects.hashCode(ip, hostname, port, directTls, authenticated, priority); } public InetAddress getIp() { @@ -522,38 +598,16 @@ public class Resolver { } @Override + @NonNull public String toString() { - return "Result{" + - "ip='" + (ip == null ? null : ip.getHostAddress()) + '\'' + - ", hostame='" + (hostname == null ? null : hostname.toString()) + '\'' + - ", port=" + port + - ", directTls=" + directTls + - ", authenticated=" + authenticated + - ", priority=" + priority + - '}'; - } - - @Override - public int compareTo(@NonNull Result result) { - if (result.priority == priority) { - if (directTls == result.directTls) { - if (ip == null && result.ip == null) { - return 0; - } else if (ip != null && result.ip != null) { - if (ip instanceof Inet4Address && result.ip instanceof Inet4Address) { - return 0; - } else { - return ip instanceof Inet4Address ? -1 : 1; - } - } else { - return ip != null ? -1 : 1; - } - } else { - return directTls ? 1 : -1; - } - } else { - return priority - result.priority; - } + return MoreObjects.toStringHelper(this) + .add("ip", ip) + .add("hostname", hostname) + .add("port", port) + .add("directTls", directTls) + .add("authenticated", authenticated) + .add("priority", priority) + .toString(); } public ContentValues toContentValues() { @@ -626,5 +680,4 @@ public class Resolver { return result; } } - } diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java index 1b36d554034c29107f7a357fe1e123365622662b..2ea69d78e0663d78d0c9a8935168b57bcccb1674 100644 --- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java +++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java @@ -18,6 +18,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.work.ForegroundInfo; +import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; @@ -35,7 +36,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.receiver.WorkManagerEventReceiver; import eu.siacs.conversations.utils.BackupFileHeader; import eu.siacs.conversations.utils.Compatibility; @@ -99,6 +99,7 @@ public class ExportBackupWorker extends Worker { @NonNull @Override public Result doWork() { + setForegroundAsync(getForegroundInfo()); final List files; try { files = export(); @@ -227,18 +228,14 @@ public class ExportBackupWorker extends Worker { IV, salt); final var notification = getNotification(); - if (!recurringBackup) { - final var cancel = new Intent(context, WorkManagerEventReceiver.class); - cancel.setAction(WorkManagerEventReceiver.ACTION_STOP_BACKUP); - final var cancelPendingIntent = - PendingIntent.getBroadcast(context, 197, cancel, PENDING_INTENT_FLAGS); - notification.addAction( - new NotificationCompat.Action.Builder( - R.drawable.ic_cancel_24dp, - context.getString(R.string.cancel), - cancelPendingIntent) - .build()); - } + final var cancelPendingIntent = + WorkManager.getInstance(context).createCancelPendingIntent(getId()); + notification.addAction( + new NotificationCompat.Action.Builder( + R.drawable.ic_cancel_24dp, + context.getString(R.string.cancel), + cancelPendingIntent) + .build()); final Progress progress = new Progress(notification, max, count); final File directory = file.getParentFile(); if (directory != null && directory.mkdirs()) { diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index ccdc688fe1d4604619a5163344e8acdc51ce786d..45431f0ffcdc0711752b1594d38b2bd722c466bd 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -5,7 +5,9 @@ import androidx.annotation.NonNull; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.base.Strings; import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; import java.util.ArrayList; import java.util.Collection; @@ -16,7 +18,7 @@ import java.util.stream.Collectors; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xmpp.InvalidJid; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Message; public class Element implements Node { private final String name; @@ -141,6 +143,10 @@ public class Element implements Node { return ImmutableList.copyOf(this.children); } + public void setAttribute(final String name, final boolean value) { + this.setAttribute(name, value ? "1" : "0"); + } + // Deprecated: you probably want bindTo or replaceChildren public Element setChildren(List children) { this.childNodes = new ArrayList(children); @@ -165,6 +171,31 @@ public class Element implements Node { return this.childNodes.stream().map(Node::getContent).filter(c -> c != null).collect(Collectors.joining()); } + public long getLongAttribute(final String name) { + final var value = Longs.tryParse(Strings.nullToEmpty(this.attributes.get(name))); + return value == null ? 0 : value; + } + + public Optional getOptionalIntAttribute(final String name) { + final String value = getAttribute(name); + if (value == null) { + return Optional.absent(); + } + return Optional.fromNullable(Ints.tryParse(value)); + } + + public Jid getAttributeAsJid(String name) { + final String jid = this.getAttribute(name); + if (jid != null && !jid.isEmpty()) { + try { + return Jid.ofEscaped(jid); + } catch (final IllegalArgumentException e) { + return InvalidJid.of(jid, this instanceof Message); + } + } + return null; + } + public Element setAttribute(String name, String value) { if (name != null && value != null) { this.attributes.put(name, value); @@ -224,7 +255,7 @@ public class Element implements Node { return result; } - public Element removeAttribute(String name) { + public Element removeAttribute(final String name) { this.attributes.remove(name); return this; } @@ -242,26 +273,6 @@ public class Element implements Node { } } - public Optional getOptionalIntAttribute(final String name) { - final String value = getAttribute(name); - if (value == null) { - return Optional.absent(); - } - return Optional.fromNullable(Ints.tryParse(value)); - } - - public Jid getAttributeAsJid(String name) { - final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.ofEscaped(jid); - } catch (final IllegalArgumentException e) { - return InvalidJid.of(jid, this instanceof MessagePacket); - } - } - return null; - } - public Hashtable getAttributes() { return this.attributes; } diff --git a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java index 8afd52c4c8daa6f26c51e3cde502c18cbb214b72..a02041acb90f9f16077047995e5473a9f2a19721 100644 --- a/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java +++ b/src/main/java/eu/siacs/conversations/xml/LocalizedContent.java @@ -37,7 +37,7 @@ public class LocalizedContent { } } } - if (contents.size() == 0) { + if (contents.isEmpty()) { return null; } final String userLanguage = Locale.getDefault().getLanguage(); diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index e10b6bb941fdccb765cd1f72d7d5948a8b049ba4..00df32651dff90b07a4850b233302c9ea63c0b1e 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -1,8 +1,29 @@ package eu.siacs.conversations.xml; public final class Namespace { + public static final String ADDRESSING = "http://jabber.org/protocol/address"; + public static final String AXOLOTL = "eu.siacs.conversations.axolotl"; + public static final String PGP_SIGNED = "jabber:x:signed"; + public static final String PGP_ENCRYPTED = "jabber:x:encrypted"; + public static final String AXOLOTL_BUNDLES = AXOLOTL + ".bundles"; + public static final String AXOLOTL_DEVICE_LIST = AXOLOTL + ".devicelist"; + public static final String HINTS = "urn:xmpp:hints"; + public static final String MESSAGE_ARCHIVE_MANAGEMENT = "urn:xmpp:mam:2"; + public static final String VERSION = "jabber:iq:version"; + public static final String LAST_MESSAGE_CORRECTION = "urn:xmpp:message-correct:0"; + public static final String RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm"; + public static final String CHAT_MARKERS = "urn:xmpp:chat-markers:0"; + public static final String CHAT_STATES = "http://jabber.org/protocol/chatstates"; + public static final String DELIVERY_RECEIPTS = "urn:xmpp:receipts"; + public static final String REACTIONS = "urn:xmpp:reactions:0"; + public static final String VCARD_TEMP = "vcard-temp"; + public static final String VCARD_TEMP_UPDATE = "vcard-temp:x:update"; + public static final String DELAY = "urn:xmpp:delay"; + public static final String OCCUPANT_ID = "urn:xmpp:occupant-id:0"; public static final String STREAMS = "http://etherx.jabber.org/streams"; + public static final String STANZAS = "urn:ietf:params:xml:ns:xmpp-stanzas"; public static final String JABBER_CLIENT = "jabber:client"; + public static final String FORWARD = "urn:xmpp:forward:0"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; @@ -23,12 +44,15 @@ public final class Namespace { public static final String FAST = "urn:xmpp:fast:0"; public static final String TLS = "urn:ietf:params:xml:ns:xmpp-tls"; public static final String PUBSUB = "http://jabber.org/protocol/pubsub"; + public static final String PUBSUB_EVENT = PUBSUB + "#event"; + public static final String MUC = "http://jabber.org/protocol/muc"; public static final String PUBSUB_PUBLISH_OPTIONS = PUBSUB + "#publish-options"; public static final String PUBSUB_CONFIG_NODE_MAX = PUBSUB + "#config-node-max"; public static final String PUBSUB_ERROR = PUBSUB + "#errors"; public static final String PUBSUB_OWNER = PUBSUB + "#owner"; public static final String NICK = "http://jabber.org/protocol/nick"; - public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = "http://jabber.org/protocol/offline"; + public static final String FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL = + "http://jabber.org/protocol/offline"; public static final String BIND = "urn:ietf:params:xml:ns:xmpp-bind"; public static final String BIND2 = "urn:xmpp:bind:0"; public static final String STREAM_MANAGEMENT = "urn:xmpp:sm:3"; @@ -38,7 +62,7 @@ public final class Namespace { public static final String BOOKMARKS = "storage:bookmarks"; public static final String SYNCHRONIZATION = "im.quicksy.synchronization:0"; public static final String AVATAR_DATA = "urn:xmpp:avatar:data"; - public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; + public static final String AVATAR_METADATA = "urn:xmpp:avatar:metadata"; public static final String AVATAR_CONVERSION = "urn:xmpp:pep-vcard-conversion:0"; public static final String JINGLE = "urn:xmpp:jingle:1"; public static final String JINGLE_ERRORS = "urn:xmpp:jingle:errors:1"; @@ -48,7 +72,8 @@ public final class Namespace { public static final String JINGLE_TRANSPORTS_S5B = "urn:xmpp:jingle:transports:s5b:1"; public static final String JINGLE_TRANSPORTS_IBB = "urn:xmpp:jingle:transports:ibb:1"; public static final String JINGLE_TRANSPORT_ICE_UDP = "urn:xmpp:jingle:transports:ice-udp:1"; - public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = "urn:xmpp:jingle:transports:webrtc-datachannel:1"; + public static final String JINGLE_TRANSPORT_WEBRTC_DATA_CHANNEL = + "urn:xmpp:jingle:transports:webrtc-datachannel:1"; public static final String JINGLE_TRANSPORT = "urn:xmpp:jingle:transports:dtls-sctp:1"; public static final String JINGLE_APPS_RTP = "urn:xmpp:jingle:apps:rtp:1"; @@ -57,9 +82,12 @@ public final class Namespace { public static final String JINGLE_APPS_GROUPING = "urn:xmpp:jingle:apps:grouping:0"; public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio"; public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video"; - public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; - public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; - public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = "urn:xmpp:jingle:apps:rtp:ssma:0"; + public static final String JINGLE_RTP_HEADER_EXTENSIONS = + "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = + "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + public static final String JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES = + "urn:xmpp:jingle:apps:rtp:ssma:0"; public static final String IBB = "http://jabber.org/protocol/ibb"; public static final String PING = "urn:xmpp:ping"; public static final String PUSH = "urn:xmpp:push:0"; @@ -70,8 +98,10 @@ public final class Namespace { public static final String INVITE = "urn:xmpp:invite"; public static final String PARS = "urn:xmpp:pars:0"; public static final String EASY_ONBOARDING_INVITE = "urn:xmpp:invite#invite"; - public static final String OMEMO_DTLS_SRTP_VERIFICATION = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; - public static final String JINGLE_TRANSPORT_ICE_OPTION = "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option"; + public static final String OMEMO_DTLS_SRTP_VERIFICATION = + "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"; + public static final String JINGLE_TRANSPORT_ICE_OPTION = + "http://gultsch.de/xmpp/drafts/jingle/transports/ice-udp/option"; public static final String UNIFIED_PUSH = "http://gultsch.de/xmpp/drafts/unified-push"; public static final String VCARD4 = "urn:ietf:params:xml:ns:vcard-4.0"; public static final String REPORTING = "urn:xmpp:reporting:1"; @@ -80,4 +110,7 @@ public final class Namespace { public static final String HASHES = "urn:xmpp:hashes:2"; public static final String MDS_DISPLAYED = "urn:xmpp:mds:displayed:0"; public static final String MDS_SERVER_ASSIST = "urn:xmpp:mds:server-assist:0"; + + public static final String ENTITY_CAPABILITIES = "http://jabber.org/protocol/caps"; + public static final String ENTITY_CAPABILITIES_2 = "urn:xmpp:caps"; } diff --git a/src/main/java/eu/siacs/conversations/xml/TagWriter.java b/src/main/java/eu/siacs/conversations/xml/TagWriter.java index 5a9f3317c4bfae527ca498f915466bcd947fad97..2401c612ca8c7a57882425148b0c0c06e3bdea31 100644 --- a/src/main/java/eu/siacs/conversations/xml/TagWriter.java +++ b/src/main/java/eu/siacs/conversations/xml/TagWriter.java @@ -10,13 +10,14 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import eu.siacs.conversations.Config; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import im.conversations.android.xmpp.model.StreamElement; public class TagWriter { private OutputStreamWriter outputStream; private boolean finished = false; - private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue(); + + private final LinkedBlockingQueue writeQueue = new LinkedBlockingQueue<>(); private CountDownLatch stanzaWriterCountDownLatch = null; private final Thread asyncStanzaWriter = new Thread() { @@ -25,13 +26,13 @@ public class TagWriter { public void run() { stanzaWriterCountDownLatch = new CountDownLatch(1); while (!isInterrupted()) { - if (finished && writeQueue.size() == 0) { + if (finished && writeQueue.isEmpty()) { break; } try { - AbstractStanza output = writeQueue.take(); + final var output = writeQueue.take(); outputStream.write(output.toString()); - if (writeQueue.size() == 0) { + if (writeQueue.isEmpty()) { outputStream.flush(); } } catch (Exception e) { @@ -74,7 +75,7 @@ public class TagWriter { } } - public synchronized void writeElement(Element element) throws IOException { + public synchronized void writeElement(final StreamElement element) throws IOException { if (outputStream == null) { throw new IOException("output stream was null"); } @@ -82,7 +83,7 @@ public class TagWriter { outputStream.flush(); } - public void writeStanzaAsync(AbstractStanza stanza) { + public void writeStanzaAsync(StreamElement stanza) { if (finished) { Log.d(Config.LOGTAG, "attempting to write stanza to finished TagWriter"); } else { diff --git a/src/main/java/eu/siacs/conversations/xml/XmlReader.java b/src/main/java/eu/siacs/conversations/xml/XmlReader.java index aee7d7819aeaecd7064f83c83ee6ac5aed5f5f25..0d5377cc9aa1d9cc9582811d0c4f6db2bef0b5f3 100644 --- a/src/main/java/eu/siacs/conversations/xml/XmlReader.java +++ b/src/main/java/eu/siacs/conversations/xml/XmlReader.java @@ -3,6 +3,12 @@ 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; @@ -11,8 +17,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import eu.siacs.conversations.Config; - public class XmlReader implements Closeable { private final XmlPullParser parser; private InputStream is; @@ -90,8 +94,21 @@ public class XmlReader implements Closeable { return null; } - public Element readElement(Tag currentTag) throws IOException { - Element element = new Element(currentTag.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) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/InvalidJid.java b/src/main/java/eu/siacs/conversations/xmpp/InvalidJid.java index f3a21c36f54bc055ccc73dd87e48a5ee66a47adf..4e3092821d4c241a004e5ce77f7e8e2940dea0ea 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/InvalidJid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/InvalidJid.java @@ -31,7 +31,7 @@ package eu.siacs.conversations.xmpp; import androidx.annotation.NonNull; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; +import im.conversations.android.xmpp.model.stanza.Stanza; public class InvalidJid implements Jid { @@ -137,10 +137,10 @@ public class InvalidJid implements Jid { } public static boolean isValid(Jid jid) { - return !(jid != null && jid instanceof InvalidJid); + return !(jid instanceof InvalidJid); } - public static boolean hasValidFrom(AbstractStanza stanza) { + public static boolean hasValidFrom(Stanza stanza) { final String from = stanza.getAttribute("from"); if (from == null) { return false; diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java deleted file mode 100644 index 6db24ef94845d95d363869b7de3225a24ee04013..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnIqPacketReceived.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; - -public interface OnIqPacketReceived extends PacketReceived { - void onIqPacketReceived(Account account, IqPacket packet); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java index 24acf16e27a59950c61357e1f5d9aa02686d9119..6ff26884bee375e74ddef4c5919f23ef05e4a35b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java +++ b/src/main/java/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java @@ -1,8 +1,7 @@ package eu.siacs.conversations.xmpp; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.stanza.Message; -public interface OnMessagePacketReceived extends PacketReceived { - void onMessagePacketReceived(Account account, MessagePacket packet); +public interface OnMessagePacketReceived { + void onMessagePacketReceived(Message packet); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java deleted file mode 100644 index e1bf839f43e26809091dc5bdf8c22626ce98e0f5..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.siacs.conversations.xmpp; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; - -public interface OnPresencePacketReceived extends PacketReceived { - void onPresencePacketReceived(Account account, PresencePacket packet); -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java deleted file mode 100644 index 05ddc392f822b1c55d0457551b823d5b590db129..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/PacketReceived.java +++ /dev/null @@ -1,5 +0,0 @@ -package eu.siacs.conversations.xmpp; - -public interface PacketReceived { - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index da2a387ab3d428823d3fd70ab3a6dd83e1eb58e9..77c78dfa2df16afa293d31bbfe732e6a7e0ae057 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -15,7 +15,6 @@ import android.util.SparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.core.util.Consumer; import com.google.common.base.MoreObjects; import com.google.common.base.Optional; @@ -70,6 +69,7 @@ import javax.net.ssl.X509KeyManager; import javax.net.ssl.X509TrustManager; import eu.siacs.conversations.AppSettings; +import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.XmppDomainVerifier; @@ -83,6 +83,9 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.parser.IqParser; +import eu.siacs.conversations.parser.MessageParser; +import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MessageArchiveService; @@ -105,18 +108,42 @@ import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; -import eu.siacs.conversations.xmpp.stanzas.AbstractAcknowledgeableStanza; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; -import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket; -import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket; -import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket; + +import im.conversations.android.xmpp.model.AuthenticationFailure; +import im.conversations.android.xmpp.model.AuthenticationRequest; +import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.bind2.Bind; +import im.conversations.android.xmpp.model.bind2.Bound; +import im.conversations.android.xmpp.model.csi.Active; +import im.conversations.android.xmpp.model.csi.Inactive; +import im.conversations.android.xmpp.model.error.Condition; +import im.conversations.android.xmpp.model.fast.Fast; +import im.conversations.android.xmpp.model.fast.RequestToken; +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.sasl.Auth; +import im.conversations.android.xmpp.model.sasl.Failure; +import im.conversations.android.xmpp.model.sasl.Mechanisms; +import im.conversations.android.xmpp.model.sasl.Response; +import im.conversations.android.xmpp.model.sasl.SaslError; +import im.conversations.android.xmpp.model.sasl.Success; +import im.conversations.android.xmpp.model.sasl2.Authenticate; +import im.conversations.android.xmpp.model.sasl2.Authentication; +import im.conversations.android.xmpp.model.sasl2.UserAgent; +import im.conversations.android.xmpp.model.sm.Ack; +import im.conversations.android.xmpp.model.sm.Enable; +import im.conversations.android.xmpp.model.sm.Enabled; +import im.conversations.android.xmpp.model.sm.Failed; +import im.conversations.android.xmpp.model.sm.Request; +import im.conversations.android.xmpp.model.sm.Resume; +import im.conversations.android.xmpp.model.sm.Resumed; +import im.conversations.android.xmpp.model.sm.StreamManagement; +import im.conversations.android.xmpp.model.stanza.Iq; +import im.conversations.android.xmpp.model.stanza.Presence; +import im.conversations.android.xmpp.model.stanza.Stanza; +import im.conversations.android.xmpp.model.tls.Proceed; +import im.conversations.android.xmpp.model.tls.StartTls; +import im.conversations.android.xmpp.processor.BindProcessor; import okhttp3.HttpUrl; @@ -151,6 +178,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.regex.Matcher; import javax.net.ssl.KeyManager; @@ -163,46 +191,12 @@ import javax.net.ssl.X509TrustManager; public class XmppConnection implements Runnable { - private static final int PACKET_IQ = 0; - private static final int PACKET_MESSAGE = 1; - private static final int PACKET_PRESENCE = 2; - public final OnIqPacketReceived registrationResponseListener = - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setOption(Account.OPTION_REGISTER, false); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": successfully registered new account on server"); - throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); - } else { - final List PASSWORD_TOO_WEAK_MSGS = - Arrays.asList( - "The password is too weak", "Please use a longer password."); - Element error = packet.findChild("error"); - Account.State state = Account.State.REGISTRATION_FAILED; - if (error != null) { - if (error.hasChild("conflict")) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (error.hasChild("resource-constraint") - && "wait".equals(error.getAttribute("type"))) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (error.hasChild("not-acceptable") - && PASSWORD_TOO_WEAK_MSGS.contains( - error.findChildContent("text"))) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; - } - } - throw new StateChangingError(state); - } - }; protected final Account account; private final Features features = new Features(this); private final HashMap disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); - private final SparseArray mStanzaQueue = new SparseArray<>(); - private final Hashtable>> packetCallbacks = - new Hashtable<>(); + private final SparseArray mStanzaQueue = new SparseArray<>(); + private final Hashtable, ScheduledFuture>>> packetCallbacks = new Hashtable<>(); private final Set advancedStreamFeaturesLoadedListeners = new HashSet<>(); private final AppSettings appSettings; @@ -215,8 +209,8 @@ public class XmppConnection implements Runnable { private boolean quickStartInProgress = false; private boolean isBound = false; private boolean offlineMessagesRetrieved = false; - private Element streamFeatures; - private Element boundStreamFeatures; + private im.conversations.android.xmpp.model.streams.Features streamFeatures; + private im.conversations.android.xmpp.model.streams.Features boundStreamFeatures; private StreamId streamId = null; private int stanzasReceived = 0; private int stanzasSent = 0; @@ -233,12 +227,13 @@ public class XmppConnection implements Runnable { private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); private boolean mInteractive = false; private int attempt = 0; - private OnPresencePacketReceived presenceListener = null; private OnJinglePacketReceived jingleListener = null; - private OnIqPacketReceived unregisteredIqListener = null; - private OnMessagePacketReceived messageListener = null; + + private final Consumer presenceListener; + private final Consumer unregisteredIqListener; + private final Consumer messageListener; private OnStatusChanged statusListener = null; - private OnBindListener bindListener = null; + private final Runnable bindListener; private OnMessageAcknowledged acknowledgedListener = null; private LoginInfo loginInfo; private HashedToken.Mechanism hashTokenRequest; @@ -254,7 +249,11 @@ public class XmppConnection implements Runnable { public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; this.mXmppConnectionService = service; - this.appSettings = new AppSettings(mXmppConnectionService.getApplicationContext()); + this.appSettings = mXmppConnectionService.getAppSettings(); + this.presenceListener = new PresenceParser(service, account); + this.unregisteredIqListener = new IqParser(service, account); + this.messageListener = new MessageParser(service, account); + this.bindListener = new BindProcessor(service, account); } private static void fixResource(final Context context, final Account account) { @@ -412,7 +411,7 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted"); return; } - if (results.size() == 0) { + if (results.isEmpty()) { Log.e( Config.LOGTAG, account.getJid().asBareJid() + ": Resolver results were empty"); @@ -664,7 +663,7 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("features", Namespace.STREAMS)) { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { - switchOverToTls(); + switchOverToTls(nextTag); } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); } else if (account.isOptionSet(Account.OPTION_REGISTER) @@ -677,8 +676,13 @@ public class XmppConnection implements Runnable { if (processSuccess(success)) { break; } - } else if (nextTag.isStart("failure")) { - final Element failure = tagReader.readElement(nextTag); + } else if (nextTag.isStart("failure", Namespace.SASL)) { + final var failure = tagReader.readElement(nextTag, Failure.class); + processFailure(failure); + } else if (nextTag.isStart("failure", Namespace.SASL_2)) { + final var failure = + tagReader.readElement( + nextTag, im.conversations.android.xmpp.model.sasl2.Failure.class); processFailure(failure); } else if (nextTag.isStart("continue", Namespace.SASL_2)) { // two step sasl2 - we don’t support this yet @@ -690,10 +694,10 @@ public class XmppConnection implements Runnable { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } else if (this.streamId != null && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) { - final Element resumed = tagReader.readElement(nextTag); + final Resumed resumed = tagReader.readElement(nextTag, Resumed.class); processResumed(resumed); } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) { - final Element failed = tagReader.readElement(nextTag); + final Failed failed = tagReader.readElement(nextTag, Failed.class); processFailed(failed, true); } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { processIq(nextTag); @@ -709,7 +713,7 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) { processPresence(nextTag); } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) { - final Element enabled = tagReader.readElement(nextTag); + final var enabled = tagReader.readElement(nextTag, Enabled.class); processEnabled(enabled); } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) { tagReader.readElement(nextTag); @@ -720,7 +724,7 @@ public class XmppConnection implements Runnable { + ": acknowledging stanza #" + this.stanzasReceived); } - final AckPacket ack = new AckPacket(this.stanzasReceived); + final Ack ack = new Ack(this.stanzasReceived); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a", Namespace.STREAM_MANAGEMENT)) { boolean accountUiNeedsRefresh = false; @@ -747,11 +751,11 @@ public class XmppConnection implements Runnable { if (accountUiNeedsRefresh) { mXmppConnectionService.updateAccountUi(); } - final Element ack = tagReader.readElement(nextTag); + final var ack = tagReader.readElement(nextTag, Ack.class); lastPacketReceived = SystemClock.elapsedRealtime(); final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { - final Optional serverSequence = ack.getOptionalIntAttribute("h"); + final Optional serverSequence = ack.getHandled(); if (serverSequence.isPresent()) { acknowledgedMessages = acknowledgeStanzaUpTo(serverSequence.get()); } else { @@ -787,11 +791,11 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final Element response; + final StreamElement response; if (version == SaslMechanism.Version.SASL) { - response = new Element("response", Namespace.SASL); + response = new Response(); } else if (version == SaslMechanism.Version.SASL_2) { - response = new Element("response", Namespace.SASL_2); + response = new im.conversations.android.xmpp.model.sasl2.Response(); } else { throw new AssertionError("Missing implementation for " + version); } @@ -811,26 +815,23 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(response); } - private boolean processSuccess(final Element success) + private boolean processSuccess(final Element element) throws IOException, XmlPullParserException { - final SaslMechanism.Version version; - try { - version = SaslMechanism.Version.of(success); - } catch (final IllegalArgumentException e) { - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } final LoginInfo currentLoginInfo = this.loginInfo; final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo); if (currentLoginInfo == null || currentSaslMechanism == null) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + final SaslMechanism.Version version; final String challenge; - if (version == SaslMechanism.Version.SASL) { + if (element instanceof Success success) { challenge = success.getContent(); - } else if (version == SaslMechanism.Version.SASL_2) { + version = SaslMechanism.Version.SASL; + } else if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) { challenge = success.findChildContent("additional-data"); + version = SaslMechanism.Version.SASL_2; } else { - throw new AssertionError("Missing implementation for " + version); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } try { currentLoginInfo.success(challenge, sslSocketOrNull(socket)); @@ -844,47 +845,24 @@ public class XmppConnection implements Runnable { if (SaslMechanism.pin(currentSaslMechanism)) { account.setPinnedMechanism(currentSaslMechanism); } - if (version == SaslMechanism.Version.SASL_2) { - final String authorizationIdentifier = - success.findChildContent("authorization-identifier"); - final Jid authorizationJid; - try { - authorizationJid = - Strings.isNullOrEmpty(authorizationIdentifier) - ? null - : Jid.ofEscaped(authorizationIdentifier); - } catch (final IllegalArgumentException e) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": SASL 2.0 authorization identifier was not a valid jid"); - throw new StateChangingException(Account.State.BIND_FAILURE); - } - if (authorizationJid == null) { - throw new StateChangingException(Account.State.BIND_FAILURE); - } + if (element instanceof im.conversations.android.xmpp.model.sasl2.Success success) { + final var authorizationJid = success.getAuthorizationIdentifier(); + checkAssignedDomainOrThrow(authorizationJid); Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": SASL 2.0 authorization identifier was " + authorizationJid); - if (!account.getJid().getDomain().equals(authorizationJid.getDomain())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server tried to re-assign domain to " - + authorizationJid.getDomain()); - throw new StateChangingError(Account.State.BIND_FAILURE); - } + // TODO this should only happen when we used Bind 2 if (authorizationJid.isFullJid() && account.setJid(authorizationJid)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": jid changed during SASL 2.0. updating database"); } - final Element bound = success.findChild("bound", Namespace.BIND2); - final Element resumed = success.findChild("resumed", Namespace.STREAM_MANAGEMENT); - final Element failed = success.findChild("failed", Namespace.STREAM_MANAGEMENT); + final Bound bound = success.getExtension(Bound.class); + final Resumed resumed = success.getExtension(Resumed.class); + final Failed failed = success.getExtension(Failed.class); final Element tokenWrapper = success.findChild("token", Namespace.FAST); final String token = tokenWrapper == null ? null : tokenWrapper.getAttribute("token"); if (bound != null && resumed != null) { @@ -911,8 +889,7 @@ public class XmppConnection implements Runnable { this.isBound = true; processNopStreamFeatures(); this.boundStreamFeatures = this.streamFeatures; - final Element streamManagementEnabled = - bound.findChild("enabled", Namespace.STREAM_MANAGEMENT); + final Enabled streamManagementEnabled = bound.getExtension(Enabled.class); final Element carbonsEnabled = bound.findChild("enabled", Namespace.CARBONS); final boolean waitForDisco; if (streamManagementEnabled != null) { @@ -931,7 +908,8 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + ": successfully enabled carbons (via Bind 2.0)"); features.carbonsEnabled = true; - } else if (loginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) { + } else if (currentLoginInfo.inlineBindFeatures != null + && currentLoginInfo.inlineBindFeatures.contains(Namespace.CARBONS)) { negotiatedCarbons = true; Log.d( Config.LOGTAG, @@ -997,7 +975,7 @@ public class XmppConnection implements Runnable { private void resetOutboundStanzaQueue() { synchronized (this.mStanzaQueue) { - final ImmutableList.Builder intermediateStanzasBuilder = + final ImmutableList.Builder intermediateStanzasBuilder = new ImmutableList.Builder<>(); if (Config.EXTENDED_SM_LOGGING) { Log.d( @@ -1007,7 +985,7 @@ public class XmppConnection implements Runnable { + this.stanzasSentBeforeAuthentication); } for (int i = this.stanzasSentBeforeAuthentication + 1; i <= this.stanzasSent; ++i) { - final AbstractAcknowledgeableStanza stanza = this.mStanzaQueue.get(i); + final Stanza stanza = this.mStanzaQueue.get(i); if (stanza != null) { intermediateStanzasBuilder.add(stanza); } @@ -1031,7 +1009,9 @@ public class XmppConnection implements Runnable { private void processNopStreamFeatures() throws IOException { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("features", Namespace.STREAMS)) { - this.streamFeatures = tagReader.readElement(tag); + this.streamFeatures = + tagReader.readElement( + tag, im.conversations.android.xmpp.model.streams.Features.class); Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1047,7 +1027,7 @@ public class XmppConnection implements Runnable { } } - private void processFailure(final Element failure) throws IOException { + private void processFailure(final AuthenticationFailure failure) throws IOException { final SaslMechanism.Version version; try { version = SaslMechanism.Version.of(failure); @@ -1061,10 +1041,21 @@ public class XmppConnection implements Runnable { account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); } - if (failure.hasChild("temporary-auth-failure")) { + final var errorCondition = failure.getErrorCondition(); + if (errorCondition instanceof SaslError.InvalidMechanism + || errorCondition instanceof SaslError.MechanismTooWeak) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": invalid or too weak mechanism. resetting quick start"); + if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, false)) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } else if (errorCondition instanceof SaslError.TemporaryAuthFailure) { throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); - } else if (failure.hasChild("account-disabled")) { - final String text = failure.findChildContent("text"); + } else if (errorCondition instanceof SaslError.AccountDisabled) { + final String text = failure.getText(); if (Strings.isNullOrEmpty(text)) { throw new StateChangingException(Account.State.UNAUTHORIZED); } @@ -1101,22 +1092,8 @@ public class XmppConnection implements Runnable { } } - private void processEnabled(final Element enabled) { - final String id; - if (enabled.getAttributeAsBoolean("resume")) { - id = enabled.getAttribute("id"); - } else { - id = null; - } - final String locationAttribute = enabled.getAttribute("location"); - final Resolver.Result currentResolverResult = this.currentResolverResult; - final Resolver.Result location; - if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) { - location = null; - } else { - location = currentResolverResult.seeOtherHost(locationAttribute); - } - final StreamId streamId = id == null ? null : new StreamId(id, location); + private void processEnabled(final Enabled enabled) { + final StreamId streamId = getStreamId(enabled); if (streamId == null) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream management enabled"); } else { @@ -1129,16 +1106,30 @@ public class XmppConnection implements Runnable { this.streamId = streamId; this.stanzasReceived = 0; this.inSmacksSession = true; - final RequestPacket r = new RequestPacket(); + final var r = new Request(); tagWriter.writeStanzaAsync(r); } - private void processResumed(final Element resumed) throws StateChangingException { + @Nullable + private StreamId getStreamId(final Enabled enabled) { + final Optional id = enabled.getResumeId(); + final String locationAttribute = enabled.getLocation(); + final Resolver.Result currentResolverResult = this.currentResolverResult; + final Resolver.Result location; + if (Strings.isNullOrEmpty(locationAttribute) || currentResolverResult == null) { + location = null; + } else { + location = currentResolverResult.seeOtherHost(locationAttribute); + } + return id.isPresent() ? new StreamId(id.get(), location) : null; + } + + private void processResumed(final Resumed resumed) throws StateChangingException { this.inSmacksSession = true; this.isBound = true; - this.tagWriter.writeStanzaAsync(new RequestPacket()); + this.tagWriter.writeStanzaAsync(new Request()); lastPacketReceived = SystemClock.elapsedRealtime(); - final Optional h = resumed.getOptionalIntAttribute("h"); + final Optional h = resumed.getHandled(); final int serverCount; if (h.isPresent()) { serverCount = h.get(); @@ -1146,7 +1137,7 @@ public class XmppConnection implements Runnable { resetStreamId(); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - final ArrayList failedStanzas = new ArrayList<>(); + final ArrayList failedStanzas = new ArrayList<>(); final boolean acknowledgedMessages; synchronized (this.mStanzaQueue) { if (serverCount < stanzasSent) { @@ -1169,8 +1160,8 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas"); - for (final AbstractAcknowledgeableStanza packet : failedStanzas) { - if (packet instanceof MessagePacket message) { + for (final Stanza packet : failedStanzas) { + if (packet instanceof im.conversations.android.xmpp.model.stanza.Message message) { mXmppConnectionService.markMessage( account, message.getTo().asBareJid(), @@ -1189,8 +1180,8 @@ public class XmppConnection implements Runnable { changeStatus(Account.State.ONLINE); } - private void processFailed(final Element failed, final boolean sendBindRequest) { - final Optional serverCount = failed.getOptionalIntAttribute("h"); + private void processFailed(final Failed failed, final boolean sendBindRequest) { + final Optional serverCount = failed.getHandled(); if (serverCount.isPresent()) { Log.d( Config.LOGTAG, @@ -1237,8 +1228,9 @@ public class XmppConnection implements Runnable { + ": server acknowledged stanza #" + mStanzaQueue.keyAt(i)); } - final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); - if (stanza instanceof MessagePacket packet && acknowledgedListener != null) { + final Stanza stanza = mStanzaQueue.valueAt(i); + if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet + && acknowledgedListener != null) { final String id = packet.getId(); final Jid to = packet.getTo(); if (id != null && to != null) { @@ -1253,29 +1245,9 @@ public class XmppConnection implements Runnable { return acknowledgedMessages; } - private @NonNull Element processPacket(final Tag currentTag, final int packetType) + private @NonNull S processPacket(final Tag currentTag, final Class clazz) throws IOException { - final Element element = - switch (packetType) { - case PACKET_IQ -> new IqPacket(); - case PACKET_MESSAGE -> new MessagePacket(); - case PACKET_PRESENCE -> new PresencePacket(); - default -> throw new AssertionError("Should never encounter invalid type"); - }; - element.setAttributes(currentTag.getAttributes()); - Tag nextTag = tagReader.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - while (!nextTag.isEnd(element.getName())) { - if (!nextTag.isNo()) { - element.addChild(tagReader.readElement(nextTag)); - } - nextTag = tagReader.readTag(); - if (nextTag == null) { - throw new IOException("interrupted mid tag"); - } - } + final S stanza = tagReader.readElement(currentTag, clazz); if (stanzasReceived == Integer.MAX_VALUE) { resetStreamId(); throw new IOException("time to restart the session. cant handle >2 billion pcks"); @@ -1287,25 +1259,19 @@ public class XmppConnection implements Runnable { Config.LOGTAG, account.getJid().asBareJid() + ": not counting stanza(" - + element.getClass().getSimpleName() + + stanza.getClass().getSimpleName() + "). Not in smacks session."); } lastPacketReceived = SystemClock.elapsedRealtime(); if (Config.BACKGROUND_STANZA_LOGGING && mXmppConnectionService.checkListeners()) { - Log.d(Config.LOGTAG, "[background stanza] " + element); - } - if (element instanceof IqPacket - && (((IqPacket) element).getType() == IqPacket.TYPE.SET) - && element.hasChild("jingle", Namespace.JINGLE)) { - return JinglePacket.upgrade((IqPacket) element); - } else { - return element; + Log.d(Config.LOGTAG, "[background stanza] " + stanza); } + return stanza; } private void processIq(final Tag currentTag) throws IOException { - final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); - if (!packet.valid()) { + final Iq packet = processPacket(currentTag, Iq.class); + if (packet.isInvalid()) { Log.e( Config.LOGTAG, "encountered invalid iq from='" @@ -1321,9 +1287,9 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + "Not processing iq. Thread was interrupted"); return; } - if (packet instanceof JinglePacket jinglePacket && isBound) { + if (packet.hasExtension(Jingle.class) && packet.getType() == Iq.Type.SET && isBound) { if (this.jingleListener != null) { - this.jingleListener.onJinglePacketReceived(account, jinglePacket); + this.jingleListener.onJinglePacketReceived(account, packet); } } else { final var callback = getIqPacketReceivedCallback(packet); @@ -1338,7 +1304,7 @@ public class XmppConnection implements Runnable { final ScheduledFuture timeoutFuture = callback.second; try { if (timeoutFuture == null || timeoutFuture.cancel(false)) { - callback.first.onIqPacketReceived(account, packet); + callback.first.accept(packet); } } catch (final StateChangingError error) { throw new StateChangingException(error.state); @@ -1346,10 +1312,10 @@ public class XmppConnection implements Runnable { } } - private Pair getIqPacketReceivedCallback(final IqPacket stanza) + private Pair, ScheduledFuture> getIqPacketReceivedCallback(final Iq stanza) throws StateChangingException { final boolean isRequest = - stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET; + stanza.getType() == Iq.Type.GET || stanza.getType() == Iq.Type.SET; if (isRequest) { if (isBound) { return new Pair<>(this.unregisteredIqListener, null); @@ -1389,8 +1355,9 @@ public class XmppConnection implements Runnable { } private void processMessage(final Tag currentTag) throws IOException { - final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); - if (!packet.valid()) { + final var packet = + processPacket(currentTag, im.conversations.android.xmpp.model.stanza.Message.class); + if (packet.isInvalid()) { Log.e( Config.LOGTAG, "encountered invalid message from='" @@ -1407,12 +1374,12 @@ public class XmppConnection implements Runnable { + "Not processing message. Thread was interrupted"); return; } - this.messageListener.onMessagePacketReceived(account, packet); + this.messageListener.accept(packet); } private void processPresence(final Tag currentTag) throws IOException { - final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); - if (!packet.valid()) { + final var packet = processPacket(currentTag, Presence.class); + if (packet.isInvalid()) { Log.e( Config.LOGTAG, "encountered invalid presence from='" @@ -1429,17 +1396,15 @@ public class XmppConnection implements Runnable { + "Not processing presence. Thread was interrupted"); return; } - this.presenceListener.onPresencePacketReceived(account, packet); + this.presenceListener.accept(packet); } private void sendStartTLS() throws IOException { - final Tag startTLS = Tag.empty("starttls"); - startTLS.setAttribute("xmlns", Namespace.TLS); - tagWriter.writeTag(startTLS); + tagWriter.writeElement(new StartTls()); } - private void switchOverToTls() throws XmlPullParserException, IOException { - tagReader.readTag(); + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException { + tagReader.readElement(currentTag, Proceed.class); final Socket socket = this.socket; final SSLSocket sslSocket = upgradeSocketToTls(socket); this.socket = sslSocket; @@ -1511,11 +1476,13 @@ public class XmppConnection implements Runnable { } private void processStreamFeatures(final Tag currentTag) throws IOException { - this.streamFeatures = tagReader.readElement(currentTag); + this.streamFeatures = + tagReader.readElement( + currentTag, im.conversations.android.xmpp.model.streams.Features.class); final boolean isSecure = isSecure(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { - if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + if (this.streamFeatures.hasStreamFeature(Authentication.class)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1524,8 +1491,7 @@ public class XmppConnection implements Runnable { if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { return; } - if (isFastTokenAvailable( - this.streamFeatures.findChild("authentication", Namespace.SASL_2))) { + if (isFastTokenAvailable(this.streamFeatures.getExtension(Authentication.class))) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1543,8 +1509,7 @@ public class XmppConnection implements Runnable { mXmppConnectionService.databaseBackend.updateAccount(account); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - if (this.streamFeatures.hasChild("starttls", Namespace.TLS) - && !features.encryptionEnabled) { + if (this.streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) { sendStartTLS(); } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { @@ -1561,15 +1526,15 @@ public class XmppConnection implements Runnable { } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasChild("authentication", Namespace.SASL_2) + } else if (this.streamFeatures.hasStreamFeature(Authentication.class) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL_2); - } else if (this.streamFeatures.hasChild("mechanisms", Namespace.SASL) + } else if (this.streamFeatures.hasStreamFeature(Mechanisms.class) && shouldAuthenticate && isSecure) { authenticate(SaslMechanism.Version.SASL); - } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT) + } else if (this.streamFeatures.streamManagement() && isSecure && LoginInfo.isSuccess(loginInfo) && streamId != null @@ -1581,7 +1546,7 @@ public class XmppConnection implements Runnable { + ": resuming after stanza #" + stanzasReceived); } - final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived); + final var resume = new Resume(this.streamId.id, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); this.tagWriter.writeStanzaAsync(resume); @@ -1609,9 +1574,9 @@ public class XmppConnection implements Runnable { private void authenticate() throws IOException { final boolean isSecure = isSecure(); - if (isSecure && this.streamFeatures.hasChild("authentication", Namespace.SASL_2)) { + if (isSecure && this.streamFeatures.hasStreamFeature(Authentication.class)) { authenticate(SaslMechanism.Version.SASL_2); - } else if (isSecure && this.streamFeatures.hasChild("mechanisms", Namespace.SASL)) { + } else if (isSecure && this.streamFeatures.hasStreamFeature(Mechanisms.class)) { authenticate(SaslMechanism.Version.SASL); } else { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); @@ -1623,13 +1588,13 @@ public class XmppConnection implements Runnable { } private void authenticate(final SaslMechanism.Version version) throws IOException { - final Element authElement; + final AuthenticationStreamFeature authElement; if (version == SaslMechanism.Version.SASL) { - authElement = this.streamFeatures.findChild("mechanisms", Namespace.SASL); + authElement = this.streamFeatures.getExtension(Mechanisms.class); } else { - authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); + authElement = this.streamFeatures.getExtension(Authentication.class); } - final Collection mechanisms = SaslMechanism.mechanisms(authElement); + final Collection mechanisms = authElement.getMechanismNames(); final Element cbElement = this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); final Collection channelBindings = ChannelBinding.of(cbElement); @@ -1641,26 +1606,28 @@ public class XmppConnection implements Runnable { final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); final boolean usingFast = SaslMechanism.hashedToken(saslMechanism); - final Element authenticate; + final AuthenticationRequest authenticate; + final LoginInfo loginInfo; if (version == SaslMechanism.Version.SASL) { - authenticate = new Element("auth", Namespace.SASL); + authenticate = new Auth(); if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.setContent(firstMessage); } quickStartAvailable = false; - this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList()); + loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList()); } else if (version == SaslMechanism.Version.SASL_2) { - final Element inline = authElement.findChild("inline", Namespace.SASL_2); - final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT); + final Authentication authentication = (Authentication) authElement; + final var inline = authentication.getInline(); + final boolean sm = inline != null && inline.hasExtension(StreamManagement.class); final HashedToken.Mechanism hashTokenRequest; if (usingFast) { hashTokenRequest = null; - } else { - final Element fast = - inline == null ? null : inline.findChild("fast", Namespace.FAST); - final Collection fastMechanisms = SaslMechanism.mechanisms(fast); + } else if (inline != null) { hashTokenRequest = - HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)); + HashedToken.Mechanism.best( + inline.getFastMechanisms(), SSLSockets.version(this.socket)); + } else { + hashTokenRequest = null; } final Collection bindFeatures = Bind2.features(inline); quickStartAvailable = @@ -1678,7 +1645,7 @@ public class XmppConnection implements Runnable { return; } } - this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures); + loginInfo = new LoginInfo(saslMechanism, version, bindFeatures); this.hashTokenRequest = hashTokenRequest; authenticate = generateAuthenticationRequest( @@ -1686,7 +1653,7 @@ public class XmppConnection implements Runnable { } else { throw new AssertionError("Missing implementation for " + version); } - + this.loginInfo = loginInfo; if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) { mXmppConnectionService.databaseBackend.updateAccount(account); } @@ -1697,17 +1664,17 @@ public class XmppConnection implements Runnable { + ": Authenticating with " + version + "/" - + LoginInfo.mechanism(this.loginInfo).getMechanism()); - authenticate.setAttribute("mechanism", LoginInfo.mechanism(this.loginInfo).getMechanism()); + + LoginInfo.mechanism(loginInfo).getMechanism()); + authenticate.setMechanism(LoginInfo.mechanism(loginInfo)); synchronized (this.mStanzaQueue) { this.stanzasSentBeforeAuthentication = this.stanzasSent; tagWriter.writeElement(authenticate); } } - private static boolean isFastTokenAvailable(final Element authentication) { - final Element inline = authentication == null ? null : authentication.findChild("inline"); - return inline != null && inline.hasChild("fast", Namespace.FAST); + private static boolean isFastTokenAvailable(final Authentication authentication) { + final var inline = authentication == null ? null : authentication.getInline(); + return inline != null && inline.hasExtension(Fast.class); } private void validate( @@ -1721,7 +1688,7 @@ public class XmppConnection implements Runnable { + mechanisms); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - validateRequireChannelBinding(saslMechanism); + checkRequireChannelBinding(saslMechanism); if (SaslMechanism.hashedToken(saslMechanism)) { return; } @@ -1740,7 +1707,7 @@ public class XmppConnection implements Runnable { } } - private void validateRequireChannelBinding(@NonNull final SaslMechanism mechanism) + private void checkRequireChannelBinding(@NonNull final SaslMechanism mechanism) throws StateChangingException { if (appSettings.isRequireChannelBinding()) { if (mechanism instanceof ChannelBindingMechanism) { @@ -1751,31 +1718,56 @@ public class XmppConnection implements Runnable { } } - private Element generateAuthenticationRequest( + private void checkAssignedDomainOrThrow(final Jid jid) throws StateChangingException { + if (jid == null) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": bind response is missing jid"); + throw new StateChangingException(Account.State.BIND_FAILURE); + } + final var current = this.account.getJid().getDomain(); + if (jid.getDomain().equals(current)) { + return; + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried to re-assign domain to " + + jid.getDomain()); + throw new StateChangingException(Account.State.BIND_FAILURE); + } + + private void checkAssignedDomain(final Jid jid) { + try { + checkAssignedDomainOrThrow(jid); + } catch (final StateChangingException e) { + throw new StateChangingError(e.state); + } + } + + private AuthenticationRequest generateAuthenticationRequest( final String firstMessage, final boolean usingFast) { return generateAuthenticationRequest( firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true); } - private Element generateAuthenticationRequest( + private AuthenticationRequest generateAuthenticationRequest( final String firstMessage, final boolean usingFast, final HashedToken.Mechanism hashedTokenRequest, final Collection bind, final boolean inlineStreamManagement) { - final Element authenticate = new Element("authenticate", Namespace.SASL_2); + final var authenticate = new Authenticate(); if (!Strings.isNullOrEmpty(firstMessage)) { authenticate.addChild("initial-response").setContent(firstMessage); } - final Element userAgent = authenticate.addChild("user-agent"); - userAgent.setAttribute("id", AccountUtils.publicDeviceId(account)); - userAgent - .addChild("software") - .setContent(mXmppConnectionService.getString(R.string.app_name)); + final var userAgent = + authenticate.addExtension( + new UserAgent( + AccountUtils.publicDeviceId( + account, appSettings.getInstallationId()))); + userAgent.setSoftware( + String.format("%s %s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME)); if (!PhoneHelper.isEmulator()) { - userAgent - .addChild("device") - .setContent(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); + userAgent.setDevice(String.format("%s %s", Build.MANUFACTURER, Build.MODEL)); } // do not include bind if 'inlineStreamManagement' is missing and we have a streamId // (because we would rather just do a normal SM/resume) @@ -1784,31 +1776,29 @@ public class XmppConnection implements Runnable { authenticate.addChild(generateBindRequest(bind)); } if (inlineStreamManagement && streamId != null) { - final ResumePacket resume = new ResumePacket(this.streamId.id, stanzasReceived); + final var resume = new Resume(this.streamId.id, stanzasReceived); this.mSmCatchupMessageCounter.set(0); this.mWaitingForSmCatchup.set(true); - authenticate.addChild(resume); + authenticate.addExtension(resume); } if (hashedTokenRequest != null) { - authenticate - .addChild("request-token", Namespace.FAST) - .setAttribute("mechanism", hashedTokenRequest.name()); + authenticate.addExtension(new RequestToken(hashedTokenRequest)); } if (usingFast) { - authenticate.addChild("fast", Namespace.FAST); + authenticate.addExtension(new Fast()); } return authenticate; } - private Element generateBindRequest(final Collection bindFeatures) { + private Bind generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); - final Element bind = new Element("bind", Namespace.BIND2); - bind.addChild("tag").setContent(mXmppConnectionService.getString(R.string.app_name)); + final var bind = new Bind(); + bind.setTag(BuildConfig.APP_NAME); if (bindFeatures.contains(Namespace.CARBONS)) { - bind.addChild("enable", Namespace.CARBONS); + bind.addExtension(new im.conversations.android.xmpp.model.carbons.Enable()); } if (bindFeatures.contains(Namespace.STREAM_MANAGEMENT)) { - bind.addChild(new EnablePacket()); + bind.addExtension(new Enable()); } return bind; } @@ -1816,12 +1806,12 @@ public class XmppConnection implements Runnable { private void register() { final String preAuth = account.getKey(Account.KEY_PRE_AUTH_REGISTRATION_TOKEN); if (preAuth != null && features.invite()) { - final IqPacket preAuthRequest = new IqPacket(IqPacket.TYPE.SET); + final Iq preAuthRequest = new Iq(Iq.Type.SET); preAuthRequest.addChild("preauth", Namespace.PARS).setAttribute("token", preAuth); sendUnmodifiedIqPacket( preAuthRequest, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { sendRegistryRequest(); } else { final String error = response.getErrorCondition(); @@ -1840,21 +1830,21 @@ public class XmppConnection implements Runnable { } private void sendRegistryRequest() { - final IqPacket register = new IqPacket(IqPacket.TYPE.GET); + final Iq register = new Iq(Iq.Type.GET); register.query(Namespace.REGISTER); register.setTo(account.getDomain()); sendUnmodifiedIqPacket( register, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + (packet) -> { + if (packet.getType() == Iq.Type.TIMEOUT) { return; } - if (packet.getType() == IqPacket.TYPE.ERROR) { + if (packet.getType() == Iq.Type.ERROR) { throw new StateChangingError(Account.State.REGISTRATION_FAILED); } final Element query = packet.query(Namespace.REGISTER); if (query.hasChild("username") && (query.hasChild("password"))) { - final IqPacket register1 = new IqPacket(IqPacket.TYPE.SET); + final Iq register1 = new Iq(Iq.Type.SET); final Element username = new Element("username").setContent(account.getUsername()); final Element password = @@ -1862,7 +1852,7 @@ public class XmppConnection implements Runnable { register1.query(Namespace.REGISTER).addChild(username); register1.query().addChild(password); register1.setFrom(account.getJid().asBareJid()); - sendUnmodifiedIqPacket(register1, registrationResponseListener, true); + sendUnmodifiedIqPacket(register1, this::processRegistrationResponse, true); } else if (query.hasChild("x", Namespace.DATA)) { final Data data = Data.parse(query.findChild("x", Namespace.DATA)); final Element blob = query.findChild("data", "urn:xmpp:bob"); @@ -1930,6 +1920,45 @@ public class XmppConnection implements Runnable { true); } + public void sendCreateAccountWithCaptchaPacket(final String id, final Data data) { + final Iq request = IqGenerator.generateCreateAccountWithCaptcha(account, id, data); + this.sendUnmodifiedIqPacket(request, this::processRegistrationResponse, true); + } + + private void processRegistrationResponse(final Iq response) { + if (response.getType() == Iq.Type.RESULT) { + account.setOption(Account.OPTION_REGISTER, false); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully registered new account on server"); + throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); + } else { + final Account.State state = getRegistrationFailedState(response); + throw new StateChangingError(state); + } + } + + @NonNull + private static Account.State getRegistrationFailedState(final Iq response) { + final List PASSWORD_TOO_WEAK_MESSAGES = + Arrays.asList("The password is too weak", "Please use a longer password."); + final var error = response.getError(); + final var condition = error == null ? null : error.getCondition(); + final Account.State state; + if (condition instanceof Condition.Conflict) { + state = Account.State.REGISTRATION_CONFLICT; + } else if (condition instanceof Condition.ResourceConstraint) { + state = Account.State.REGISTRATION_PLEASE_WAIT; + } else if (condition instanceof Condition.NotAcceptable + && PASSWORD_TOO_WEAK_MESSAGES.contains(error.getTextAsString())) { + state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; + } else { + state = Account.State.REGISTRATION_FAILED; + } + return state; + } + private void setAccountCreationFailed(final String url) { final HttpUrl httpUrl = url == null ? null : HttpUrl.parse(url); if (httpUrl != null && httpUrl.isHttps()) { @@ -1973,65 +2002,42 @@ public class XmppConnection implements Runnable { } clearIqCallbacks(); if (account.getJid().isBareJid()) { - account.setResource(this.createNewResource()); + account.setResource(createNewResource()); } else { fixResource(mXmppConnectionService, account); } - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); final String resource = - Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND ? nextRandomId() : account.getResource(); - iq.addChild("bind", Namespace.BIND).addChild("resource").setContent(resource); + Config.USE_RANDOM_RESOURCE_ON_EVERY_BIND + ? CryptoHelper.random(9) + : account.getResource(); + iq.addExtension(new im.conversations.android.xmpp.model.bind.Bind()).setResource(resource); this.sendUnmodifiedIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.TIMEOUT) { + (packet) -> { + if (packet.getType() == Iq.Type.TIMEOUT) { return; } - final Element bind = packet.findChild("bind"); - if (bind != null && packet.getType() == IqPacket.TYPE.RESULT) { + final var bind = + packet.getExtension( + im.conversations.android.xmpp.model.bind.Bind.class); + if (bind != null && packet.getType() == Iq.Type.RESULT) { isBound = true; - final Element jid = bind.findChild("jid"); - if (jid != null && jid.getContent() != null) { - try { - Jid assignedJid = Jid.ofEscaped(jid.getContent()); - if (!account.getJid().getDomain().equals(assignedJid.getDomain())) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server tried to re-assign domain to " - + assignedJid.getDomain()); - throw new StateChangingError(Account.State.BIND_FAILURE); - } - if (account.setJid(assignedJid)) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": jid changed during bind. updating database"); - mXmppConnectionService.databaseBackend.updateAccount(account); - } - if (streamFeatures.hasChild("session") - && !streamFeatures - .findChild("session") - .hasChild("optional")) { - sendStartSession(); - } else { - final boolean waitForDisco = enableStreamManagement(); - sendPostBindInitialization(waitForDisco, false); - } - return; - } catch (final IllegalArgumentException e) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": server reported invalid jid (" - + jid.getContent() - + ") on bind"); - } - } else { + final Jid assignedJid = bind.getJid(); + checkAssignedDomain(assignedJid); + if (account.setJid(assignedJid)) { Log.d( Config.LOGTAG, - account.getJid() - + ": disconnecting because of bind failure. (no jid)"); + account.getJid().asBareJid() + + ": jid changed during bind. updating database"); + mXmppConnectionService.databaseBackend.updateAccount(account); + } + if (streamFeatures.hasChild("session") + && !streamFeatures.findChild("session").hasChild("optional")) { + sendStartSession(); + } else { + final boolean waitForDisco = enableStreamManagement(); + sendPostBindInitialization(waitForDisco, false); } } else { Log.d( @@ -2039,23 +2045,24 @@ public class XmppConnection implements Runnable { account.getJid() + ": disconnecting because of bind failure (" + packet); + final var error = packet.getError(); + // TODO error.is(Condition) + if (packet.getType() == Iq.Type.ERROR + && error != null + && error.hasChild("conflict")) { + account.setResource(createNewResource()); + } + throw new StateChangingError(Account.State.BIND_FAILURE); } - final Element error = packet.findChild("error"); - if (packet.getType() == IqPacket.TYPE.ERROR - && error != null - && error.hasChild("conflict")) { - account.setResource(createNewResource()); - } - throw new StateChangingError(Account.State.BIND_FAILURE); }, true); } private void clearIqCallbacks() { - final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT); - final ArrayList callbacks = new ArrayList<>(); + final Iq failurePacket = new Iq(Iq.Type.TIMEOUT); + final ArrayList> callbacks = new ArrayList<>(); synchronized (this.packetCallbacks) { - if (this.packetCallbacks.size() == 0) { + if (this.packetCallbacks.isEmpty()) { return; } Log.d( @@ -2064,19 +2071,18 @@ public class XmppConnection implements Runnable { + ": clearing " + this.packetCallbacks.size() + " iq callbacks"); - final Iterator>> iterator = - this.packetCallbacks.values().iterator(); + final var iterator = this.packetCallbacks.values().iterator(); while (iterator.hasNext()) { - Pair> entry = iterator.next(); + final var entry = iterator.next(); if (entry.second.second == null || entry.second.second.cancel(false)) { callbacks.add(entry.second.first); } iterator.remove(); } } - for (OnIqPacketReceived callback : callbacks) { + for (final var callback : callbacks) { try { - callback.onIqPacketReceived(account, failurePacket); + callback.accept(failurePacket); } catch (StateChangingError error) { Log.d( Config.LOGTAG, @@ -2108,15 +2114,15 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": sending legacy session to outdated server"); - final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); + final Iq startSession = new Iq(Iq.Type.SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); this.sendUnmodifiedIqPacket( startSession, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final boolean waitForDisco = enableStreamManagement(); sendPostBindInitialization(waitForDisco, false); - } else if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + } else if (packet.getType() != Iq.Type.TIMEOUT) { throw new StateChangingError(Account.State.SESSION_FAILURE); } }, @@ -2124,11 +2130,10 @@ public class XmppConnection implements Runnable { } private boolean enableStreamManagement() { - final boolean streamManagement = - this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT); + final boolean streamManagement = this.streamFeatures.streamManagement(); if (streamManagement) { synchronized (this.mStanzaQueue) { - final EnablePacket enable = new EnablePacket(); + final var enable = new Enable(); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; mStanzaQueue.clear(); @@ -2185,13 +2190,13 @@ public class XmppConnection implements Runnable { private void sendServiceDiscoveryInfo(final Jid jid) { mPendingServiceDiscoveries.incrementAndGet(); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + final Iq iq = new Iq(Iq.Type.GET); iq.setTo(jid); iq.query("http://jabber.org/protocol/disco#info"); this.sendIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { boolean advancedStreamFeaturesLoaded; synchronized (XmppConnection.this.disco) { ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); @@ -2209,7 +2214,7 @@ public class XmppConnection implements Runnable { || jid.equals(account.getJid().asBareJid()))) { enableAdvancedStreamFeatures(); } - } else if (packet.getType() == IqPacket.TYPE.ERROR) { + } else if (packet.getType() == Iq.Type.ERROR) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -2233,7 +2238,7 @@ public class XmppConnection implements Runnable { enableAdvancedStreamFeatures(); } } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (packet.getType() != Iq.Type.TIMEOUT) { if (mPendingServiceDiscoveries.decrementAndGet() == 0 && mWaitForDisco.compareAndSet(true, false)) { finalizeBind(); @@ -2243,12 +2248,12 @@ public class XmppConnection implements Runnable { } private void discoverMamPreferences() { - IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.addChild("prefs", MessageArchiveService.Version.MAM_2.namespace); sendIqPacket( request, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Element prefs = response.findChild( "prefs", MessageArchiveService.Version.MAM_2.namespace); @@ -2263,13 +2268,13 @@ public class XmppConnection implements Runnable { } private void discoverCommands() { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(account.getDomain()); request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); sendIqPacket( request, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element query = response.findChild("query", Namespace.DISCO_ITEMS); if (query == null) { return; @@ -2298,17 +2303,14 @@ public class XmppConnection implements Runnable { private void finalizeBind() { this.offlineMessagesRetrieved = false; - if (bindListener != null) { - bindListener.onBind(account); - } - changeStatusToOnline(); + this.bindListener.run(); + this.changeStatusToOnline(); } private void enableAdvancedStreamFeatures() { if (getFeatures().blocking() && !features.blockListRequested) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Requesting block list"); - this.sendIqPacket( - getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); + this.sendIqPacket(getIqGenerator().generateGetBlockList(), unregisteredIqListener); } for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { @@ -2324,13 +2326,13 @@ public class XmppConnection implements Runnable { private void sendServiceDiscoveryItems(final Jid server) { mPendingServiceDiscoveries.incrementAndGet(); - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + final Iq iq = new Iq(Iq.Type.GET); iq.setTo(server.getDomain()); iq.query("http://jabber.org/protocol/disco#items"); this.sendIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { final HashSet items = new HashSet<>(); final List elements = packet.query().getChildren(); for (final Element element : elements) { @@ -2353,7 +2355,7 @@ public class XmppConnection implements Runnable { + ": could not query disco items of " + server); } - if (packet.getType() != IqPacket.TYPE.TIMEOUT) { + if (packet.getType() != Iq.Type.TIMEOUT) { if (mPendingServiceDiscoveries.decrementAndGet() == 0 && mWaitForDisco.compareAndSet(true, false)) { finalizeBind(); @@ -2363,12 +2365,12 @@ public class XmppConnection implements Runnable { } private void sendEnableCarbons() { - final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); + final Iq iq = new Iq(Iq.Type.SET); iq.addChild("enable", Namespace.CARBONS); this.sendIqPacket( iq, - (account, packet) -> { - if (packet.getType() == IqPacket.TYPE.RESULT) { + (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": successfully enabled carbons"); @@ -2389,6 +2391,10 @@ public class XmppConnection implements Runnable { return; } if (streamError.hasChild("conflict")) { + final var loginInfo = this.loginInfo; + if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) { + this.appSettings.resetInstallationId(); + } account.setResource(createNewResource()); Log.d( Config.LOGTAG, @@ -2396,7 +2402,7 @@ public class XmppConnection implements Runnable { + ": switching resource due to conflict (" + account.getResource() + ")"); - throw new IOException(); + throw new IOException("Closed stream due to resource conflict"); } else if (streamError.hasChild("host-unknown")) { throw new StateChangingException(Account.State.HOST_UNKNOWN); } else if (streamError.hasChild("policy-violation")) { @@ -2437,8 +2443,8 @@ public class XmppConnection implements Runnable { private void failPendingMessages(final String error) { synchronized (this.mStanzaQueue) { for (int i = 0; i < mStanzaQueue.size(); ++i) { - final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i); - if (stanza instanceof MessagePacket packet) { + final Stanza stanza = mStanzaQueue.valueAt(i); + if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message packet) { final String id = packet.getId(); final Jid to = packet.getTo(); mXmppConnectionService.markMessage( @@ -2472,11 +2478,11 @@ public class XmppConnection implements Runnable { SaslMechanism.Version.SASL_2, Bind2.QUICKSTART_FEATURES); final boolean usingFast = quickStartMechanism instanceof HashedToken; - final Element authenticate = + final AuthenticationRequest authenticate = generateAuthenticationRequest( quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast); - authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism()); + authenticate.setMechanism(quickStartMechanism); sendStartStream(true, false); synchronized (this.mStanzaQueue) { this.stanzasSentBeforeAuthentication = this.stanzasSent; @@ -2507,35 +2513,28 @@ public class XmppConnection implements Runnable { tagWriter.writeTag(stream, flush); } - private String createNewResource() { - return mXmppConnectionService.getString(R.string.app_name) + '.' + nextRandomId(true); + private static String createNewResource() { + return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3)); } - private String nextRandomId() { - return nextRandomId(false); - } - - private String nextRandomId(final boolean s) { - return CryptoHelper.random(s ? 3 : 9); - } - - public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { + public String sendIqPacket(final Iq packet, final Consumer callback) { return sendIqPacket(packet, callback, null); } - public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback, Long timeout) { + public String sendIqPacket(final Iq packet, final Consumer callback, Long timeout) { packet.setFrom(account.getJid()); return this.sendUnmodifiedIqPacket(packet, callback, false, timeout); } - public String sendUnmodifiedIqPacket(final IqPacket packet, final OnIqPacketReceived callback, boolean force) { + public String sendUnmodifiedIqPacket(final Iq packet, final Consumer callback, boolean force) { return sendUnmodifiedIqPacket(packet, callback, force, null); } public synchronized String sendUnmodifiedIqPacket( - final IqPacket packet, final OnIqPacketReceived callback, boolean force, Long timeout) { + final Iq packet, final Consumer callback, boolean force, Long timeout) { + // TODO if callback != null verify that type is get or set if (packet.getId() == null) { - packet.setAttribute("id", nextRandomId()); + packet.setId(CryptoHelper.random(9)); } if (callback != null) { synchronized (this.packetCallbacks) { @@ -2543,9 +2542,9 @@ public class XmppConnection implements Runnable { if (timeout != null) { timeoutFuture = SCHEDULER.schedule(() -> { synchronized (this.packetCallbacks) { - final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.TIMEOUT); - final Pair> removedCallback = packetCallbacks.remove(packet.getId()); - if (removedCallback != null) removedCallback.second.first.onIqPacketReceived(account, failurePacket); + final var failurePacket = new Iq(Iq.Type.TIMEOUT); + final var removedCallback = packetCallbacks.remove(packet.getId()); + if (removedCallback != null) removedCallback.second.first.accept(failurePacket); } }, timeout, TimeUnit.SECONDS); } @@ -2556,19 +2555,19 @@ public class XmppConnection implements Runnable { return packet.getId(); } - public void sendMessagePacket(final MessagePacket packet) { + public void sendMessagePacket(final im.conversations.android.xmpp.model.stanza.Message packet) { this.sendPacket(packet); } - public void sendPresencePacket(final PresencePacket packet) { + public void sendPresencePacket(final Presence packet) { this.sendPacket(packet); } - private synchronized void sendPacket(final AbstractStanza packet) { + private synchronized void sendPacket(final StreamElement packet) { sendPacket(packet, false); } - private synchronized void sendPacket(final AbstractStanza packet, final boolean force) { + private synchronized void sendPacket(final StreamElement packet, final boolean force) { if (stanzasSent == Integer.MAX_VALUE) { resetStreamId(); disconnect(true); @@ -2584,7 +2583,7 @@ public class XmppConnection implements Runnable { + " do not write stanza to unbound stream " + packet.toString()); } - if (packet instanceof AbstractAcknowledgeableStanza stanza) { + if (packet instanceof Stanza stanza) { if (this.mStanzaQueue.size() != 0) { int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1); if (currentHighestKey != stanzasSent) { @@ -2603,7 +2602,9 @@ public class XmppConnection implements Runnable { + stanzasSent); } this.mStanzaQueue.append(stanzasSent, stanza); - if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) { + if (stanza instanceof im.conversations.android.xmpp.model.stanza.Message + && stanza.getId() != null + && inSmacksSession) { if (Config.EXTENDED_SM_LOGGING) { Log.d( Config.LOGTAG, @@ -2611,7 +2612,7 @@ public class XmppConnection implements Runnable { + ": requesting ack for message stanza #" + stanzasSent); } - tagWriter.writeStanzaAsync(new RequestPacket()); + tagWriter.writeStanzaAsync(new Request()); } } } @@ -2619,7 +2620,7 @@ public class XmppConnection implements Runnable { public void sendPing() { if (!r()) { - final IqPacket iq = new IqPacket(IqPacket.TYPE.GET); + final Iq iq = new Iq(Iq.Type.GET); iq.setFrom(account.getJid()); iq.addChild("ping", Namespace.PING); this.sendIqPacket(iq, null); @@ -2627,18 +2628,6 @@ public class XmppConnection implements Runnable { this.lastPingSent = SystemClock.elapsedRealtime(); } - public void setOnMessagePacketReceivedListener(final OnMessagePacketReceived listener) { - this.messageListener = listener; - } - - public void setOnUnregisteredIqPacketReceivedListener(final OnIqPacketReceived listener) { - this.unregisteredIqListener = listener; - } - - public void setOnPresencePacketReceivedListener(final OnPresencePacketReceived listener) { - this.presenceListener = listener; - } - public void setOnJinglePacketReceivedListener(final OnJinglePacketReceived listener) { this.jingleListener = listener; } @@ -2647,10 +2636,6 @@ public class XmppConnection implements Runnable { this.statusListener = listener; } - public void setOnBindListener(final OnBindListener listener) { - this.bindListener = listener; - } - public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { this.acknowledgedListener = listener; } @@ -2746,7 +2731,7 @@ public class XmppConnection implements Runnable { public boolean r() { if (getFeatures().sm()) { - this.tagWriter.writeStanzaAsync(new RequestPacket()); + this.tagWriter.writeStanzaAsync(new Request()); return true; } else { return false; @@ -2824,11 +2809,11 @@ public class XmppConnection implements Runnable { } public void sendActive() { - this.sendPacket(new ActivePacket()); + this.sendPacket(new Active()); } public void sendInactive() { - this.sendPacket(new InactivePacket()); + this.sendPacket(new Inactive()); } public void resetAttemptCount(boolean resetConnectTime) { @@ -2848,11 +2833,11 @@ public class XmppConnection implements Runnable { public void trackOfflineMessageRetrieval(boolean trackOfflineMessageRetrieval) { if (trackOfflineMessageRetrieval) { - final IqPacket iqPing = new IqPacket(IqPacket.TYPE.GET); + final Iq iqPing = new Iq(Iq.Type.GET); iqPing.addChild("ping", Namespace.PING); this.sendIqPacket( iqPing, - (a, response) -> { + (response) -> { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -2868,6 +2853,20 @@ public class XmppConnection implements Runnable { return this.offlineMessagesRetrieved; } + public void fetchRoster() { + final Iq iqPacket = new Iq(Iq.Type.GET); + final var version = account.getRosterVersion(); + if (Strings.isNullOrEmpty(account.getRosterVersion())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster"); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": fetching roster version " + version); + } + iqPacket.query(Namespace.ROSTER).setAttribute("ver", version); + sendIqPacket(iqPacket, unregisteredIqListener); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { @@ -3049,13 +3048,12 @@ public class XmppConnection implements Runnable { public boolean sm() { return streamId != null || (connection.streamFeatures != null - && connection.streamFeatures.hasChild( - "sm", Namespace.STREAM_MANAGEMENT)); + && connection.streamFeatures.streamManagement()); } public boolean csi() { return connection.streamFeatures != null - && connection.streamFeatures.hasChild("csi", Namespace.CSI); + && connection.streamFeatures.clientStateIndication(); } public boolean pep() { @@ -3074,6 +3072,21 @@ public class XmppConnection implements Runnable { } } + public boolean bind2() { + final var loginInfo = XmppConnection.this.loginInfo; + return loginInfo != null && !loginInfo.inlineBindFeatures.isEmpty(); + } + + public boolean sasl2() { + final var loginInfo = XmppConnection.this.loginInfo; + return loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2; + } + + public String loginMechanism() { + final var loginInfo = XmppConnection.this.loginInfo; + return loginInfo == null ? null : loginInfo.saslMechanism.getMechanism(); + } + public boolean pepPublishOptions() { return hasDiscoFeature(account.getJid().asBareJid(), Namespace.PUBSUB_PUBLISH_OPTIONS); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index 9f0bd72aeffe7defb4dcae08b9470bfa9ba05590..3a47934854bd4eb25439342286536a2a83d856a2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -59,8 +59,8 @@ public class Data extends Element { field.setValues(values); } - public void submit(Bundle options) { - for (Field field : getFields()) { + public void submit(final Bundle options) { + for (final Field field : getFields()) { if (options.containsKey(field.getFieldName())) { field.setValue(options.getString(field.getFieldName())); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java index 847678a05753255f1bd600ef928403d0e289038e..2c3c3f762695e712f4f0a3011018ffb4949e1e3e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractContentMap.java @@ -8,7 +8,8 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import java.util.List; import java.util.Map; @@ -47,8 +48,9 @@ public abstract class AbstractContentMap< return ImmutableList.copyOf(contents.keySet()); } - JinglePacket toJinglePacket(final JinglePacket.Action action, final String sessionId) { - final JinglePacket jinglePacket = new JinglePacket(action, sessionId); + Iq toJinglePacket(final Jingle.Action action, final String sessionId) { + final Iq iq = new Iq(Iq.Type.SET); + final var jinglePacket = iq.addExtension(new Jingle(action, sessionId)); for (final Map.Entry> entry : this.contents.entrySet()) { final DescriptionTransport descriptionTransport = entry.getValue(); final Content content = @@ -65,7 +67,7 @@ public abstract class AbstractContentMap< if (this.group != null) { jinglePacket.addGroup(this.group); } - return jinglePacket; + return iq; } void requireContentDescriptions() { 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 6aeb348c11b126d35ab803809594a385dac027c5..7716792885eb154a8cb0d8a75b23ccad04f51f98 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -19,9 +19,9 @@ import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Arrays; import java.util.Collection; @@ -184,10 +184,10 @@ public abstract class AbstractJingleConnection { return TERMINATED.contains(this.state); } - abstract void deliverPacket(JinglePacket jinglePacket); + abstract void deliverPacket(Iq jinglePacket); protected void receiveOutOfOrderAction( - final JinglePacket jinglePacket, final JinglePacket.Action action) { + final Iq jinglePacket, final Jingle.Action action) { Log.d( Config.LOGTAG, String.format( @@ -205,7 +205,7 @@ public abstract class AbstractJingleConnection { } } - protected void terminateWithOutOfOrder(final JinglePacket jinglePacket) { + protected void terminateWithOutOfOrder(final Iq jinglePacket) { Log.d( Config.LOGTAG, id.account.getJid().asBareJid() + ": terminating session with out-of-order"); @@ -235,37 +235,38 @@ public abstract class AbstractJingleConnection { if (previous != State.NULL && trigger != null) { trigger.accept(target); } - final JinglePacket jinglePacket = - new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); + final var iq = new Iq(Iq.Type.SET); + final var jinglePacket = + iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId)); jinglePacket.setReason(reason, text); - send(jinglePacket); + send(iq); finish(); } - protected void send(final JinglePacket jinglePacket) { + protected void send(final Iq jinglePacket) { jinglePacket.setTo(id.with); xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse); } - protected void respondOk(final JinglePacket jinglePacket) { + protected void respondOk(final Iq jinglePacket) { xmppConnectionService.sendIqPacket( - id.account, jinglePacket.generateResponse(IqPacket.TYPE.RESULT), null); + id.account, jinglePacket.generateResponse(Iq.Type.RESULT), null); } - protected void respondWithTieBreak(final JinglePacket jinglePacket) { + protected void respondWithTieBreak(final Iq jinglePacket) { respondWithJingleError(jinglePacket, "tie-break", "conflict", "cancel"); } - protected void respondWithOutOfOrder(final JinglePacket jinglePacket) { + protected void respondWithOutOfOrder(final Iq jinglePacket) { respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); } - protected void respondWithItemNotFound(final JinglePacket jinglePacket) { + protected void respondWithItemNotFound(final Iq jinglePacket) { respondWithJingleError(jinglePacket, null, "item-not-found", "cancel"); } private void respondWithJingleError( - final IqPacket original, + final Iq original, String jingleCondition, String condition, String conditionType) { @@ -273,18 +274,18 @@ public abstract class AbstractJingleConnection { id.account, original, jingleCondition, condition, conditionType); } - private synchronized void handleIqResponse(final Account account, final IqPacket response) { - if (response.getType() == IqPacket.TYPE.ERROR) { + private synchronized void handleIqResponse(final Iq response) { + if (response.getType() == Iq.Type.ERROR) { handleIqErrorResponse(response); return; } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { handleIqTimeoutResponse(response); } } - protected void handleIqErrorResponse(final IqPacket response) { - Preconditions.checkArgument(response.getType() == IqPacket.TYPE.ERROR); + protected void handleIqErrorResponse(final Iq response) { + Preconditions.checkArgument(response.getType() == Iq.Type.ERROR); final String errorCondition = response.getErrorCondition(); Log.d( Config.LOGTAG, @@ -316,8 +317,8 @@ public abstract class AbstractJingleConnection { this.finish(); } - protected void handleIqTimeoutResponse(final IqPacket response) { - Preconditions.checkArgument(response.getType() == IqPacket.TYPE.TIMEOUT); + protected void handleIqTimeoutResponse(final Iq response) { + Preconditions.checkArgument(response.getType() == Iq.Type.TIMEOUT); Log.d( Config.LOGTAG, id.account.getJid().asBareJid() @@ -361,8 +362,8 @@ public abstract class AbstractJingleConnection { this.sessionId = sessionId; } - public static Id of(Account account, JinglePacket jinglePacket) { - return new Id(account, jinglePacket.getFrom(), jinglePacket.getSessionId()); + public static Id of(Account account, Iq iq, final Jingle jingle) { + return new Id(account, iq.getFrom(), jingle.getSessionId()); } public static Id of(Account account, Jid with, final String sessionId) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java index c678c91cb8b1de6fba4994a69ec3408620202954..d0f39747eae75b72c815bb5c00c9d99d41296418 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/FileTransferContentMap.java @@ -13,11 +13,10 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; -import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; import eu.siacs.conversations.xmpp.jingle.transports.Transport; +import im.conversations.android.xmpp.model.jingle.Jingle; import java.util.Arrays; import java.util.Collections; @@ -39,7 +38,7 @@ public class FileTransferContentMap super(group, contents); } - public static FileTransferContentMap of(final JinglePacket jinglePacket) { + public static FileTransferContentMap of(final Jingle jinglePacket) { final Map> contents = of(jinglePacket.getJingleContents()); return new FileTransferContentMap(jinglePacket.getGroup(), contents); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java index 7b2f884578f55ed00e28db715b23db1ac534ba7a..5149470f09bca5fe0ef6eb9c494a407fac5a30cc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java @@ -10,7 +10,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; import org.webrtc.PeerConnection; @@ -20,9 +20,9 @@ import java.util.List; public final class IceServers { - public static List parse(final IqPacket response) { + public static List parse(final Iq response) { ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); - if (response.getType() == IqPacket.TYPE.RESULT) { + if (response.getType() == Iq.Type.RESULT) { final Element services = response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); final List children = diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 7a3301bfceb948ddf98e593ca8cdf7e920d0a43b..de0d9a502d56fce5e4701531c07caeb48313b7f9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -34,14 +34,13 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport; import eu.siacs.conversations.xmpp.jingle.transports.Transport; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import java.lang.ref.WeakReference; import java.security.SecureRandom; @@ -77,9 +76,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Base64.encodeToString(id, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); } - public void deliverPacket(final Account account, final JinglePacket packet) { - final String sessionId = packet.getSessionId(); - final JinglePacket.Action action = packet.getAction(); + public void deliverPacket(final Account account, final Iq packet) { + final var jingle = packet.getExtension(Jingle.class); + Preconditions.checkNotNull(jingle,"Passed iq packet w/o jingle extension to Connection Manager"); + final String sessionId = jingle.getSessionId(); + final Jingle.Action action = jingle.getAction(); if (sessionId == null) { respondWithJingleError(account, packet, "unknown-session", "item-not-found", "cancel"); return; @@ -88,13 +89,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { respondWithJingleError(account, packet, null, "bad-request", "cancel"); return; } - final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet); + final AbstractJingleConnection.Id id = AbstractJingleConnection.Id.of(account, packet, jingle); final AbstractJingleConnection existingJingleConnection = connections.get(id); if (existingJingleConnection != null) { existingJingleConnection.deliverPacket(packet); - } else if (action == JinglePacket.Action.SESSION_INITIATE) { + } else if (action == Jingle.Action.SESSION_INITIATE) { final Jid from = packet.getFrom(); - final Content content = packet.getJingleContent(); + final Content content = jingle.getJingleContent(); final String descriptionNamespace = content == null ? null : content.getDescriptionNamespace(); final AbstractJingleConnection connection; @@ -165,14 +166,14 @@ public class JingleConnectionManager extends AbstractConnectionManager { } private void sendSessionTerminate( - final Account account, final IqPacket request, final AbstractJingleConnection.Id id) { + final Account account, final Iq request, final AbstractJingleConnection.Id id) { mXmppConnectionService.sendIqPacket( - account, request.generateResponse(IqPacket.TYPE.RESULT), null); - final JinglePacket sessionTermination = - new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); - sessionTermination.setTo(id.with); + account, request.generateResponse(Iq.Type.RESULT), null); + final var iq = new Iq(Iq.Type.SET); + iq.setTo(id.with); + final var sessionTermination = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId)); sessionTermination.setReason(Reason.BUSY, null); - mXmppConnectionService.sendIqPacket(account, sessionTermination, null); + mXmppConnectionService.sendIqPacket(account, iq, null); } private boolean isUsingClearNet(final Account account) { @@ -265,11 +266,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { void respondWithJingleError( final Account account, - final IqPacket original, + final Iq original, final String jingleCondition, final String condition, final String conditionType) { - final IqPacket response = original.generateResponse(IqPacket.TYPE.ERROR); + final Iq response = original.generateResponse(Iq.Type.ERROR); final Element error = response.addChild("error"); error.setAttribute("type", conditionType); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); @@ -440,7 +441,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final int activeDevices = account.activeDevicesWithRtpCapability(); Log.d(Config.LOGTAG, "active devices with rtp capability: " + activeDevices); if (activeDevices == 0) { - final MessagePacket reject = + final var reject = mXmppConnectionService .getMessageGenerator() .sessionReject(from, sessionId); @@ -494,10 +495,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (remoteMsgId == null) { return; } - final MessagePacket errorMessage = new MessagePacket(); + final var errorMessage = + new im.conversations.android.xmpp.model.stanza.Message(); errorMessage.setTo(from); errorMessage.setId(remoteMsgId); - errorMessage.setType(MessagePacket.TYPE_ERROR); + errorMessage.setType(im.conversations.android.xmpp.model.stanza.Message.Type.ERROR); final Element error = errorMessage.addChild("error"); error.setAttribute("code", "404"); error.setAttribute("type", "cancel"); @@ -722,7 +724,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { rtpSessionProposal.sessionId, RtpEndUserState.RETRACTED); } - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal); writeLogMissedOutgoing( account, @@ -791,7 +793,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { this.rtpSessionProposals.put(proposal, DeviceDiscoveryState.SEARCHING); mXmppConnectionService.notifyJingleRtpConnectionUpdate( account, proposal.with, proposal.sessionId, RtpEndUserState.FINDING_DEVICE); - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); return proposal; @@ -801,7 +803,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { public void sendJingleMessageFinish( final Contact contact, final String sessionId, final Reason reason) { final var account = contact.getAccount(); - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService .getMessageGenerator() .sessionFinish(contact.getJid(), sessionId, reason); @@ -843,7 +845,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return false; } - public void deliverIbbPacket(final Account account, final IqPacket packet) { + public void deliverIbbPacket(final Account account, final Iq packet) { final String sid; final Element payload; final InbandBytestreamsTransport.PacketType packetType; @@ -869,7 +871,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Config.LOGTAG, account.getJid().asBareJid() + ": unable to deliver ibb packet. missing sid"); account.getXmppConnection() - .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + .sendIqPacket(packet.generateResponse(Iq.Type.ERROR), null); return; } for (final AbstractJingleConnection connection : this.connections.values()) { @@ -880,11 +882,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (inBandTransport.deliverPacket(packetType, packet.getFrom(), payload)) { account.getXmppConnection() .sendIqPacket( - packet.generateResponse(IqPacket.TYPE.RESULT), null); + packet.generateResponse(Iq.Type.RESULT), null); } else { account.getXmppConnection() .sendIqPacket( - packet.generateResponse(IqPacket.TYPE.ERROR), null); + packet.generateResponse(Iq.Type.ERROR), null); } return; } @@ -895,7 +897,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { Config.LOGTAG, account.getJid().asBareJid() + ": unable to deliver ibb packet with sid=" + sid); account.getXmppConnection() - .sendIqPacket(packet.generateResponse(IqPacket.TYPE.ERROR), null); + .sendIqPacket(packet.generateResponse(Iq.Type.ERROR), null); } public void notifyRebound(final Account account) { @@ -946,7 +948,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { account.getJid().asBareJid() + ": resending session proposal to " + proposal.with); - final MessagePacket messagePacket = + final var messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); } 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 68564ec3c4e08569a6abec58c9fa31c867200c94..cb8e1d48da2eea7e4184252f101e8026156ba1a0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -31,7 +31,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; @@ -39,7 +38,9 @@ import eu.siacs.conversations.xmpp.jingle.transports.InbandBytestreamsTransport; import eu.siacs.conversations.xmpp.jingle.transports.SocksByteStreamsTransport; import eu.siacs.conversations.xmpp.jingle.transports.Transport; import eu.siacs.conversations.xmpp.jingle.transports.WebRTCDataChannelTransport; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import org.bouncycastle.crypto.engines.AESEngine; import org.bouncycastle.crypto.io.CipherInputStream; @@ -112,22 +113,23 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } @Override - void deliverPacket(final JinglePacket jinglePacket) { - switch (jinglePacket.getAction()) { - case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket); - case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket); - case SESSION_INFO -> receiveSessionInfo(jinglePacket); - case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket); - case TRANSPORT_ACCEPT -> receiveTransportAccept(jinglePacket); - case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket); - case TRANSPORT_REPLACE -> receiveTransportReplace(jinglePacket); + void deliverPacket(final Iq iq) { + final var jingle = iq.getExtension(Jingle.class); + switch (jingle.getAction()) { + case SESSION_ACCEPT -> receiveSessionAccept(iq, jingle); + case SESSION_INITIATE -> receiveSessionInitiate(iq, jingle); + case SESSION_INFO -> receiveSessionInfo(iq, jingle); + case SESSION_TERMINATE -> receiveSessionTerminate(iq, jingle); + case TRANSPORT_ACCEPT -> receiveTransportAccept(iq, jingle); + case TRANSPORT_INFO -> receiveTransportInfo(iq, jingle); + case TRANSPORT_REPLACE -> receiveTransportReplace(iq, jingle); default -> { - respondOk(jinglePacket); + respondOk(iq); Log.d( Config.LOGTAG, String.format( "%s: received unhandled jingle action %s", - id.account.getJid().asBareJid(), jinglePacket.getAction())); + id.account.getJid().asBareJid(), jingle.getAction())); } } } @@ -203,33 +205,34 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (transition( State.SESSION_INITIALIZED, () -> this.initiatorFileTransferContentMap = contentMap)) { - final var jinglePacket = - contentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final var iq = + contentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId); + final var jingle = iq.getExtension(Jingle.class); if (xmppAxolotlMessage != null) { this.transportSecurity = new TransportSecurity( xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV()); - final var contents = jinglePacket.getJingleContents(); + final var contents = jingle.getJingleContents(); final var rawContent = contents.get(Iterables.getOnlyElement(contentMap.contents.keySet())); if (rawContent != null) { rawContent.setSecurity(xmppAxolotlMessage); } } - jinglePacket.setTo(id.with); + iq.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, - jinglePacket, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + iq, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { xmppConnectionService.markMessage(message, Message.STATUS_OFFERED); return; } - if (response.getType() == IqPacket.TYPE.ERROR) { + if (response.getType() == Iq.Type.ERROR) { handleIqErrorResponse(response); return; } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { handleIqTimeoutResponse(response); } }); @@ -237,15 +240,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } } - private void receiveSessionAccept(final JinglePacket jinglePacket) { + private void receiveSessionAccept(final Iq jinglePacket, final Jingle jingle) { Log.d(Config.LOGTAG, "receive file transfer session accept"); if (isResponder()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT); return; } final FileTransferContentMap contentMap; try { - contentMap = FileTransferContentMap.of(jinglePacket); + contentMap = FileTransferContentMap.of(jingle); contentMap.requireOnlyFileTransferDescription(); } catch (final RuntimeException e) { Log.d( @@ -261,7 +264,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private void receiveSessionAccept( - final JinglePacket jinglePacket, final FileTransferContentMap contentMap) { + final Iq jinglePacket, final FileTransferContentMap contentMap) { if (transition(State.SESSION_ACCEPTED, () -> setRemoteContentMap(contentMap))) { respondOk(jinglePacket); final var transport = this.transport; @@ -280,7 +283,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.d( Config.LOGTAG, id.account.getJid().asBareJid() + ": receive out of order session-accept"); - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT); } } @@ -309,16 +312,16 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } } - private void receiveSessionInitiate(final JinglePacket jinglePacket) { + private void receiveSessionInitiate(final Iq jinglePacket, final Jingle jingle) { if (isInitiator()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE); return; } Log.d(Config.LOGTAG, "receive session initiate " + jinglePacket); final FileTransferContentMap contentMap; final FileTransferDescription.File file; try { - contentMap = FileTransferContentMap.of(jinglePacket); + contentMap = FileTransferContentMap.of(jingle); contentMap.requireContentDescriptions(); file = contentMap.requireOnlyFile(); // TODO check is offer @@ -332,7 +335,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection return; } final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; - final var contents = jinglePacket.getJingleContents(); + final var contents = jingle.getJingleContents(); final var rawContent = contents.get(Iterables.getOnlyElement(contentMap.contents.keySet())); final var security = rawContent == null ? null : rawContent.getSecurity(jinglePacket.getFrom()); @@ -349,7 +352,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private void receiveSessionInitiate( - final JinglePacket jinglePacket, + final Iq jinglePacket, final FileTransferContentMap contentMap, final FileTransferDescription.File file, final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage) { @@ -396,7 +399,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.d( Config.LOGTAG, id.account.getJid().asBareJid() + ": receive out of order session-initiate"); - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE); } } @@ -453,9 +456,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void sendSessionAccept(final FileTransferContentMap contentMap) { setLocalContentMap(contentMap); - final var jinglePacket = - contentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); - send(jinglePacket); + final var iq = + contentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId); + send(iq); // this needs to come after session-accept or else our candidate-error might arrive first this.transport.connect(); this.transport.readyToSentAdditionalCandidates(); @@ -541,9 +544,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection sendSessionTerminate(Reason.ofThrowable(rootCause), rootCause.getMessage()); } - private void receiveSessionInfo(final JinglePacket jinglePacket) { + private void receiveSessionInfo(final Iq jinglePacket, final Jingle jingle) { respondOk(jinglePacket); - final var sessionInfo = FileTransferDescription.getSessionInfo(jinglePacket); + final var sessionInfo = FileTransferDescription.getSessionInfo(jingle); if (sessionInfo instanceof FileTransferDescription.Checksum checksum) { receiveSessionInfoChecksum(checksum); } else if (sessionInfo instanceof FileTransferDescription.Received received) { @@ -559,9 +562,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.d(Config.LOGTAG, "peer confirmed received " + received); } - private void receiveSessionTerminate(final JinglePacket jinglePacket) { + private void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) { respondOk(jinglePacket); - final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); + final Jingle.ReasonWrapper wrapper = jingle.getReason(); final State previous = this.state; Log.d( Config.LOGTAG, @@ -590,15 +593,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection finish(); } - private void receiveTransportAccept(final JinglePacket jinglePacket) { + private void receiveTransportAccept(final Iq jinglePacket, final Jingle jingle) { if (isResponder()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_ACCEPT); return; } Log.d(Config.LOGTAG, "receive transport accept " + jinglePacket); final GenericTransportInfo transportInfo; try { - transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo(); + transportInfo = FileTransferContentMap.of(jingle).requireOnlyTransportInfo(); } catch (final RuntimeException e) { Log.d( Config.LOGTAG, @@ -610,15 +613,15 @@ public class JingleFileTransferConnection extends AbstractJingleConnection return; } if (isInState(State.SESSION_ACCEPTED)) { - final var group = jinglePacket.getGroup(); + final var group = jingle.getGroup(); receiveTransportAccept(jinglePacket, new Transport.TransportInfo(transportInfo, group)); } else { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_ACCEPT); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_ACCEPT); } } private void receiveTransportAccept( - final JinglePacket jinglePacket, final Transport.TransportInfo transportInfo) { + final Iq jinglePacket, final Transport.TransportInfo transportInfo) { final FileTransferContentMap remoteContentMap = getRemoteContentMap().withTransport(transportInfo); setRemoteContentMap(remoteContentMap); @@ -637,11 +640,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } } - private void receiveTransportInfo(final JinglePacket jinglePacket) { + private void receiveTransportInfo(final Iq jinglePacket, final Jingle jingle) { final FileTransferContentMap contentMap; final GenericTransportInfo transportInfo; try { - contentMap = FileTransferContentMap.of(jinglePacket); + contentMap = FileTransferContentMap.of(jingle); transportInfo = contentMap.requireOnlyTransportInfo(); } catch (final RuntimeException e) { Log.d( @@ -725,14 +728,14 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } } - private void receiveTransportReplace(final JinglePacket jinglePacket) { + private void receiveTransportReplace(final Iq jinglePacket, final Jingle jingle) { if (isInitiator()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_REPLACE); return; } final GenericTransportInfo transportInfo; try { - transportInfo = FileTransferContentMap.of(jinglePacket).requireOnlyTransportInfo(); + transportInfo = FileTransferContentMap.of(jingle).requireOnlyTransportInfo(); } catch (final RuntimeException e) { Log.d( Config.LOGTAG, @@ -746,12 +749,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (isInState(State.SESSION_ACCEPTED)) { receiveTransportReplace(jinglePacket, transportInfo); } else { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.TRANSPORT_REPLACE); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_REPLACE); } } private void receiveTransportReplace( - final JinglePacket jinglePacket, final GenericTransportInfo transportInfo) { + final Iq jinglePacket, final GenericTransportInfo transportInfo) { respondOk(jinglePacket); final Transport currentTransport = this.transport; if (currentTransport != null) { @@ -796,11 +799,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void sendTransportAccept(final FileTransferContentMap contentMap) { setLocalContentMap(contentMap); - final var jinglePacket = + final var iq = contentMap .transportInfo() - .toJinglePacket(JinglePacket.Action.TRANSPORT_ACCEPT, id.sessionId); - send(jinglePacket); + .toJinglePacket(Jingle.Action.TRANSPORT_ACCEPT, id.sessionId); + send(iq); transport.connect(); } @@ -982,11 +985,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) { - final var jinglePacket = - new JinglePacket(JinglePacket.Action.SESSION_INFO, this.id.sessionId); - jinglePacket.addJingleChild(sessionInfo.asElement()); - jinglePacket.setTo(this.id.with); - send(jinglePacket); + final var iq = new Iq(Iq.Type.SET); + final var jinglePacket = iq.addExtension(new Jingle(Jingle.Action.SESSION_INFO, this.id.sessionId)); + jinglePacket.addChild(sessionInfo.asElement()); + send(iq); } @Override @@ -1039,11 +1041,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void sendTransportReplace(final FileTransferContentMap contentMap) { setLocalContentMap(contentMap); - final var jinglePacket = + final var iq = contentMap .transportInfo() - .toJinglePacket(JinglePacket.Action.TRANSPORT_REPLACE, id.sessionId); - send(jinglePacket); + .toJinglePacket(Jingle.Action.TRANSPORT_REPLACE, id.sessionId); + send(iq); } @Override @@ -1068,9 +1070,9 @@ public class JingleFileTransferConnection extends AbstractJingleConnection + contentName); return; } - final JinglePacket jinglePacket = - transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - send(jinglePacket); + final Iq iq = + transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + send(iq); } @Override @@ -1081,12 +1083,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.e(Config.LOGTAG, "local content map is null on candidate used"); return; } - final var jinglePacket = + final var iq = contentMap .candidateUsed(streamId, candidate.cid) - .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - Log.d(Config.LOGTAG, "sending candidate used " + jinglePacket); - send(jinglePacket); + .toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "sending candidate used " + iq); + send(iq); } @Override @@ -1096,12 +1098,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.e(Config.LOGTAG, "local content map is null on candidate used"); return; } - final var jinglePacket = + final var iq = contentMap .candidateError(streamId) - .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - Log.d(Config.LOGTAG, "sending candidate error " + jinglePacket); - send(jinglePacket); + .toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "sending candidate error " + iq); + send(iq); } @Override @@ -1111,11 +1113,11 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.e(Config.LOGTAG, "local content map is null on candidate used"); return; } - final var jinglePacket = + final var iq = contentMap .proxyActivated(streamId, candidate.cid) - .toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - send(jinglePacket); + .toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + send(iq); } @Override @@ -1251,10 +1253,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection message, Message.STATUS_SEND_FAILED, Message.ERROR_MESSAGE_CANCELLED); } terminateTransport(); - final JinglePacket jinglePacket = - new JinglePacket(JinglePacket.Action.SESSION_TERMINATE, id.sessionId); - jinglePacket.setReason(reason, "User requested to stop file transfer"); - send(jinglePacket); + final Iq iq = new Iq(Iq.Type.SET); + final var jingle = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId)); + jingle.setReason(reason, "User requested to stop file transfer"); + send(iq); finish(); return true; } else { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3f2bf1bf113f07d021fa6fc10007fb715c466d5b..0de9c6258b2e0f80dff743067d12e79c8279b008 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -45,13 +45,13 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import eu.siacs.conversations.xmpp.stanzas.MessagePacket; + +import im.conversations.android.xmpp.model.jingle.Jingle; +import im.conversations.android.xmpp.model.stanza.Iq; import org.webrtc.DtmfSender; import org.webrtc.EglBase; @@ -145,24 +145,25 @@ public class JingleRtpConnection extends AbstractJingleConnection } @Override - synchronized void deliverPacket(final JinglePacket jinglePacket) { - switch (jinglePacket.getAction()) { - case SESSION_INITIATE -> receiveSessionInitiate(jinglePacket); - case TRANSPORT_INFO -> receiveTransportInfo(jinglePacket); - case SESSION_ACCEPT -> receiveSessionAccept(jinglePacket); - case SESSION_TERMINATE -> receiveSessionTerminate(jinglePacket); - case CONTENT_ADD -> receiveContentAdd(jinglePacket); - case CONTENT_ACCEPT -> receiveContentAccept(jinglePacket); - case CONTENT_REJECT -> receiveContentReject(jinglePacket); - case CONTENT_REMOVE -> receiveContentRemove(jinglePacket); - case CONTENT_MODIFY -> receiveContentModify(jinglePacket); + synchronized void deliverPacket(final Iq iq) { + final var jingle = iq.getExtension(Jingle.class); + switch (jingle.getAction()) { + case SESSION_INITIATE -> receiveSessionInitiate(iq, jingle); + case TRANSPORT_INFO -> receiveTransportInfo(iq, jingle); + case SESSION_ACCEPT -> receiveSessionAccept(iq, jingle); + case SESSION_TERMINATE -> receiveSessionTerminate(iq); + case CONTENT_ADD -> receiveContentAdd(iq, jingle); + case CONTENT_ACCEPT -> receiveContentAccept(iq); + case CONTENT_REJECT -> receiveContentReject(iq, jingle); + case CONTENT_REMOVE -> receiveContentRemove(iq, jingle); + case CONTENT_MODIFY -> receiveContentModify(iq, jingle); default -> { - respondOk(jinglePacket); + respondOk(iq); Log.d( Config.LOGTAG, String.format( "%s: received unhandled jingle action %s", - id.account.getJid().asBareJid(), jinglePacket.getAction())); + id.account.getJid().asBareJid(), jingle.getAction())); } } } @@ -193,9 +194,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return webRTCWrapper.applyDtmfTone(tone); } - private void receiveSessionTerminate(final JinglePacket jinglePacket) { + private void receiveSessionTerminate(final Iq jinglePacket) { respondOk(jinglePacket); - final JinglePacket.ReasonWrapper wrapper = jinglePacket.getReason(); + final var jingle = jinglePacket.getExtension(Jingle.class); + final Jingle.ReasonWrapper wrapper = jingle.getReason(); final State previous = this.state; Log.d( Config.LOGTAG, @@ -224,7 +226,7 @@ public class JingleRtpConnection extends AbstractJingleConnection finish(); } - private void receiveTransportInfo(final JinglePacket jinglePacket) { + private void receiveTransportInfo(final Iq jinglePacket, final Jingle jingle) { // Due to the asynchronicity of processing session-init we might move from NULL|PROCEED to // INITIALIZED only after transport-info has been received if (isInState( @@ -235,7 +237,7 @@ public class JingleRtpConnection extends AbstractJingleConnection State.SESSION_ACCEPTED)) { final RtpContentMap contentMap; try { - contentMap = RtpContentMap.of(jinglePacket); + contentMap = RtpContentMap.of(jingle); } catch (final IllegalArgumentException | NullPointerException e) { Log.d( Config.LOGTAG, @@ -265,7 +267,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveTransportInfo( - final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Iq jinglePacket, final RtpContentMap contentMap) { final Set>> candidates = contentMap.contents.entrySet(); final RtpContentMap remote = getRemoteContentMap(); @@ -304,17 +306,17 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveContentAdd(final JinglePacket jinglePacket) { + private void receiveContentAdd(final Iq iq, final Jingle jingle) { final RtpContentMap modification; try { - modification = RtpContentMap.of(jinglePacket); + modification = RtpContentMap.of(jingle); modification.requireContentDescriptions(); } catch (final RuntimeException e) { Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": improperly formatted contents", Throwables.getRootCause(e)); - respondOk(jinglePacket); + respondOk(iq); webRTCWrapper.close(); sendSessionTerminate(Reason.of(e), e.getMessage()); return; @@ -330,12 +332,12 @@ public class JingleRtpConnection extends AbstractJingleConnection new FutureCallback<>() { @Override public void onSuccess(final RtpContentMap rtpContentMap) { - receiveContentAdd(jinglePacket, rtpContentMap); + receiveContentAdd(iq, rtpContentMap); } @Override public void onFailure(@NonNull Throwable throwable) { - respondOk(jinglePacket); + respondOk(iq); final Throwable rootCause = Throwables.getRootCause(throwable); Log.d( Config.LOGTAG, @@ -349,12 +351,12 @@ public class JingleRtpConnection extends AbstractJingleConnection }, MoreExecutors.directExecutor()); } else { - terminateWithOutOfOrder(jinglePacket); + terminateWithOutOfOrder(iq); } } private void receiveContentAdd( - final JinglePacket jinglePacket, final RtpContentMap modification) { + final Iq jinglePacket, final RtpContentMap modification) { final RtpContentMap remote = getRemoteContentMap(); if (!Collections.disjoint(modification.getNames(), remote.getNames())) { respondOk(jinglePacket); @@ -406,10 +408,11 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveContentAccept(final JinglePacket jinglePacket) { + private void receiveContentAccept(final Iq jinglePacket) { + final var jingle = jinglePacket.getExtension(Jingle.class); final RtpContentMap receivedContentAccept; try { - receivedContentAccept = RtpContentMap.of(jinglePacket); + receivedContentAccept = RtpContentMap.of(jingle); receivedContentAccept.requireContentDescriptions(); } catch (final RuntimeException e) { Log.d( @@ -494,14 +497,14 @@ public class JingleRtpConnection extends AbstractJingleConnection updateEndUserState(); } - private void receiveContentModify(final JinglePacket jinglePacket) { + private void receiveContentModify(final Iq jinglePacket, final Jingle jingle) { if (this.state != State.SESSION_ACCEPTED) { terminateWithOutOfOrder(jinglePacket); return; } final Map modification = Maps.transformEntries( - jinglePacket.getJingleContents(), (key, value) -> value.getSenders()); + jingle.getJingleContents(), (key, value) -> value.getSenders()); final boolean isInitiator = isInitiator(); final RtpContentMap currentOutgoing = this.outgoingContentAdd; final RtpContentMap remoteContentMap = this.getRemoteContentMap(); @@ -604,10 +607,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return candidateBuilder.build(); } - private void receiveContentReject(final JinglePacket jinglePacket) { + private void receiveContentReject(final Iq jinglePacket, final Jingle jingle) { final RtpContentMap receivedContentReject; try { - receivedContentReject = RtpContentMap.of(jinglePacket); + receivedContentReject = RtpContentMap.of(jingle); } catch (final RuntimeException e) { Log.d( Config.LOGTAG, @@ -660,10 +663,10 @@ public class JingleRtpConnection extends AbstractJingleConnection + summary); } - private void receiveContentRemove(final JinglePacket jinglePacket) { + private void receiveContentRemove(final Iq jinglePacket, final Jingle jingle) { final RtpContentMap receivedContentRemove; try { - receivedContentRemove = RtpContentMap.of(jinglePacket); + receivedContentRemove = RtpContentMap.of(jingle); receivedContentRemove.requireContentDescriptions(); } catch (final RuntimeException e) { Log.d( @@ -697,8 +700,8 @@ public class JingleRtpConnection extends AbstractJingleConnection String.format( "%s only supports %s as a means to retract a not yet accepted %s", BuildConfig.APP_NAME, - JinglePacket.Action.CONTENT_REMOVE, - JinglePacket.Action.CONTENT_ADD)); + Jingle.Action.CONTENT_REMOVE, + Jingle.Action.CONTENT_ADD)); } } @@ -723,10 +726,10 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } this.outgoingContentAdd = null; - final JinglePacket retract = + final Iq retract = outgoingContentAdd .toStub() - .toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId); + .toJinglePacket(Jingle.Action.CONTENT_REMOVE, id.sessionId); this.send(retract); Log.d( Config.LOGTAG, @@ -782,16 +785,16 @@ public class JingleRtpConnection extends AbstractJingleConnection "content addition is receive only. we want to upgrade to 'both'"); final RtpContentMap modifiedSenders = incomingContentAdd.modifiedSenders(Content.Senders.BOTH); - final JinglePacket proposedContentModification = + final Iq proposedContentModification = modifiedSenders .toStub() - .toJinglePacket(JinglePacket.Action.CONTENT_MODIFY, id.sessionId); + .toJinglePacket(Jingle.Action.CONTENT_MODIFY, id.sessionId); proposedContentModification.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, proposedContentModification, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d( Config.LOGTAG, id.account.getJid().asBareJid() @@ -885,7 +888,7 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onFailure(@NonNull final Throwable throwable) { - failureToPerformAction(JinglePacket.Action.CONTENT_ACCEPT, throwable); + failureToPerformAction(Jingle.Action.CONTENT_ACCEPT, throwable); } }, MoreExecutors.directExecutor()); @@ -897,9 +900,9 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void sendContentAccept(final RtpContentMap contentAcceptMap) { - final JinglePacket jinglePacket = - contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId); - send(jinglePacket); + final Iq iq = + contentAcceptMap.toJinglePacket(Jingle.Action.CONTENT_ACCEPT, id.sessionId); + send(iq); } public synchronized void rejectContentAdd() { @@ -913,20 +916,20 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void rejectContentAdd(final RtpContentMap contentMap) { - final JinglePacket jinglePacket = + final Iq iq = contentMap .toStub() - .toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId); + .toJinglePacket(Jingle.Action.CONTENT_REJECT, id.sessionId); Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": rejecting content " + ContentAddition.summary(contentMap)); - send(jinglePacket); + send(iq); } private boolean checkForIceRestart( - final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) { + final Iq jinglePacket, final RtpContentMap rtpContentMap) { final RtpContentMap existing = getRemoteContentMap(); final Set existingCredentials; final IceUdpTransportInfo.Credentials newCredentials; @@ -1005,7 +1008,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private boolean applyIceRestart( - final JinglePacket jinglePacket, + final Iq jinglePacket, final RtpContentMap restartContentMap, final boolean isOffer) throws ExecutionException, InterruptedException { @@ -1106,7 +1109,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private ListenableFuture receiveRtpContentMap( - final JinglePacket jinglePacket, final boolean expectVerification) { + final Jingle jinglePacket, final boolean expectVerification) { try { return receiveRtpContentMap(RtpContentMap.of(jinglePacket), expectVerification); } catch (final Exception e) { @@ -1149,12 +1152,12 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveSessionInitiate(final JinglePacket jinglePacket) { + private void receiveSessionInitiate(final Iq jinglePacket, final Jingle jingle) { if (isInitiator()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_INITIATE); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_INITIATE); return; } - final ListenableFuture future = receiveRtpContentMap(jinglePacket, false); + final ListenableFuture future = receiveRtpContentMap(jingle, false); Futures.addCallback( future, new FutureCallback<>() { @@ -1173,7 +1176,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveSessionInitiate( - final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Iq jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(true); @@ -1233,13 +1236,13 @@ public class JingleRtpConnection extends AbstractJingleConnection } } - private void receiveSessionAccept(final JinglePacket jinglePacket) { + private void receiveSessionAccept(final Iq jinglePacket, final Jingle jingle) { if (isResponder()) { - receiveOutOfOrderAction(jinglePacket, JinglePacket.Action.SESSION_ACCEPT); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.SESSION_ACCEPT); return; } final ListenableFuture future = - receiveRtpContentMap(jinglePacket, this.omemoVerification.hasFingerprint()); + receiveRtpContentMap(jingle, this.omemoVerification.hasFingerprint()); Futures.addCallback( future, new FutureCallback<>() { @@ -1264,7 +1267,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void receiveSessionAccept( - final JinglePacket jinglePacket, final RtpContentMap contentMap) { + final Iq jinglePacket, final RtpContentMap contentMap) { try { contentMap.requireContentDescriptions(); contentMap.requireDTLSFingerprint(); @@ -1409,7 +1412,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void failureToPerformAction( - final JinglePacket.Action action, final Throwable throwable) { + final Jingle.Action action, final Throwable throwable) { if (isTerminated()) { return; } @@ -1480,8 +1483,8 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } transitionOrThrow(State.SESSION_ACCEPTED); - final JinglePacket sessionAccept = - rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_ACCEPT, id.sessionId); + final Iq sessionAccept = + rtpContentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId); send(sessionAccept); } @@ -1951,8 +1954,8 @@ public class JingleRtpConnection extends AbstractJingleConnection return; } this.transitionOrThrow(targetState); - final JinglePacket sessionInitiate = - rtpContentMap.toJinglePacket(JinglePacket.Action.SESSION_INITIATE, id.sessionId); + final Iq sessionInitiate = + rtpContentMap.toJinglePacket(Jingle.Action.SESSION_INITIATE, id.sessionId); send(sessionInitiate); } @@ -2020,9 +2023,9 @@ public class JingleRtpConnection extends AbstractJingleConnection + contentName); return; } - final JinglePacket jinglePacket = - transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - send(jinglePacket); + final Iq iq = + transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + send(iq); } public RtpEndUserState getEndUserState() { @@ -2340,7 +2343,8 @@ public class JingleRtpConnection extends AbstractJingleConnection this.jingleConnectionManager.ensureConnectionIsRegistered(this); this.webRTCWrapper.setup(this.xmppConnectionService); this.webRTCWrapper.initializePeerConnection(media, iceServers, trickle); - this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled()); + // this.webRTCWrapper.setMicrophoneEnabledOrThrow(callIntegration.isMicrophoneEnabled()); + this.webRTCWrapper.setMicrophoneEnabledOrThrow(true); } private void acceptCallFromProposed() { @@ -2375,8 +2379,8 @@ public class JingleRtpConnection extends AbstractJingleConnection } private void sendJingleMessage(final String action, final Jid to) { - final MessagePacket messagePacket = new MessagePacket(); - messagePacket.setType(MessagePacket.TYPE_CHAT); // we want to carbon copy those + final var messagePacket = new im.conversations.android.xmpp.model.stanza.Message(); + messagePacket.setType(im.conversations.android.xmpp.model.stanza.Message.Type.CHAT); // we want to carbon copy those messagePacket.setTo(to); final Element intent = messagePacket @@ -2397,7 +2401,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void sendJingleMessageFinish(final Reason reason) { final var account = id.getAccount(); - final MessagePacket messagePacket = + final var messagePacket = xmppConnectionService .getMessageGenerator() .sessionFinish(id.with, id.sessionId, reason); @@ -2556,34 +2560,34 @@ public class JingleRtpConnection extends AbstractJingleConnection private void initiateIceRestart(final RtpContentMap rtpContentMap) { final RtpContentMap transportInfo = rtpContentMap.transportInfo(); - final JinglePacket jinglePacket = - transportInfo.toJinglePacket(JinglePacket.Action.TRANSPORT_INFO, id.sessionId); - Log.d(Config.LOGTAG, "initiating ice restart: " + jinglePacket); - jinglePacket.setTo(id.with); + final Iq iq = + transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + Log.d(Config.LOGTAG, "initiating ice restart: " + iq); + iq.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, - jinglePacket, - (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + iq, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, "received success to our ice restart"); setLocalContentMap(rtpContentMap); webRTCWrapper.setIsReadyToReceiveIceCandidates(true); return; } - if (response.getType() == IqPacket.TYPE.ERROR) { + if (response.getType() == Iq.Type.ERROR) { if (isTieBreak(response)) { Log.d(Config.LOGTAG, "received tie-break as result of ice restart"); return; } handleIqErrorResponse(response); } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { handleIqTimeoutResponse(response); } }); } - private boolean isTieBreak(final IqPacket response) { + private boolean isTieBreak(final Iq response) { final Element error = response.findChild("error"); return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS); } @@ -2604,7 +2608,7 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onFailure(@NonNull Throwable throwable) { - failureToPerformAction(JinglePacket.Action.CONTENT_ADD, throwable); + failureToPerformAction(Jingle.Action.CONTENT_ADD, throwable); } }, MoreExecutors.directExecutor()); @@ -2612,21 +2616,21 @@ public class JingleRtpConnection extends AbstractJingleConnection private void sendContentAdd(final RtpContentMap contentAdd) { - final JinglePacket jinglePacket = - contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId); - jinglePacket.setTo(id.with); + final Iq iq = + contentAdd.toJinglePacket(Jingle.Action.CONTENT_ADD, id.sessionId); + iq.setTo(id.with); xmppConnectionService.sendIqPacket( id.account, - jinglePacket, - (connection, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + iq, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d( Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": received ACK to our content-add"); return; } - if (response.getType() == IqPacket.TYPE.ERROR) { + if (response.getType() == Iq.Type.ERROR) { if (isTieBreak(response)) { this.outgoingContentAdd = null; Log.d(Config.LOGTAG, "received tie-break as result of our content-add"); @@ -2634,7 +2638,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } handleIqErrorResponse(response); } - if (response.getType() == IqPacket.TYPE.TIMEOUT) { + if (response.getType() == Iq.Type.TIMEOUT) { handleIqTimeoutResponse(response); } }); @@ -2782,7 +2786,12 @@ public class JingleRtpConnection extends AbstractJingleConnection @Override public void onCallIntegrationMicrophoneEnabled(final boolean enabled) { - this.webRTCWrapper.setMicrophoneEnabled(enabled); + // this is called every time we switch audio devices. Thus it would re-enable a microphone + // that was previous disabled by the user. A proper implementation would probably be to + // track user choice and enable the microphone with a userEnabled() && + // callIntegration.isMicrophoneEnabled() condition + Log.d(Config.LOGTAG, "ignoring onCallIntegrationMicrophoneEnabled(" + enabled + ")"); + // this.webRTCWrapper.setMicrophoneEnabled(enabled); } @Override @@ -2827,13 +2836,13 @@ public class JingleRtpConnection extends AbstractJingleConnection private void discoverIceServers(final OnIceServersDiscovered onIceServersDiscovered) { if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) { - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(id.account.getDomain()); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); xmppConnectionService.sendIqPacket( id.account, request, - (account, response) -> { + (response) -> { final var iceServers = IceServers.parse(response); if (iceServers.isEmpty()) { Log.w( diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java index 9a60b3924273c14a4acc235dc5a06f9ff425db75..263905051990a159aba342f173d58aa6a07b65cf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/OnJinglePacketReceived.java @@ -1,9 +1,8 @@ package eu.siacs.conversations.xmpp.jingle; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xmpp.PacketReceived; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +import im.conversations.android.xmpp.model.stanza.Iq; -public interface OnJinglePacketReceived extends PacketReceived { - void onJinglePacketReceived(Account account, JinglePacket packet); +public interface OnJinglePacketReceived { + void onJinglePacketReceived(Account account, Iq packet); } 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 b151af17e02fc837a599433ce568e1b630d35dbf..24e82bc4f6c24003b20413ac8a70bf3a4d0db73b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -18,9 +18,9 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; -import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import im.conversations.android.xmpp.model.jingle.Jingle; import java.util.Collection; import java.util.HashMap; @@ -39,7 +39,7 @@ public class RtpContentMap extends AbstractContentMap> contents = of(jinglePacket.getJingleContents()); if (isOmemoVerified(contents)) { @@ -53,7 +53,7 @@ public class RtpContentMap extends AbstractContentMap> contents) { final Collection> values = contents.values(); - if (values.size() == 0) { + if (values.isEmpty()) { return false; } for (final DescriptionTransport descriptionTransport : diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java index 3878d98d94554a539280bc402cff9b2079df60ba..0553e203e55a2f56b5ea64f57c3c8ce90a5fae68 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/FileTransferDescription.java @@ -15,6 +15,7 @@ import com.google.common.primitives.Longs; import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.jingle.Jingle; import java.util.List; @@ -55,15 +56,11 @@ public class FileTransferDescription extends GenericDescription { return new File(size, name, mediaType, hashes); } - public static SessionInfo getSessionInfo(@NonNull final JinglePacket jinglePacket) { - Preconditions.checkNotNull(jinglePacket); + public static SessionInfo getSessionInfo(@NonNull final Jingle jingle) { + Preconditions.checkNotNull(jingle); Preconditions.checkArgument( - jinglePacket.getAction() == JinglePacket.Action.SESSION_INFO, + jingle.getAction() == Jingle.Action.SESSION_INFO, "jingle packet is not a session-info"); - final Element jingle = jinglePacket.findChild("jingle", Namespace.JINGLE); - if (jingle == null) { - return null; - } final Element checksum = jingle.findChild("checksum", Namespace.JINGLE_APPS_FILE_TRANSFER); if (checksum != null) { final Element file = checksum.findChild("file", Namespace.JINGLE_APPS_FILE_TRANSFER); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java index ce2d4b31f2a61298ca733bcb1fd1abff523fc73e..849b2a404ec13bd75330c5fed894bc3ec6b0ea6c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/InbandBytestreamsTransport.java @@ -16,7 +16,7 @@ import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.stanzas.IbbTransportInfo; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; import java.io.Closeable; import java.io.IOException; @@ -96,7 +96,7 @@ public class InbandBytestreamsTransport implements Transport { } private void openInBandTransport() { - final var iqPacket = new IqPacket(IqPacket.TYPE.SET); + final var iqPacket = new Iq(Iq.Type.SET); iqPacket.setTo(with); final var open = iqPacket.addChild("open", Namespace.IBB); open.setAttribute("block-size", this.blockSize); @@ -106,8 +106,8 @@ public class InbandBytestreamsTransport implements Transport { xmppConnection.sendIqPacket(iqPacket, this::receiveResponseToOpen); } - private void receiveResponseToOpen(final Account account, final IqPacket response) { - if (response.getType() == IqPacket.TYPE.RESULT) { + private void receiveResponseToOpen(final Iq response) { + if (response.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, "ibb open was accepted"); this.transportCallback.onTransportEstablished(); this.blockSenderThread.start(); @@ -284,7 +284,7 @@ public class InbandBytestreamsTransport implements Transport { private void sendIbbBlock(final int sequence, final byte[] block) { Log.d(Config.LOGTAG, "sending ibb block #" + sequence + " " + block.length + " bytes"); - final var iqPacket = new IqPacket(IqPacket.TYPE.SET); + final var iqPacket = new Iq(Iq.Type.SET); iqPacket.setTo(with); final var data = iqPacket.addChild("data", Namespace.IBB); data.setAttribute("sid", this.streamId); @@ -292,8 +292,8 @@ public class InbandBytestreamsTransport implements Transport { data.setContent(BaseEncoding.base64().encode(block)); this.xmppConnection.sendIqPacket( iqPacket, - (a, response) -> { - if (response.getType() != IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() != Iq.Type.RESULT) { Log.d( Config.LOGTAG, "received iq error in response to data block #" + sequence); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java index bbda1c62271125a9d0af2753c724c903aa05d5ec..2925592ea498772ccf62ab48d7b183562f21bf17 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/SocksByteStreamsTransport.java @@ -32,7 +32,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.DirectConnectionUtils; import eu.siacs.conversations.xmpp.jingle.stanzas.SocksByteStreamsTransportInfo; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; import java.io.IOException; import java.io.InputStream; @@ -250,7 +250,7 @@ public class SocksByteStreamsTransport implements Transport { private ListenableFuture activateProxy(final Candidate candidate) { Log.d(Config.LOGTAG, "trying to activate our proxy " + candidate); final SettableFuture iqFuture = SettableFuture.create(); - final IqPacket proxyActivation = new IqPacket(IqPacket.TYPE.SET); + final Iq proxyActivation = new Iq(Iq.Type.SET); proxyActivation.setTo(candidate.jid); final Element query = proxyActivation.addChild("query", Namespace.BYTE_STREAMS); query.setAttribute("sid", this.streamId); @@ -258,17 +258,18 @@ public class SocksByteStreamsTransport implements Transport { activate.setContent(id.with.toEscapedString()); xmppConnection.sendIqPacket( proxyActivation, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { Log.d(Config.LOGTAG, "our proxy has been activated"); transportCallback.onProxyActivated(this.streamId, candidate); iqFuture.set(candidate.cid); - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + } else if (response.getType() == Iq.Type.TIMEOUT) { iqFuture.setException(new TimeoutException()); } else { + final var account = id.account; Log.d( Config.LOGTAG, - a.getJid().asBareJid() + account.getJid().asBareJid() + ": failed to activate proxy on " + candidate.jid); iqFuture.setException(new IllegalStateException("Proxy activation failed")); @@ -314,14 +315,14 @@ public class SocksByteStreamsTransport implements Transport { return Futures.immediateFailedFuture( new IllegalStateException("No proxy/streamer found")); } - final IqPacket iqRequest = new IqPacket(IqPacket.TYPE.GET); + final Iq iqRequest = new Iq(Iq.Type.GET); iqRequest.setTo(streamer); iqRequest.query(Namespace.BYTE_STREAMS); final SettableFuture candidateFuture = SettableFuture.create(); xmppConnection.sendIqPacket( iqRequest, - (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element query = response.findChild("query", Namespace.BYTE_STREAMS); final Element streamHost = query == null @@ -349,7 +350,7 @@ public class SocksByteStreamsTransport implements Transport { 655360 + (initiator ? 0 : 15), CandidateType.PROXY)); - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + } else if (response.getType() == Iq.Type.TIMEOUT) { candidateFuture.setException(new TimeoutException()); } else { candidateFuture.setException( 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 e4dd730d518cd8c3b34ba2134b298b7e65b7daff..904c4aba89568f863bef293db89959cf58e187c7 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 @@ -22,7 +22,7 @@ import eu.siacs.conversations.xmpp.jingle.IceServers; import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; import org.webrtc.CandidatePairChangeEvent; import org.webrtc.DataChannel; @@ -234,14 +234,14 @@ public class WebRTCDataChannelTransport implements Transport { if (xmppConnection.getFeatures().externalServiceDiscovery()) { final SettableFuture> iceServerFuture = SettableFuture.create(); - final IqPacket request = new IqPacket(IqPacket.TYPE.GET); + final Iq request = new Iq(Iq.Type.GET); request.setTo(this.account.getDomain()); request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); xmppConnection.sendIqPacket( request, - (account, response) -> { + (response) -> { final var iceServers = IceServers.parse(response); - if (iceServers.size() == 0) { + if (iceServers.isEmpty()) { Log.w( Config.LOGTAG, account.getJid().asBareJid() 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 ef1da85615c5c6c2db62590981dee5e312b52617..ee3770ead9f3a56322ba8fd62c4660b7f954b701 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -4,7 +4,7 @@ import android.os.Bundle; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.xmpp.model.stanza.Iq; public class PublishOptions { @@ -37,8 +37,8 @@ public class PublishOptions { return options; } - public static boolean preconditionNotMet(IqPacket response) { - final Element error = response.getType() == IqPacket.TYPE.ERROR ? response.findChild("error") : null; + public static boolean preconditionNotMet(Iq response) { + final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; return error != null && error.hasChild("precondition-not-met", Namespace.PUBSUB_ERROR); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java deleted file mode 100644 index 2291a9896110d594f99c5fda4f15210b03975cb6..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractAcknowledgeableStanza.java +++ /dev/null @@ -1,42 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.InvalidJid; - -abstract public class AbstractAcknowledgeableStanza extends AbstractStanza { - - protected AbstractAcknowledgeableStanza(String name) { - super(name); - } - - - public String getId() { - return this.getAttribute("id"); - } - - public void setId(final String id) { - setAttribute("id", id); - } - - private Element getErrorConditionElement() { - final Element error = findChild("error"); - if (error == null) { - return null; - } - for (final Element element : error.getChildren()) { - if (!element.getName().equals("text")) { - return element; - } - } - return null; - } - - public String getErrorCondition() { - final Element condition = getErrorConditionElement(); - return condition == null ? null : condition.getName(); - } - - public boolean valid() { - return InvalidJid.isValid(getFrom()) && InvalidJid.isValid(getTo()); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java deleted file mode 100644 index c0b3d07bdd46afd9d939303c2a142a43894bd56a..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java +++ /dev/null @@ -1,53 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.Jid; - -public class AbstractStanza extends Element { - - protected AbstractStanza(final String name) { - super(name); - } - - public Jid getTo() { - return getAttributeAsJid("to"); - } - - public Jid getFrom() { - return getAttributeAsJid("from"); - } - - public void setTo(final Jid to) { - if (to != null) { - setAttribute("to", to); - } - } - - public void setFrom(final Jid from) { - if (from != null) { - setAttribute("from", from); - } - } - - public boolean fromServer(final Account account) { - final Jid from = getFrom(); - return from == null - || from.equals(account.getDomain()) - || from.equals(account.getJid().asBareJid()) - || from.equals(account.getJid()); - } - - public boolean toServer(final Account account) { - final Jid to = getTo(); - return to == null - || to.equals(account.getDomain()) - || to.equals(account.getJid().asBareJid()) - || to.equals(account.getJid()); - } - - public boolean fromAccount(final Account account) { - final Jid from = getFrom(); - return from != null && from.asBareJid().equals(account.getJid().asBareJid()); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java deleted file mode 100644 index ba8588e1f552d5ab58387eed08929dd8a300d561..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ /dev/null @@ -1,75 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -import eu.siacs.conversations.xml.Element; - -public class IqPacket extends AbstractAcknowledgeableStanza { - - public enum TYPE { - ERROR, - SET, - RESULT, - GET, - INVALID, - TIMEOUT - } - - public IqPacket(final TYPE type) { - super("iq"); - if (type != TYPE.INVALID) { - this.setAttribute("type", type.toString().toLowerCase()); - } - } - - public IqPacket() { - super("iq"); - } - - public Element query() { - Element query = findChild("query"); - if (query == null) { - query = addChild("query"); - } - return query; - } - - public Element query(final String xmlns) { - final Element query = query(); - query.setAttribute("xmlns", xmlns); - return query(); - } - - public TYPE getType() { - final String type = getAttribute("type"); - if (type == null) { - return TYPE.INVALID; - } - switch (type) { - case "error": - return TYPE.ERROR; - case "result": - return TYPE.RESULT; - case "set": - return TYPE.SET; - case "get": - return TYPE.GET; - case "timeout": - return TYPE.TIMEOUT; - default: - return TYPE.INVALID; - } - } - - public IqPacket generateResponse(final TYPE type) { - final IqPacket packet = new IqPacket(type); - packet.setTo(this.getFrom()); - packet.setId(this.getId()); - return packet; - } - - @Override - public boolean valid() { - String id = getId(); - return id != null && super.valid(); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java deleted file mode 100644 index bac76adfc7f419c4f5bf10c953d43743772888be..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ /dev/null @@ -1,98 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -import android.util.Pair; - -import eu.siacs.conversations.parser.AbstractParser; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.LocalizedContent; - -public class MessagePacket extends AbstractAcknowledgeableStanza { - public static final int TYPE_CHAT = 0; - public static final int TYPE_NORMAL = 2; - public static final int TYPE_GROUPCHAT = 3; - public static final int TYPE_ERROR = 4; - public static final int TYPE_HEADLINE = 5; - - public MessagePacket() { - super("message"); - } - - public LocalizedContent getBody() { - return findInternationalizedChildContentInDefaultNamespace("body"); - } - - public void setBody(String text) { - removeChild(findChild("body")); - prependChild(new Element("body").setContent(text)); - } - - public void setAxolotlMessage(Element axolotlMessage) { - removeChild(findChild("body")); - prependChild(axolotlMessage); - } - - public void setType(int type) { - switch (type) { - case TYPE_CHAT: - this.setAttribute("type", "chat"); - break; - case TYPE_GROUPCHAT: - this.setAttribute("type", "groupchat"); - break; - case TYPE_NORMAL: - break; - case TYPE_ERROR: - this.setAttribute("type","error"); - break; - default: - this.setAttribute("type", "chat"); - break; - } - } - - public int getType() { - String type = getAttribute("type"); - if (type == null) { - return TYPE_NORMAL; - } else if (type.equals("normal")) { - return TYPE_NORMAL; - } else if (type.equals("chat")) { - return TYPE_CHAT; - } else if (type.equals("groupchat")) { - return TYPE_GROUPCHAT; - } else if (type.equals("error")) { - return TYPE_ERROR; - } else if (type.equals("headline")) { - return TYPE_HEADLINE; - } else { - return TYPE_NORMAL; - } - } - - public Pair getForwardedMessagePacket(String name, String namespace) { - Element wrapper = findChild(name, namespace); - if (wrapper == null) { - return null; - } - Element forwarded = wrapper.findChild("forwarded", "urn:xmpp:forward:0"); - if (forwarded == null) { - return null; - } - MessagePacket packet = create(forwarded.findChild("message")); - if (packet == null) { - return null; - } - Long timestamp = AbstractParser.parseTimestamp(forwarded, null); - return new Pair(packet,timestamp); - } - - public static MessagePacket create(Element element) { - if (element == null) { - return null; - } - MessagePacket packet = new MessagePacket(); - packet.setAttributes(element.getAttributes()); - packet.setChildren(element.getChildren()); - return packet; - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java deleted file mode 100644 index c321498d86cd8bd3a7d38d8f76df7090c9212d85..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/PresencePacket.java +++ /dev/null @@ -1,8 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas; - -public class PresencePacket extends AbstractAcknowledgeableStanza { - - public PresencePacket() { - super("presence"); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java deleted file mode 100644 index e1c465f726b44c02fc28648ed893fc1b45a50ae2..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/ActivePacket.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.csi; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public class ActivePacket extends AbstractStanza { - public ActivePacket() { - super("active"); - setAttribute("xmlns", Namespace.CSI); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java deleted file mode 100644 index 1b74de066d17eb5a806506a65c3fff8addcd49e7..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/csi/InactivePacket.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.csi; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public class InactivePacket extends AbstractStanza { - public InactivePacket() { - super("inactive"); - setAttribute("xmlns", Namespace.CSI); - } -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java deleted file mode 100644 index 9e7b991a4b1c29e6399f6771398c635680cc016a..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/AckPacket.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.streammgmt; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public class AckPacket extends AbstractStanza { - - public AckPacket(final int sequence) { - super("a"); - this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); - this.setAttribute("h", Integer.toString(sequence)); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java deleted file mode 100644 index 95558b143230ba3e02e87544089c52c2b030581a..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/EnablePacket.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.streammgmt; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public class EnablePacket extends AbstractStanza { - - public EnablePacket() { - super("enable"); - this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); - this.setAttribute("resume", "true"); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java deleted file mode 100644 index 4e0e0f11aa192f97bd461fffcb7d762477d90e9d..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/RequestPacket.java +++ /dev/null @@ -1,13 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.streammgmt; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public class RequestPacket extends AbstractStanza { - - public RequestPacket() { - super("r"); - this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java deleted file mode 100644 index 38681d7c1a370957ad4a251d310279c71b387ac8..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/streammgmt/ResumePacket.java +++ /dev/null @@ -1,15 +0,0 @@ -package eu.siacs.conversations.xmpp.stanzas.streammgmt; - -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.stanzas.AbstractStanza; - -public class ResumePacket extends AbstractStanza { - - public ResumePacket(final String id, final int sequence) { - super("resume"); - this.setAttribute("xmlns", Namespace.STREAM_MANAGEMENT); - this.setAttribute("previd", id); - this.setAttribute("h", Integer.toString(sequence)); - } - -} diff --git a/src/main/java/im/conversations/android/xmpp/Entity.java b/src/main/java/im/conversations/android/xmpp/Entity.java new file mode 100644 index 0000000000000000000000000000000000000000..a578d250780e40c8b5a588a74f1ea43496f2c515 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Entity.java @@ -0,0 +1,34 @@ +package im.conversations.android.xmpp; + +import org.jxmpp.jid.Jid; + +public abstract class Entity { + + public final Jid address; + + private Entity(final Jid address) { + this.address = address; + } + + public static class DiscoItem extends Entity { + + private DiscoItem(Jid address) { + super(address); + } + } + + public static class Presence extends Entity { + + private Presence(Jid address) { + super(address); + } + } + + public static Presence presence(final Jid address) { + return new Presence(address); + } + + public static DiscoItem discoItem(final Jid address) { + return new DiscoItem(address); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..b282c07910706508ee0ff0a8b1ee70aa6699e400 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities.java @@ -0,0 +1,133 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Ordering; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public final class EntityCapabilities { + public static EntityCapsHash hash(final InfoQuery info) { + final StringBuilder s = new StringBuilder(); + final List orderedIdentities = + Ordering.from( + (Comparator) + (a, b) -> + ComparisonChain.start() + .compare( + blankNull(a.getCategory()), + blankNull(b.getCategory())) + .compare( + blankNull(a.getType()), + blankNull(b.getType())) + .compare( + blankNull(a.getLang()), + blankNull(b.getLang())) + .compare( + blankNull(a.getIdentityName()), + blankNull(b.getIdentityName())) + .result()) + .sortedCopy(info.getIdentities()); + + for (final Identity id : orderedIdentities) { + s.append(blankNull(id.getCategory())) + .append("/") + .append(blankNull(id.getType())) + .append("/") + .append(blankNull(id.getLang())) + .append("/") + .append(blankNull(id.getIdentityName())) + .append("<"); + } + + final List features = + Ordering.natural() + .sortedCopy(Collections2.transform(info.getFeatures(), Feature::getVar)); + for (final String feature : features) { + s.append(clean(feature)).append("<"); + } + + final List extensions = + Ordering.from(Comparator.comparing(Data::getFormType)) + .sortedCopy(info.getExtensions(Data.class)); + + for (final Data extension : extensions) { + s.append(clean(extension.getFormType())).append("<"); + final List fields = + Ordering.from( + Comparator.comparing( + (Field lhs) -> Strings.nullToEmpty(lhs.getFieldName()))) + .sortedCopy(extension.getFields()); + for (final Field field : fields) { + s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); + final List values = Ordering.natural().sortedCopy(field.getValues()); + for (final String value : values) { + s.append(blankNull(value)).append("<"); + } + } + } + return new EntityCapsHash( + Hashing.sha1().hashString(s.toString(), StandardCharsets.UTF_8).asBytes()); + } + + private static String clean(String s) { + return s.replace("<", "<"); + } + + private static String blankNull(String s) { + return s == null ? "" : clean(s); + } + + public abstract static class Hash { + public final byte[] hash; + + protected Hash(byte[] hash) { + this.hash = hash; + } + + public String encoded() { + return BaseEncoding.base64().encode(hash); + } + + public abstract String capabilityNode(final String node); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Hash hash1 = (Hash) o; + return Arrays.equals(hash, hash1.hash); + } + + @Override + public int hashCode() { + return Arrays.hashCode(hash); + } + } + + public static class EntityCapsHash extends Hash { + + protected EntityCapsHash(byte[] hash) { + super(hash); + } + + @Override + public String capabilityNode(String node) { + return String.format("%s#%s", node, encoded()); + } + + public static EntityCapsHash of(final String encoded) { + return new EntityCapsHash(BaseEncoding.base64().decode(encoded)); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java new file mode 100644 index 0000000000000000000000000000000000000000..1d8a35a68d2d5822c2d0ada756b91e473bb698e9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -0,0 +1,185 @@ +package im.conversations.android.xmpp; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.Ordering; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.Bytes; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.Hash; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.data.Value; +import im.conversations.android.xmpp.model.disco.info.Feature; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Objects; + +public class EntityCapabilities2 { + + private static final char UNIT_SEPARATOR = 0x1f; + private static final char RECORD_SEPARATOR = 0x1e; + + private static final char GROUP_SEPARATOR = 0x1d; + + private static final char FILE_SEPARATOR = 0x1c; + + public static EntityCaps2Hash hash(final InfoQuery info) { + return hash(Hash.Algorithm.SHA_256, info); + } + + public static EntityCaps2Hash hash(final Hash.Algorithm algorithm, final InfoQuery info) { + final String result = algorithm(info); + final var hashFunction = toHashFunction(algorithm); + return new EntityCaps2Hash( + algorithm, hashFunction.hashString(result, StandardCharsets.UTF_8).asBytes()); + } + + private static HashFunction toHashFunction(final Hash.Algorithm algorithm) { + switch (algorithm) { + case SHA_1: + return Hashing.sha1(); + case SHA_256: + return Hashing.sha256(); + case SHA_512: + return Hashing.sha512(); + default: + throw new IllegalArgumentException("Unknown hash algorithm"); + } + } + + private static String asHex(final String message) { + return Joiner.on(' ') + .join( + Collections2.transform( + Bytes.asList(message.getBytes(StandardCharsets.UTF_8)), + b -> String.format("%02x", b))); + } + + private static String algorithm(final InfoQuery infoQuery) { + return features(infoQuery.getFeatures()) + + identities(infoQuery.getIdentities()) + + extensions(infoQuery.getExtensions(Data.class)); + } + + private static String identities(final Collection identities) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + identities, EntityCapabilities2::identity))) + + FILE_SEPARATOR; + } + + private static String identity(final Identity identity) { + return Strings.nullToEmpty(identity.getCategory()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getType()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getLang()) + + UNIT_SEPARATOR + + Strings.nullToEmpty(identity.getIdentityName()) + + UNIT_SEPARATOR + + RECORD_SEPARATOR; + } + + private static String features(Collection features) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + features, EntityCapabilities2::feature))) + + FILE_SEPARATOR; + } + + private static String feature(final Feature feature) { + return Strings.nullToEmpty(feature.getVar()) + UNIT_SEPARATOR; + } + + private static String value(final Value value) { + return Strings.nullToEmpty(value.getContent()) + UNIT_SEPARATOR; + } + + private static String values(final Collection values) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + values, EntityCapabilities2::value))); + } + + private static String field(final Field field) { + return Strings.nullToEmpty(field.getFieldName()) + + UNIT_SEPARATOR + + values(field.getExtensions(Value.class)) + + RECORD_SEPARATOR; + } + + private static String fields(final Collection fields) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + fields, EntityCapabilities2::field))) + + GROUP_SEPARATOR; + } + + private static String extension(final Data data) { + return fields(data.getExtensions(Field.class)); + } + + private static String extensions(final Collection extensions) { + return Joiner.on("") + .join( + Ordering.natural() + .sortedCopy( + Collections2.transform( + extensions, + EntityCapabilities2::extension))) + + FILE_SEPARATOR; + } + + public static class EntityCaps2Hash extends EntityCapabilities.Hash { + + public final Hash.Algorithm algorithm; + + protected EntityCaps2Hash(final Hash.Algorithm algorithm, byte[] hash) { + super(hash); + this.algorithm = algorithm; + } + + public static EntityCaps2Hash of(final Hash.Algorithm algorithm, final String encoded) { + return new EntityCaps2Hash(algorithm, BaseEncoding.base64().decode(encoded)); + } + + @Override + public String capabilityNode(String node) { + return String.format( + "%s#%s.%s", Namespace.ENTITY_CAPABILITIES_2, algorithm.toString(), encoded()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + EntityCaps2Hash that = (EntityCaps2Hash) o; + return algorithm == that.algorithm; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), algorithm); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java b/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..04352b559312c5930456d4729783f49e691d4bf9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/ExtensionFactory.java @@ -0,0 +1,78 @@ +package im.conversations.android.xmpp; + + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.xmpp.model.Extension; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +public final class ExtensionFactory { + + public static Element create(final String name, final String namespace) { + final Class clazz = of(name, namespace); + if (clazz == null) { + return new Element(name, namespace); + } + final Constructor constructor; + try { + constructor = clazz.getDeclaredConstructor(); + } catch (final NoSuchMethodException e) { + throw new IllegalStateException( + String.format("%s has no default constructor", clazz.getName()),e); + } + try { + return constructor.newInstance(); + } catch (final IllegalAccessException + | InstantiationException + | InvocationTargetException e) { + throw new IllegalStateException( + String.format("%s has inaccessible default constructor", clazz.getName()),e); + } + } + + private static Class of(final String name, final String namespace) { + return Extensions.EXTENSION_CLASS_MAP.get(new Id(name, namespace)); + } + + public static Id id(final Class clazz) { + return Extensions.EXTENSION_CLASS_MAP.inverse().get(clazz); + } + + private ExtensionFactory() {} + + public static class Id { + public final String name; + public final String namespace; + + public Id(String name, String namespace) { + this.name = name; + this.namespace = namespace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Id id = (Id) o; + return Objects.equal(name, id.name) && Objects.equal(namespace, id.namespace); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, namespace); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("namespace", namespace) + .toString(); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..81a55f18c5a80b7acf2a90ccda9661169ff0e05f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/NodeConfiguration.java @@ -0,0 +1,112 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.collect.ImmutableMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class NodeConfiguration implements Map { + + private static final String PERSIST_ITEMS = "pubsub#persist_items"; + private static final String ACCESS_MODEL = "pubsub#access_model"; + private static final String SEND_LAST_PUBLISHED_ITEM = "pubsub#send_last_published_item"; + private static final String MAX_ITEMS = "pubsub#max_items"; + private static final String NOTIFY_DELETE = "pubsub#notify_delete"; + private static final String NOTIFY_RETRACT = "pubsub#notify_retract"; + + public static final NodeConfiguration OPEN = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "open") + .build()); + public static final NodeConfiguration PRESENCE = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "presence") + .build()); + public static final NodeConfiguration WHITELIST_MAX_ITEMS = + new NodeConfiguration( + new ImmutableMap.Builder() + .put(PERSIST_ITEMS, Boolean.TRUE) + .put(ACCESS_MODEL, "whitelist") + .put(SEND_LAST_PUBLISHED_ITEM, "never") + .put(MAX_ITEMS, "max") + .put(NOTIFY_DELETE, Boolean.TRUE) + .put(NOTIFY_RETRACT, Boolean.TRUE) + .build()); + private final Map delegate; + + private NodeConfiguration(Map map) { + this.delegate = map; + } + + @Override + public int size() { + return this.delegate.size(); + } + + @Override + public boolean isEmpty() { + return this.delegate.isEmpty(); + } + + @Override + public boolean containsKey(@Nullable Object o) { + return this.delegate.containsKey(o); + } + + @Override + public boolean containsValue(@Nullable Object o) { + return this.delegate.containsValue(o); + } + + @Nullable + @Override + public Object get(@Nullable Object o) { + return this.delegate.get(o); + } + + @Nullable + @Override + public Object put(String s, Object o) { + return this.delegate.put(s, o); + } + + @Nullable + @Override + public Object remove(@Nullable Object o) { + return this.delegate.remove(o); + } + + @Override + public void putAll(@NonNull Map map) { + this.delegate.putAll(map); + } + + @Override + public void clear() { + this.delegate.clear(); + } + + @NonNull + @Override + public Set keySet() { + return this.delegate.keySet(); + } + + @NonNull + @Override + public Collection values() { + return this.delegate.values(); + } + + @NonNull + @Override + public Set> entrySet() { + return this.delegate.entrySet(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Page.java b/src/main/java/im/conversations/android/xmpp/Page.java new file mode 100644 index 0000000000000000000000000000000000000000..21aa219a44731d091955bf4791fbdffa70fee2f5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Page.java @@ -0,0 +1,31 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; + +public class Page { + + public final String first; + public final String last; + public final Integer count; + + public Page(String first, String last, Integer count) { + this.first = first; + this.last = last; + this.count = count; + } + + public static Page emptyWithCount(final String id, final Integer count) { + return new Page(id, id, count); + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("first", first) + .add("last", last) + .add("count", count) + .toString(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Range.java b/src/main/java/im/conversations/android/xmpp/Range.java new file mode 100644 index 0000000000000000000000000000000000000000..8aff5094e91d54fbde845cefc481c0156fbca3b1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Range.java @@ -0,0 +1,40 @@ +package im.conversations.android.xmpp; + +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; + +public class Range { + + public final Order order; + public final String id; + + public Range(final Order order, final String id) { + this.order = order; + this.id = id; + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("order", order).add("id", id).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Range range = (Range) o; + return order == range.order && Objects.equal(id, range.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(order, id); + } + + public enum Order { + NORMAL, + REVERSE + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Timestamps.java b/src/main/java/im/conversations/android/xmpp/Timestamps.java new file mode 100644 index 0000000000000000000000000000000000000000..0135901abf1dca3b2a8292c9bb8b0d538bb5ab13 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/Timestamps.java @@ -0,0 +1,44 @@ +package im.conversations.android.xmpp; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public final class Timestamps { + + private Timestamps() { + throw new IllegalStateException("Do not instantiate me"); + } + + public static long parse(final String input) throws ParseException { + if (input == null) { + throw new IllegalArgumentException("timestamp should not be null"); + } + final String timestamp = input.replace("Z", "+0000"); + final SimpleDateFormat simpleDateFormat = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); + final long milliseconds = getMilliseconds(timestamp); + final String formatted = + timestamp.substring(0, 19) + timestamp.substring(timestamp.length() - 5); + final Date date = simpleDateFormat.parse(formatted); + if (date == null) { + throw new IllegalArgumentException("Date was null"); + } + return date.getTime() + milliseconds; + } + + private static long getMilliseconds(final String timestamp) { + if (timestamp.length() >= 25 && timestamp.charAt(19) == '.') { + final String millis = timestamp.substring(19, timestamp.length() - 5); + try { + double fractions = Double.parseDouble("0" + millis); + return Math.round(1000 * fractions); + } catch (final NumberFormatException e) { + return 0; + } + } else { + return 0; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/AuthenticationFailure.java b/src/main/java/im/conversations/android/xmpp/model/AuthenticationFailure.java new file mode 100644 index 0000000000000000000000000000000000000000..7d6790a6449d08e579f36197ce021f9be3eda54b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/AuthenticationFailure.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model; + +import im.conversations.android.xmpp.model.sasl.SaslError; + +public abstract class AuthenticationFailure extends StreamElement { + + protected AuthenticationFailure(Class clazz) { + super(clazz); + } + + public SaslError getErrorCondition() { + return this.getExtension(SaslError.class); + } + + public String getText() { + return this.findChildContent("text"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java b/src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..b2122ab863637c257b1b957b6e200c2f27aa5f5d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/AuthenticationRequest.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model; + +import eu.siacs.conversations.crypto.sasl.SaslMechanism; + +public abstract class AuthenticationRequest extends StreamElement{ + + + protected AuthenticationRequest(Class clazz) { + super(clazz); + } + + public abstract void setMechanism(final SaslMechanism mechanism); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java b/src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java new file mode 100644 index 0000000000000000000000000000000000000000..3b9a03761125dd451ea7ce4a96d66a979f2440d8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/AuthenticationStreamFeature.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model; + +import java.util.Collection; + +public abstract class AuthenticationStreamFeature extends StreamFeature{ + + public AuthenticationStreamFeature(final Class clazz) { + super(clazz); + } + + public abstract Collection getMechanismNames(); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ByteContent.java b/src/main/java/im/conversations/android/xmpp/model/ByteContent.java new file mode 100644 index 0000000000000000000000000000000000000000..0ca6212ff75a2deadcb807c049c4c7917bbc5abe --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ByteContent.java @@ -0,0 +1,33 @@ +package im.conversations.android.xmpp.model; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; + +import eu.siacs.conversations.xml.Element; + +public interface ByteContent { + + String getContent(); + + default byte[] asBytes() { + final var content = this.getContent(); + if (Strings.isNullOrEmpty(content)) { + throw new IllegalStateException( + String.format("%s element is lacking content", getClass().getName())); + } + final var contentCleaned = CharMatcher.whitespace().removeFrom(content); + if (BaseEncoding.base64().canDecode(contentCleaned)) { + return BaseEncoding.base64().decode(contentCleaned); + } else { + throw new IllegalStateException( + String.format("%s element contains invalid base64", getClass().getName())); + } + } + + default void setContent(final byte[] bytes) { + setContent(BaseEncoding.base64().encode(bytes)); + } + + Element setContent(final String content); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java new file mode 100644 index 0000000000000000000000000000000000000000..00e2b652aa9e1970191a9f1e053c9ed6f2e4b8a4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceipt.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model; + +public abstract class DeliveryReceipt extends Extension { + + protected DeliveryReceipt(Class clazz) { + super(clazz); + } + + public abstract String getId(); +} diff --git a/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..a5a7533bbc23902494dc345f9b0146bb29e2f4f1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/DeliveryReceiptRequest.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model; + +public abstract class DeliveryReceiptRequest extends Extension { + + protected DeliveryReceiptRequest(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/Extension.java b/src/main/java/im/conversations/android/xmpp/model/Extension.java new file mode 100644 index 0000000000000000000000000000000000000000..0d4c50eef3b4a8626ee6f1eaf2d619978f373f78 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/Extension.java @@ -0,0 +1,62 @@ +package im.conversations.android.xmpp.model; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.xmpp.ExtensionFactory; + +import java.util.Collection; + +public class Extension extends Element { + + private Extension(final ExtensionFactory.Id id) { + super(id.name, id.namespace); + } + + public Extension(final Class clazz) { + this( + Preconditions.checkNotNull( + ExtensionFactory.id(clazz), + String.format( + "%s does not seem to be annotated with @XmlElement", + clazz.getName()))); + Preconditions.checkArgument( + getClass().equals(clazz), "clazz passed in constructor must match class"); + } + + public boolean hasExtension(final Class clazz) { + return Iterables.any(getChildren(), clazz::isInstance); + } + + public E getExtension(final Class clazz) { + final var extension = Iterables.find(getChildren(), clazz::isInstance, null); + if (extension == null) { + return null; + } + return clazz.cast(extension); + } + + public Collection getExtensions(final Class clazz) { + return Collections2.transform( + Collections2.filter(getChildren(), clazz::isInstance), clazz::cast); + } + + public Collection getExtensionIds() { + return Collections2.transform( + getChildren(), c -> new ExtensionFactory.Id(c.getName(), c.getNamespace())); + } + + public T addExtension(T child) { + this.addChild(child); + return child; + } + + public void addExtensions(final Collection extensions) { + for (final Extension extension : extensions) { + addExtension(extension); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/Hash.java b/src/main/java/im/conversations/android/xmpp/model/Hash.java new file mode 100644 index 0000000000000000000000000000000000000000..8c41add8d9eefd69ddf246534e66b7bd74256769 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/Hash.java @@ -0,0 +1,46 @@ +package im.conversations.android.xmpp.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.common.base.CaseFormat; +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; + +@XmlElement(namespace = Namespace.HASHES) +public class Hash extends Extension { + public Hash() { + super(Hash.class); + } + + public Algorithm getAlgorithm() { + return Algorithm.tryParse(this.getAttribute("algo")); + } + + public void setAlgorithm(final Algorithm algorithm) { + this.setAttribute("algo", algorithm.toString()); + } + + public enum Algorithm { + SHA_1, + SHA_256, + SHA_512; + + public static Algorithm tryParse(@Nullable final String name) { + try { + return valueOf( + CaseFormat.LOWER_HYPHEN.to( + CaseFormat.UPPER_UNDERSCORE, Strings.nullToEmpty(name))); + } catch (final IllegalArgumentException e) { + return null; + } + } + + @NonNull + @Override + public String toString() { + return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN, super.toString()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/StreamElement.java b/src/main/java/im/conversations/android/xmpp/model/StreamElement.java new file mode 100644 index 0000000000000000000000000000000000000000..ca5fd0053b0839a479d5fddedc999b64b09f764f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/StreamElement.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model; + +public abstract class StreamElement extends Extension { + + protected StreamElement(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/StreamFeature.java b/src/main/java/im/conversations/android/xmpp/model/StreamFeature.java new file mode 100644 index 0000000000000000000000000000000000000000..eadd8d8c4407e9b175ab8e5359e64cd5d505dbd5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/StreamFeature.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model; + +public abstract class StreamFeature extends Extension{ + + public StreamFeature(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/addressing/Address.java b/src/main/java/im/conversations/android/xmpp/model/addressing/Address.java new file mode 100644 index 0000000000000000000000000000000000000000..f812ec53beac1689a78daf6fd10f5127c2d1f9e1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/addressing/Address.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.addressing; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Address extends Extension { + public Address() { + super(Address.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java b/src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java new file mode 100644 index 0000000000000000000000000000000000000000..3ecafc53095f671f5b4de839d2e58b4074c0e3e9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/addressing/Addresses.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.addressing; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Addresses extends Extension { + public Addresses() { + super(Addresses.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java b/src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..6c504489e2943155c7d2a23cb19354beefea692f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/addressing/package-info.java @@ -0,0 +1,6 @@ +@XmlPackage(namespace = Namespace.ADDRESSING) +package im.conversations.android.xmpp.model.addressing; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java new file mode 100644 index 0000000000000000000000000000000000000000..b661bca3a329475738cf5167871380828e6f7825 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Data.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.avatar; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.AVATAR_DATA) +public class Data extends Extension implements ByteContent { + + public Data() { + super(Data.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java new file mode 100644 index 0000000000000000000000000000000000000000..f544af72fc784e0b935bbac0c767bd3504194807 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Info.java @@ -0,0 +1,37 @@ +package im.conversations.android.xmpp.model.avatar; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.AVATAR_METADATA) +public class Info extends Extension { + + public Info() { + super(Info.class); + } + + public long getHeight() { + return this.getLongAttribute("height"); + } + + public long getWidth() { + return this.getLongAttribute("width"); + } + + public long getBytes() { + return this.getLongAttribute("bytes"); + } + + public String getType() { + return this.getAttribute("type"); + } + + public String getUrl() { + return this.getAttribute("url"); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java b/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java new file mode 100644 index 0000000000000000000000000000000000000000..400f989572c2813aaeacdce5863e012ba128af3e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/avatar/Metadata.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.avatar; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.AVATAR_METADATA) +public class Metadata extends Extension { + + public Metadata() { + super(Metadata.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java new file mode 100644 index 0000000000000000000000000000000000000000..2321c2e49b6ebed7b8bb2b3a4bce3f9e41fb459a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Bundle.java @@ -0,0 +1,60 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.state.PreKeyRecord; + +@XmlElement +public class Bundle extends Extension { + + public Bundle() { + super(Bundle.class); + } + + public SignedPreKey getSignedPreKey() { + return this.getExtension(SignedPreKey.class); + } + + public SignedPreKeySignature getSignedPreKeySignature() { + return this.getExtension(SignedPreKeySignature.class); + } + + public IdentityKey getIdentityKey() { + return this.getExtension(IdentityKey.class); + } + + public PreKey getRandomPreKey() { + final var preKeys = this.getExtension(PreKeys.class); + final Collection preKeyList = + preKeys == null ? Collections.emptyList() : preKeys.getExtensions(PreKey.class); + return Iterables.get(preKeyList, (int) (preKeyList.size() * Math.random()), null); + } + + public void setIdentityKey(final ECPublicKey ecPublicKey) { + final var identityKey = this.addExtension(new IdentityKey()); + identityKey.setContent(ecPublicKey); + } + + public void setSignedPreKey( + final int id, final ECPublicKey ecPublicKey, final byte[] signature) { + final var signedPreKey = this.addExtension(new SignedPreKey()); + signedPreKey.setId(id); + signedPreKey.setContent(ecPublicKey); + final var signedPreKeySignature = this.addExtension(new SignedPreKeySignature()); + signedPreKeySignature.setContent(signature); + } + + public void addPreKeys(final List preKeyRecords) { + final var preKeys = this.addExtension(new PreKeys()); + for (final PreKeyRecord preKeyRecord : preKeyRecords) { + final var preKey = preKeys.addExtension(new PreKey()); + preKey.setId(preKeyRecord.getId()); + preKey.setContent(preKeyRecord.getKeyPair().getPublicKey()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java new file mode 100644 index 0000000000000000000000000000000000000000..0ad10d7025e912b3b9d88952e64000ce7db4c46f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Device.java @@ -0,0 +1,22 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Device extends Extension { + + public Device() { + super(Device.class); + } + + public Integer getDeviceId() { + return Ints.tryParse(Strings.nullToEmpty(this.getAttribute("id"))); + } + + public void setDeviceId(int deviceId) { + this.setAttribute("id", deviceId); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java new file mode 100644 index 0000000000000000000000000000000000000000..ec4fce469437793b0a7d63d129d12fb3f45cd1a1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/DeviceList.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +@XmlElement(name = "list") +public class DeviceList extends Extension { + + public DeviceList() { + super(DeviceList.class); + } + + public Collection getDevices() { + return this.getExtensions(Device.class); + } + + public Set getDeviceIds() { + return ImmutableSet.copyOf( + Collections2.filter( + Collections2.transform(getDevices(), Device::getDeviceId), + Objects::nonNull)); + } + + public void setDeviceIds(Collection deviceIds) { + for (final Integer deviceId : deviceIds) { + final var device = this.addExtension(new Device()); + device.setDeviceId(deviceId); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java new file mode 100644 index 0000000000000000000000000000000000000000..2008fb017d2a9cdba76c7756ea6187a2c15fd1ec --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/ECPublicKeyContent.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.xmpp.model.ByteContent; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; + +public interface ECPublicKeyContent extends ByteContent { + + default ECPublicKey asECPublicKey() { + try { + return Curve.decodePoint(asBytes(), 0); + } catch (InvalidKeyException e) { + throw new IllegalStateException( + String.format("%s does not contain a valid ECPublicKey", getClass().getName()), + e); + } + } + + default void setContent(final ECPublicKey ecPublicKey) { + setContent(ecPublicKey.serialize()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java new file mode 100644 index 0000000000000000000000000000000000000000..1a98068ab73cedbef8f385cae450cfaf37ad6f64 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Encrypted.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Encrypted extends Extension { + + public Encrypted() { + super(Encrypted.class); + } + + public boolean hasPayload() { + return hasExtension(Payload.class); + } + + public Header getHeader() { + return getExtension(Header.class); + } + + public Payload getPayload() { + return getExtension(Payload.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java new file mode 100644 index 0000000000000000000000000000000000000000..91e2bd87ba6d44f152d8ee90d3eaafc639b0a369 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Header.java @@ -0,0 +1,45 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.base.Optional; +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Header extends Extension { + + public Header() { + super(Header.class); + } + + public void addIv(byte[] iv) { + this.addExtension(new IV()).setContent(iv); + } + + public void setSourceDevice(long sourceDeviceId) { + this.setAttribute("sid", sourceDeviceId); + } + + public Optional getSourceDevice() { + return getOptionalIntAttribute("sid"); + } + + public Collection getKeys() { + return this.getExtensions(Key.class); + } + + public Key getKey(final int deviceId) { + return Iterables.find( + getKeys(), key -> Objects.equals(key.getRemoteDeviceId(), deviceId), null); + } + + public byte[] getIv() { + final IV iv = this.getExtension(IV.class); + if (iv == null) { + throw new IllegalStateException("No IV in header"); + } + return iv.asBytes(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java new file mode 100644 index 0000000000000000000000000000000000000000..22164976a92c84dfbcd9a2a12c9161ce5c5f998a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/IV.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "iv") +public class IV extends Extension implements ByteContent { + + public IV() { + super(IV.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java new file mode 100644 index 0000000000000000000000000000000000000000..f48fcbd7cb3bd3c0aa6a7992c2c6b7ebe0ba959f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/IdentityKey.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "identityKey") +public class IdentityKey extends Extension implements ECPublicKeyContent { + + public IdentityKey() { + super(IdentityKey.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java new file mode 100644 index 0000000000000000000000000000000000000000..3ad7357b8dd934303b42d71e5b37f941e95abbca --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Key.java @@ -0,0 +1,29 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Key extends Extension implements ByteContent { + + public Key() { + super(Key.class); + } + + public void setIsPreKey(boolean isPreKey) { + this.setAttribute("prekey", isPreKey); + } + + public boolean isPreKey() { + return this.getAttributeAsBoolean("prekey"); + } + + public void setRemoteDeviceId(final int remoteDeviceId) { + this.setAttribute("rid", remoteDeviceId); + } + + public Integer getRemoteDeviceId() { + return getOptionalIntAttribute("rid").orNull(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java new file mode 100644 index 0000000000000000000000000000000000000000..9c58701100005988ebd2684d5e0475fdb1d0dcf0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/Payload.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Payload extends Extension implements ByteContent { + + public Payload() { + super(Payload.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java new file mode 100644 index 0000000000000000000000000000000000000000..a7d39c1daf82b541387ee867ffb29c57e0cdcaf6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKey.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "preKeyPublic") +public class PreKey extends Extension implements ECPublicKeyContent { + + public PreKey() { + super(PreKey.class); + } + + public int getId() { + return Ints.saturatedCast(this.getLongAttribute("preKeyId")); + } + + public void setId(int id) { + this.setAttribute("preKeyId", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java new file mode 100644 index 0000000000000000000000000000000000000000..3613b8aa8a9ade6129bbeaa9462314246d282e5d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/PreKeys.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "prekeys") +public class PreKeys extends Extension { + + public PreKeys() { + super(PreKeys.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java new file mode 100644 index 0000000000000000000000000000000000000000..0e0ca728231e71a4f4dd9df3227a6fed537a59db --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKey.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.axolotl; + +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "signedPreKeyPublic") +public class SignedPreKey extends Extension implements ECPublicKeyContent { + + public SignedPreKey() { + super(SignedPreKey.class); + } + + public int getId() { + return Ints.saturatedCast(this.getLongAttribute("signedPreKeyId")); + } + + public void setId(final int id) { + this.setAttribute("signedPreKeyId", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java new file mode 100644 index 0000000000000000000000000000000000000000..5051cb1b14fc3c9e996cf7bd6cbe966eba130fee --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/SignedPreKeySignature.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "signedPreKeySignature") +public class SignedPreKeySignature extends Extension implements ByteContent { + + public SignedPreKeySignature() { + super(SignedPreKeySignature.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java b/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..ad3d21c16c79dfbbcf989c77f28cd94cd91de124 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/axolotl/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.AXOLOTL) +package im.conversations.android.xmpp.model.axolotl; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/Bind.java b/src/main/java/im/conversations/android/xmpp/model/bind/Bind.java new file mode 100644 index 0000000000000000000000000000000000000000..27264f7545e079f2c3cb6415f4763ab3c334dc3b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/Bind.java @@ -0,0 +1,34 @@ +package im.conversations.android.xmpp.model.bind; + +import com.google.common.base.Strings; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Bind extends Extension { + + public Bind() { + super(Bind.class); + } + + public void setResource(final String resource) { + this.addExtension(new Resource(resource)); + } + + public eu.siacs.conversations.xmpp.Jid getJid() { + final var jidExtension = this.getExtension(Jid.class); + if (jidExtension == null) { + return null; + } + final var content = jidExtension.getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } + try { + return eu.siacs.conversations.xmpp.Jid.ofEscaped(content); + } catch (final IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/Jid.java b/src/main/java/im/conversations/android/xmpp/model/bind/Jid.java new file mode 100644 index 0000000000000000000000000000000000000000..04633a009929cce3b7776f87f5cd28c3f767cd49 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/Jid.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.bind; + + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Jid extends Extension { + + public Jid() { + super(Jid.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/Resource.java b/src/main/java/im/conversations/android/xmpp/model/bind/Resource.java new file mode 100644 index 0000000000000000000000000000000000000000..b3fd1e5c17d070ba994754f499e74a0dd0d7cf31 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/Resource.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.bind; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Resource extends Extension { + public Resource() { + super(Resource.class); + } + + public Resource(final String resource) { + this(); + this.setContent(resource); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bind/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..aebaeeb729420e22431f57a7834d23c9578f6be0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BIND) +package im.conversations.android.xmpp.model.bind; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java new file mode 100644 index 0000000000000000000000000000000000000000..3af144105420ffbbd222dd5f8c4423b0aaaf07d5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Bind.java @@ -0,0 +1,28 @@ +package im.conversations.android.xmpp.model.bind2; + +import java.util.Collection; +import java.util.Collections; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Bind extends Extension { + + public Bind() { + super(Bind.class); + } + + public Inline getInline() { + return this.getExtension(Inline.class); + } + + public Collection getInlineFeatures() { + final var inline = getInline(); + return inline == null ? Collections.emptyList() : inline.getExtensions(Feature.class); + } + + public void setTag(final String tag) { + this.addExtension(new Tag(tag)); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java new file mode 100644 index 0000000000000000000000000000000000000000..0144edb9147ae909c1e11626fcd38c8a20c6cde7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Bound.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Bound extends Extension { + public Bound() { + super(Bound.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java new file mode 100644 index 0000000000000000000000000000000000000000..66720abbc631da53c7308095d87138db0d561156 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Feature.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Feature extends Extension { + + public Feature() { + super(Feature.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java new file mode 100644 index 0000000000000000000000000000000000000000..641a9d4f4556673493d9648c4d8e80e0e73123f5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Inline.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Inline extends Extension { + + public Inline() { + super(Inline.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java b/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java new file mode 100644 index 0000000000000000000000000000000000000000..5fac1d9e378ba68012ac299d8ac98713fba9f985 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/Tag.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.bind2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Tag extends Extension { + + public Tag() { + super(Tag.class); + } + + public Tag(final String tag) { + this(); + setContent(tag); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..2d8c5e92cb122bfb76f8dada143389791de0295d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bind2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BIND2) +package im.conversations.android.xmpp.model.bind2; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java new file mode 100644 index 0000000000000000000000000000000000000000..6f5d00b3e566d1651059d75fb350c2a233959439 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Block extends Extension { + + public Block() { + super(Block.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java new file mode 100644 index 0000000000000000000000000000000000000000..a56662d77152bf2d22bc11d76f8d01cb90f30c52 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Blocklist extends Extension { + public Blocklist() { + super(Blocklist.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..647b0ae9912de215bffb4f5057bbc95dae3e6bfd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.blocking; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Item extends Extension { + + public Item() { + super(Item.class); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java new file mode 100644 index 0000000000000000000000000000000000000000..90cec110c050788661af7afbae475d68a7906d26 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Unblock extends Extension { + + public Unblock() { + super(Unblock.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java b/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..22d8f0e1f0b7bdb1d92d58403e5fdf3c6a3d459f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BLOCKING) +package im.conversations.android.xmpp.model.blocking; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; 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 new file mode 100644 index 0000000000000000000000000000000000000000..0f924e8883b40157d0aa96676ab5f6412e971138 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Conference.java @@ -0,0 +1,32 @@ +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/Extensions.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.java new file mode 100644 index 0000000000000000000000000000000000000000..b9385cf5473de88d5d43d771a57fe88995bc2536 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Extensions.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 Extensions extends Extension { + + public Extensions() { + super(Extensions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java b/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.java new file mode 100644 index 0000000000000000000000000000000000000000..ee5efa3864baebb0c848c65769b6b0a8d4a69022 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/Nick.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 Nick extends Extension { + + public Nick() { + super(Nick.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 new file mode 100644 index 0000000000000000000000000000000000000000..1bb963be849a57cdfeae7a2eb1c8930db49c799a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/bookmark/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.BOOKMARKS2) +package im.conversations.android.xmpp.model.bookmark; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..d0f23b2833e9d1e0694d168e5fe0d65769887d4a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/Capabilities.java @@ -0,0 +1,43 @@ +package im.conversations.android.xmpp.model.capabilties; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import com.google.common.io.BaseEncoding; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.Hash; + +@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES_2) +public class Capabilities extends Extension { + + public Capabilities() { + super(Capabilities.class); + } + + public EntityCapabilities2.EntityCaps2Hash getHash() { + final Optional sha256Hash = + Iterables.tryFind( + getExtensions(Hash.class), h -> h.getAlgorithm() == Hash.Algorithm.SHA_256); + if (sha256Hash.isPresent()) { + final String content = sha256Hash.get().getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } + if (BaseEncoding.base64().canDecode(content)) { + return EntityCapabilities2.EntityCaps2Hash.of(Hash.Algorithm.SHA_256, content); + } + } + return null; + } + + public void setHash(final EntityCapabilities2.EntityCaps2Hash caps2Hash) { + final Hash hash = new Hash(); + hash.setAlgorithm(caps2Hash.algorithm); + hash.setContent(caps2Hash.encoded()); + this.addExtension(hash); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..f8ed4ef66196030c51ad5bdac612ffc417e0c2c7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/EntityCapabilities.java @@ -0,0 +1,39 @@ +package im.conversations.android.xmpp.model.capabilties; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import im.conversations.android.xmpp.model.Extension; + +public interface EntityCapabilities { + + E getExtension(final Class clazz); + + default NodeHash getCapabilities() { + final String node; + final im.conversations.android.xmpp.EntityCapabilities.Hash hash; + final var capabilities = this.getExtension(Capabilities.class); + final var legacyCapabilities = this.getExtension(LegacyCapabilities.class); + if (capabilities != null) { + node = null; + hash = capabilities.getHash(); + } else if (legacyCapabilities != null) { + node = legacyCapabilities.getNode(); + hash = legacyCapabilities.getHash(); + } else { + return null; + } + return hash == null ? null : new NodeHash(node, hash); + } + + class NodeHash { + public final String node; + public final im.conversations.android.xmpp.EntityCapabilities.Hash hash; + + private NodeHash( + @Nullable String node, + @NonNull final im.conversations.android.xmpp.EntityCapabilities.Hash hash) { + this.node = node; + this.hash = hash; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java b/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java new file mode 100644 index 0000000000000000000000000000000000000000..797d627cf10ce40f3c7c54841092d4ae012e65c1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/capabilties/LegacyCapabilities.java @@ -0,0 +1,45 @@ +package im.conversations.android.xmpp.model.capabilties; + +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "c", namespace = Namespace.ENTITY_CAPABILITIES) +public class LegacyCapabilities extends Extension { + + private static final String HASH_ALGORITHM = "sha-1"; + + public LegacyCapabilities() { + super(LegacyCapabilities.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public EntityCapabilities.EntityCapsHash getHash() { + final String hash = getAttribute("hash"); + final String ver = getAttribute("ver"); + if (Strings.isNullOrEmpty(ver) || Strings.isNullOrEmpty(hash)) { + return null; + } + if (HASH_ALGORITHM.equals(hash) && BaseEncoding.base64().canDecode(ver)) { + return EntityCapabilities.EntityCapsHash.of(ver); + } else { + return null; + } + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public void setHash(final EntityCapabilities.EntityCapsHash hash) { + this.setAttribute("hash", HASH_ALGORITHM); + this.setAttribute("ver", hash.encoded()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java new file mode 100644 index 0000000000000000000000000000000000000000..38b740e8c0c04ac1b59ce4edf5e906bba9e0ed3d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Enable.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Enable extends Extension { + + public Enable() { + super(Enable.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java new file mode 100644 index 0000000000000000000000000000000000000000..507869a60c5967760f42c916d5d5544a29b5ff71 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Received.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Received extends Extension { + + public Received() { + super(Received.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java b/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java new file mode 100644 index 0000000000000000000000000000000000000000..0201c53c636cf8e70a88de457c7a354ce435cc10 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/Sent.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.carbons; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Sent extends Extension { + + public Sent() { + super(Sent.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java b/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..f4c76376a384778c84484ee269d9e0135f69ce18 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/carbons/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CARBONS) +package im.conversations.android.xmpp.model.carbons; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java b/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java new file mode 100644 index 0000000000000000000000000000000000000000..dbd7395572c7b614f9c7caf83996fbe94e8aca55 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.correction; + +import androidx.annotation.NonNull; +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.LAST_MESSAGE_CORRECTION) +public class Replace extends Extension { + + public Replace() { + super(Replace.class); + } + + public String getId() { + return Strings.emptyToNull(this.getAttribute("id")); + } + + public void setId(@NonNull final String id) { + this.setAttribute("id", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/Active.java b/src/main/java/im/conversations/android/xmpp/model/csi/Active.java new file mode 100644 index 0000000000000000000000000000000000000000..21fb65bb4a7d22e05c6ac8f6ba8cc15dd75f9551 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/Active.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.csi; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Active extends StreamElement { + + public Active() { + super(Active.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java b/src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java new file mode 100644 index 0000000000000000000000000000000000000000..60bd59edb145ce99cee42e4d8aa0e4f67e759307 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/ClientStateIndication.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.csi; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; + +@XmlElement(name = "csi") +public class ClientStateIndication extends StreamFeature { + + public ClientStateIndication() { + super(ClientStateIndication.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java b/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java new file mode 100644 index 0000000000000000000000000000000000000000..7c36b593d544f29a7c2859a3ebd6ccee39c3a05b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/Inactive.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.csi; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Inactive extends StreamElement { + + public Inactive() { + super(Inactive.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java b/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..58d26b1f13186513a6338807cca6eacb2102c11f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/csi/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CSI) +package im.conversations.android.xmpp.model.csi; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java new file mode 100644 index 0000000000000000000000000000000000000000..c754ee48de9364084628b26fc596d2645741ead3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -0,0 +1,110 @@ +package im.conversations.android.xmpp.model.data; + +import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Map; + +@XmlElement(name = "x") +public class Data extends Extension { + + private static final String FORM_TYPE = "FORM_TYPE"; + private static final String FIELD_TYPE_HIDDEN = "hidden"; + private static final String FORM_TYPE_SUBMIT = "submit"; + + public Data() { + super(Data.class); + } + + public String getFormType() { + final var fields = this.getExtensions(Field.class); + final var formTypeField = Iterables.find(fields, f -> FORM_TYPE.equals(f.getFieldName())); + return Iterables.getFirst(formTypeField.getValues(), null); + } + + public Collection getFields() { + return Collections2.filter( + this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); + } + + private void addField(final String name, final Object value) { + addField(name, value, null); + } + + private void addField(final String name, final Object value, final String type) { + if (value == null) { + throw new IllegalArgumentException("Null values are not supported on data fields"); + } + final var field = this.addExtension(new Field()); + field.setFieldName(name); + if (type != null) { + field.setType(type); + } + if (value instanceof Collection) { + for (final Object subValue : (Collection) value) { + if (subValue instanceof String) { + final var valueExtension = field.addExtension(new Value()); + valueExtension.setContent((String) subValue); + } else { + throw new IllegalArgumentException( + String.format( + "%s is not a supported field value", + subValue.getClass().getSimpleName())); + } + } + } else { + final var valueExtension = field.addExtension(new Value()); + if (value instanceof String) { + valueExtension.setContent((String) value); + } else if (value instanceof Integer) { + valueExtension.setContent(String.valueOf(value)); + } else if (value instanceof Boolean) { + valueExtension.setContent(Boolean.TRUE.equals(value) ? "1" : "0"); + } else { + throw new IllegalArgumentException( + String.format( + "%s is not a supported field value", + value.getClass().getSimpleName())); + } + } + } + + private void setFormType(final String formType) { + this.addField(FORM_TYPE, formType, FIELD_TYPE_HIDDEN); + } + + public static Data of(final String formType, final Map values) { + final var data = new Data(); + data.setType(FORM_TYPE_SUBMIT); + data.setFormType(formType); + for (final Map.Entry entry : values.entrySet()) { + data.addField(entry.getKey(), entry.getValue()); + } + return data; + } + + public Data submit(final Map values) { + final String formType = this.getFormType(); + final var submit = new Data(); + submit.setType(FORM_TYPE_SUBMIT); + if (formType != null) { + submit.setFormType(formType); + } + for (final Field existingField : this.getFields()) { + final var fieldName = existingField.getFieldName(); + final Object submittedValue = values.get(fieldName); + if (submittedValue != null) { + submit.addField(fieldName, submittedValue); + } else { + submit.addField(fieldName, existingField.getValues()); + } + } + return submit; + } + + private void setType(final String type) { + this.setAttribute("type", type); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java new file mode 100644 index 0000000000000000000000000000000000000000..f3f72fab86e7924e107674d02de4707030ebbf13 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -0,0 +1,29 @@ +package im.conversations.android.xmpp.model.data; +import eu.siacs.conversations.xml.Element; +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; + +@XmlElement +public class Field extends Extension { + public Field() { + super(Field.class); + } + + public String getFieldName() { + return getAttribute("var"); + } + + public Collection getValues() { + return Collections2.transform(getExtensions(Value.class), Element::getContent); + } + + public void setFieldName(String name) { + this.setAttribute("var", name); + } + + public void setType(String type) { + this.setAttribute("type", type); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Option.java b/src/main/java/im/conversations/android/xmpp/model/data/Option.java new file mode 100644 index 0000000000000000000000000000000000000000..b9c3e9aae4f1d7285dcfa07ed4f5a20f984f32d4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Option.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.data; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Option extends Extension { + + public Option() { + super(Option.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Value.java b/src/main/java/im/conversations/android/xmpp/model/data/Value.java new file mode 100644 index 0000000000000000000000000000000000000000..8e9eccc4d715f6151e7cff6c9b9065f4cb3c6adc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/Value.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.data; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Value extends Extension { + + public Value() { + super(Value.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/data/package-info.java b/src/main/java/im/conversations/android/xmpp/model/data/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..fcc0e1f790ba4f9a77e79115244ff4528f507abd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/data/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DATA) +package im.conversations.android.xmpp.model.data; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java b/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java new file mode 100644 index 0000000000000000000000000000000000000000..b294f83d456fb0fa17779f187eb7838200e34cce --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/delay/Delay.java @@ -0,0 +1,30 @@ +package im.conversations.android.xmpp.model.delay; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.Timestamps; +import im.conversations.android.xmpp.model.Extension; +import java.text.ParseException; +import java.time.Instant; + +@XmlElement(namespace = Namespace.DELAY) +public class Delay extends Extension { + + public Delay() { + super(Delay.class); + } + + public Instant getStamp() { + final var stamp = this.getAttribute("stamp"); + if (Strings.isNullOrEmpty(stamp)) { + return null; + } + try { + return Instant.ofEpochMilli(Timestamps.parse(stamp)); + } catch (final IllegalArgumentException | ParseException e) { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java new file mode 100644 index 0000000000000000000000000000000000000000..86a93af0dcfd00eb77688ca5f3da5074a190b27c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Service.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.disco.external; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Service extends Extension { + + public Service() { + super(Service.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java new file mode 100644 index 0000000000000000000000000000000000000000..36338083da5d03530ee42027f618225e8c248632 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.disco.external; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Services extends Extension { + + public Services() { + super(Services.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..868a5a17531c7b5fedb1764f24afcc15fcb3cb82 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.EXTERNAL_SERVICE_DISCOVERY) +package im.conversations.android.xmpp.model.disco.external; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java new file mode 100644 index 0000000000000000000000000000000000000000..dd288918cba6541a123dea1544edee5aa8689591 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/Feature.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.disco.info; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Feature extends Extension { + public Feature() { + super(Feature.class); + } + + public String getVar() { + return this.getAttribute("var"); + } + + public void setVar(final String feature) { + this.setAttribute("var", feature); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java new file mode 100644 index 0000000000000000000000000000000000000000..6da0a4aa22f1770095dbf0bae98de2a6344a453b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/Identity.java @@ -0,0 +1,39 @@ +package im.conversations.android.xmpp.model.disco.info; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Identity extends Extension { + public Identity() { + super(Identity.class); + } + + public String getCategory() { + return this.getAttribute("category"); + } + + public String getType() { + return this.getAttribute("type"); + } + + public String getLang() { + return this.getAttribute("xml:lang"); + } + + public String getIdentityName() { + return this.getAttribute("name"); + } + + public void setIdentityName(final String name) { + this.setAttribute("name", name); + } + + public void setType(final String type) { + this.setAttribute("type", type); + } + + public void setCategory(final String category) { + this.setAttribute("category", category); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..55f104e25bb547419e1e74ffa077ec563a354ea3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java @@ -0,0 +1,38 @@ +package im.conversations.android.xmpp.model.disco.info; + +import com.google.common.collect.Iterables; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; + +@XmlElement(name = "query") +public class InfoQuery extends Extension { + + public InfoQuery() { + super(InfoQuery.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public Collection getFeatures() { + return this.getExtensions(Feature.class); + } + + public boolean hasFeature(final String feature) { + return Iterables.any(getFeatures(), f -> feature.equals(f.getVar())); + } + + public Collection getIdentities() { + return this.getExtensions(Identity.class); + } + + public boolean hasIdentityWithCategory(final String category) { + return Iterables.any(getIdentities(), i -> category.equals(i.getCategory())); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..60eb24a5983d156b91500f99418ae0b1788e7e02 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DISCO_INFO) +package im.conversations.android.xmpp.model.disco.info; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java b/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..f5bf2b98400d94fcbcc11c815638d15f645170bb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/items/Item.java @@ -0,0 +1,22 @@ +package im.conversations.android.xmpp.model.disco.items; + +import androidx.annotation.Nullable; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Item extends Extension { + public Item() { + super(Item.class); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } + + public @Nullable String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..981132ed6905fc901ca3c5f208d2b6af993de4cd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/items/ItemsQuery.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.disco.items; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "query") +public class ItemsQuery extends Extension { + public ItemsQuery() { + super(ItemsQuery.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public String getNode() { + return this.getAttribute("node"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java b/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..a170e5cee398c171c36b5fa396362aa836ad8dcb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/disco/items/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DISCO_ITEMS) +package im.conversations.android.xmpp.model.disco.items; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Condition.java b/src/main/java/im/conversations/android/xmpp/model/error/Condition.java new file mode 100644 index 0000000000000000000000000000000000000000..bd68c2c433cad9bb56a6b70a3c74629cf347f6d0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/error/Condition.java @@ -0,0 +1,188 @@ +package im.conversations.android.xmpp.model.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public abstract class Condition extends Extension { + + private Condition(Class clazz) { + super(clazz); + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class BadRequest extends Condition { + + public BadRequest() { + super(BadRequest.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Conflict extends Condition { + + public Conflict() { + super(Conflict.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class FeatureNotImplemented extends Condition { + + public FeatureNotImplemented() { + super(FeatureNotImplemented.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Forbidden extends Condition { + + public Forbidden() { + super(Forbidden.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Gone extends Condition { + + public Gone() { + super(Gone.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class InternalServerError extends Condition { + + public InternalServerError() { + super(InternalServerError.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class ItemNotFound extends Condition { + + public ItemNotFound() { + super(ItemNotFound.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class JidMalformed extends Condition { + + public JidMalformed() { + super(JidMalformed.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class NotAcceptable extends Condition { + + public NotAcceptable() { + super(NotAcceptable.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class NotAllowed extends Condition { + + public NotAllowed() { + super(NotAllowed.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class NotAuthorized extends Condition { + + public NotAuthorized() { + super(NotAuthorized.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class PaymentRequired extends Condition { + + public PaymentRequired() { + super(PaymentRequired.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RecipientUnavailable extends Condition { + + public RecipientUnavailable() { + super(RecipientUnavailable.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class Redirect extends Condition { + + public Redirect() { + super(Redirect.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RegistrationRequired extends Condition { + + public RegistrationRequired() { + super(RegistrationRequired.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RemoteServerNotFound extends Condition { + + public RemoteServerNotFound() { + super(RemoteServerNotFound.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class RemoteServerTimeout extends Condition { + + public RemoteServerTimeout() { + super(RemoteServerTimeout.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class ResourceConstraint extends Condition { + + public ResourceConstraint() { + super(ResourceConstraint.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class ServiceUnavailable extends Condition { + + public ServiceUnavailable() { + super(ServiceUnavailable.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class SubscriptionRequired extends Condition { + + public SubscriptionRequired() { + super(SubscriptionRequired.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class UndefinedCondition extends Condition { + + public UndefinedCondition() { + super(UndefinedCondition.class); + } + } + + @XmlElement(namespace = Namespace.STANZAS) + public static class UnexpectedRequest extends Condition { + + public UnexpectedRequest() { + super(UnexpectedRequest.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Error.java b/src/main/java/im/conversations/android/xmpp/model/error/Error.java new file mode 100644 index 0000000000000000000000000000000000000000..0a07e73f9d078a9412fedae7c34438567ba077d8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/error/Error.java @@ -0,0 +1,55 @@ +package im.conversations.android.xmpp.model.error; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Locale; +import eu.siacs.conversations.xml.Namespace; + +@XmlElement(namespace = Namespace.JABBER_CLIENT) +public class Error extends Extension { + + public Error() { + super(Error.class); + } + + public Condition getCondition() { + return this.getExtension(Condition.class); + } + + public void setCondition(final Condition condition) { + this.addExtension(condition); + } + + public Text getText() { + return this.getExtension(Text.class); + } + + public String getTextAsString() { + final var text = getText(); + return text == null ? null : text.getContent(); + } + + public void setType(final Type type) { + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + + public void addExtensions(final Extension[] extensions) { + for (final Extension extension : extensions) { + this.addExtension(extension); + } + } + + public enum Type { + MODIFY, + CANCEL, + AUTH, + WAIT + } + + public static class Extension extends im.conversations.android.xmpp.model.Extension { + + public Extension(Class clazz) { + super(clazz); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/error/Text.java b/src/main/java/im/conversations/android/xmpp/model/error/Text.java new file mode 100644 index 0000000000000000000000000000000000000000..478b1f5cd531ea54ff7402c7a5e9c7ec4c23bec7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/error/Text.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.STANZAS) +public class Text extends Extension { + + public Text() { + super(Text.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/Fast.java b/src/main/java/im/conversations/android/xmpp/model/fast/Fast.java new file mode 100644 index 0000000000000000000000000000000000000000..1291d8ea06a5c566c8672c0d71d7e1e6f1d7490f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/Fast.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.fast; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Fast extends Extension { + public Fast() { + super(Fast.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java b/src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java new file mode 100644 index 0000000000000000000000000000000000000000..240f5de0e13d9b9dbd4cf08c7369469600ab8b4a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/Mechanism.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.fast; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Mechanism extends Extension { + public Mechanism() { + super(Mechanism.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java b/src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java new file mode 100644 index 0000000000000000000000000000000000000000..4ac5a9205f4189d246d95b00aa82d99188e5e148 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/RequestToken.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.fast; + +import eu.siacs.conversations.crypto.sasl.HashedToken; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class RequestToken extends Extension { + public RequestToken() { + super(RequestToken.class); + } + + public RequestToken(final HashedToken.Mechanism mechanism) { + this(); + this.setAttribute("mechanism", mechanism.name()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/Token.java b/src/main/java/im/conversations/android/xmpp/model/fast/Token.java new file mode 100644 index 0000000000000000000000000000000000000000..258cd9abadf650fa97e15ab4d45e1e5aa990d69d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/Token.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.fast; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Token extends Extension { + + public Token() { + super(Token.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/fast/package-info.java b/src/main/java/im/conversations/android/xmpp/model/fast/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..effc9e511cb824aff5454a29e3a300f4e6121e11 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/fast/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.FAST) +package im.conversations.android.xmpp.model.fast; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java b/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java new file mode 100644 index 0000000000000000000000000000000000000000..80a646a41df7901ab9fbf18842f37f19757972c0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/forward/Forwarded.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.forward; + +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.stanza.Message; + +@XmlElement(namespace = Namespace.FORWARD) +public class Forwarded extends Extension { + + public Forwarded() { + super(Forwarded.class); + } + + public Message getMessage() { + return this.getExtension(Message.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/hints/Store.java b/src/main/java/im/conversations/android/xmpp/model/hints/Store.java new file mode 100644 index 0000000000000000000000000000000000000000..fe82612adcc61412c55a664610cac99372e2188d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/hints/Store.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.hints; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Store extends Extension { + + public Store() { + super(Store.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/hints/package-info.java b/src/main/java/im/conversations/android/xmpp/model/hints/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..76c25d6551e88a2647b9afa5dba86676743fa930 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/hints/package-info.java @@ -0,0 +1,6 @@ +@XmlPackage(namespace = Namespace.HINTS) +package im.conversations.android.xmpp.model.hints; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java new file mode 100644 index 0000000000000000000000000000000000000000..5857f058518768c5ac26016552de2cad6b291baf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Body.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Body extends Extension { + + public Body() { + super(Body.class); + } + + public Body(final String content) { + this(); + setContent(content); + } + + public String getLang() { + return this.getAttribute("xml:lang"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java new file mode 100644 index 0000000000000000000000000000000000000000..7c5b3bdc98fdedf964b766ada9f7f47645e936fd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Priority.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Priority extends Extension { + + public Priority() { + super(Priority.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Show.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Show.java new file mode 100644 index 0000000000000000000000000000000000000000..44dc512be24e2aa4c32de44cfe4aa0f82eb25f8a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Show.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Show extends Extension { + public Show() { + super(Show.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Status.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Status.java new file mode 100644 index 0000000000000000000000000000000000000000..3175230d7191576d7c836350a4ca75cfe5273467 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Status.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Status extends Extension { + + + public Status() { + super(Status.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java new file mode 100644 index 0000000000000000000000000000000000000000..4ae3b8ed519a234698cc3ec30f12d4f1a25d44f4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Subject.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Subject extends Extension { + + public Subject() { + super(Subject.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java b/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java new file mode 100644 index 0000000000000000000000000000000000000000..703429ef0ba9fb9d3122af6f222f619cf4bb9705 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/Thread.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Thread extends Extension { + + public Thread() { + super(Thread.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java b/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..448804d7c8c6d93849f8c151009486937c74c3d8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jabber/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JABBER_CLIENT) +package im.conversations.android.xmpp.model.jabber; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java similarity index 62% rename from src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java rename to src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java index a24040d3dbe82efcbfc558e0ca56c006cf4d82bd..aeb79ffd2f328e340348a472f86289004cd90f72 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/im/conversations/android/xmpp/model/jingle/Jingle.java @@ -1,4 +1,4 @@ -package eu.siacs.conversations.xmpp.jingle.stanzas; +package im.conversations.android.xmpp.model.jingle; import androidx.annotation.NonNull; @@ -10,66 +10,38 @@ import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.jingle.stanzas.Content; +import eu.siacs.conversations.xmpp.jingle.stanzas.Group; +import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; -import java.util.Map; - -public class JinglePacket extends IqPacket { - - private JinglePacket() { - super(); - } +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; - public JinglePacket(final Action action, final String sessionId) { - super(TYPE.SET); - final Element jingle = addChild("jingle", Namespace.JINGLE); - jingle.setAttribute("sid", sessionId); - jingle.setAttribute("action", action.toString()); - } +import java.util.Map; - public static JinglePacket upgrade(final IqPacket iqPacket) { - Preconditions.checkArgument(iqPacket.hasChild("jingle", Namespace.JINGLE)); - Preconditions.checkArgument(iqPacket.getType() == TYPE.SET); - final JinglePacket jinglePacket = new JinglePacket(); - jinglePacket.setAttributes(iqPacket.getAttributes()); - jinglePacket.setChildren(iqPacket.getChildren()); - return jinglePacket; - } +@XmlElement +public class Jingle extends Extension { - // TODO deprecate this somehow and make file transfer fail if there are multiple (or something) - public Content getJingleContent() { - final Element content = getJingleChild("content"); - return content == null ? null : Content.upgrade(content); + public Jingle() { + super(Jingle.class); } - public Group getGroup() { - final Element jingle = findChild("jingle", Namespace.JINGLE); - final Element group = jingle.findChild("group", Namespace.JINGLE_APPS_GROUPING); - return group == null ? null : Group.upgrade(group); + public Jingle(final Action action, final String sessionId) { + this(); + this.setAttribute("sid", sessionId); + this.setAttribute("action", action.toString()); } - public void addGroup(final Group group) { - this.addJingleChild(group); - } - - public Map getJingleContents() { - final Element jingle = findChild("jingle", Namespace.JINGLE); - ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); - for (final Element child : jingle.getChildren()) { - if ("content".equals(child.getName())) { - final Content content = Content.upgrade(child); - builder.put(content.getContentName(), content); - } - } - return builder.build(); + public String getSessionId() { + return this.getAttribute("sid"); } - public void addJingleContent(final Content content) { // take content interface - addJingleChild(content); + public Action getAction() { + return Action.of(this.getAttribute("action")); } public ReasonWrapper getReason() { - final Element reasonElement = getJingleChild("reason"); + final Element reasonElement = this.findChild("reason"); if (reasonElement == null) { return new ReasonWrapper(Reason.UNKNOWN, null); } @@ -86,8 +58,7 @@ public class JinglePacket extends IqPacket { } public void setReason(final Reason reason, final String text) { - final Element jingle = findChild("jingle", Namespace.JINGLE); - final Element reasonElement = jingle.addChild("reason"); + final Element reasonElement = this.addChild("reason"); reasonElement.addChild(reason.toString()); if (!Strings.isNullOrEmpty(text)) { reasonElement.addChild("text").setContent(text); @@ -97,31 +68,44 @@ public class JinglePacket extends IqPacket { // RECOMMENDED for session-initiate, NOT RECOMMENDED otherwise public void setInitiator(final Jid initiator) { Preconditions.checkArgument(initiator.isFullJid(), "initiator should be a full JID"); - findChild("jingle", Namespace.JINGLE).setAttribute("initiator", initiator); + this.setAttribute("initiator", initiator); } // RECOMMENDED for session-accept, NOT RECOMMENDED otherwise - public void setResponder(Jid responder) { + public void setResponder(final Jid responder) { Preconditions.checkArgument(responder.isFullJid(), "responder should be a full JID"); - findChild("jingle", Namespace.JINGLE).setAttribute("responder", responder); + this.setAttribute("responder", responder); } - public Element getJingleChild(final String name) { - final Element jingle = findChild("jingle", Namespace.JINGLE); - return jingle == null ? null : jingle.findChild(name); + public Group getGroup() { + final Element group = this.findChild("group", Namespace.JINGLE_APPS_GROUPING); + return group == null ? null : Group.upgrade(group); } - public void addJingleChild(final Element child) { - final Element jingle = findChild("jingle", Namespace.JINGLE); - jingle.addChild(child); + public void addGroup(final Group group) { + this.addChild(group); } - public String getSessionId() { - return findChild("jingle", Namespace.JINGLE).getAttribute("sid"); + // TODO deprecate this somehow and make file transfer fail if there are multiple (or something) + public Content getJingleContent() { + final Element content = this.findChild("content"); + return content == null ? null : Content.upgrade(content); } - public Action getAction() { - return Action.of(findChild("jingle", Namespace.JINGLE).getAttribute("action")); + public void addJingleContent(final Content content) { // take content interface + this.addChild(content); + } + + + public Map getJingleContents() { + ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (final Element child : this.getChildren()) { + if ("content".equals(child.getName())) { + final Content content = Content.upgrade(child); + builder.put(content.getContentName(), content); + } + } + return builder.build(); } public enum Action { diff --git a/src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java b/src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..a0c6dfbbd76a9b48d975ea8a63fdac2c39957858 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jingle/error/JingleCondition.java @@ -0,0 +1,44 @@ +package im.conversations.android.xmpp.model.jingle.error; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.error.Error; + +public abstract class JingleCondition extends Error.Extension { + + private JingleCondition(Class clazz) { + super(clazz); + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class OutOfOrder extends JingleCondition { + + public OutOfOrder() { + super(OutOfOrder.class); + } + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class TieBreak extends JingleCondition { + + public TieBreak() { + super(TieBreak.class); + } + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class UnknownSession extends JingleCondition { + + public UnknownSession() { + super(UnknownSession.class); + } + } + + @XmlElement(namespace = Namespace.JINGLE_ERRORS) + public static class UnsupportedInfo extends JingleCondition { + + public UnsupportedInfo() { + super(UnsupportedInfo.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java b/src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..6af2511e8da94ec172d8f1276422988db9812426 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jingle/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JINGLE) +package im.conversations.android.xmpp.model.jingle; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java new file mode 100644 index 0000000000000000000000000000000000000000..20ae15aee379933b996ec257fcc49acdc3c0a3f5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Accept.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Accept extends JingleMessage { + + public Accept() { + super(Accept.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java b/src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..0045844f0a47231e302d51db057c862a5a994a3c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/JingleMessage.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class JingleMessage extends Extension { + + public JingleMessage(Class clazz) { + super(clazz); + } + + public String getSessionId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java new file mode 100644 index 0000000000000000000000000000000000000000..b6be44ee091b85165958c182ebef230dd6c1bbe8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Proceed.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.jmi; + +import com.google.common.primitives.Ints; + +import eu.siacs.conversations.xml.Element; +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Proceed extends JingleMessage { + + public Proceed() { + super(Proceed.class); + } + + public Integer getDeviceId() { + // TODO use proper namespace and create extension + final Element device = this.findChild("device"); + final String id = device == null ? null : device.getAttribute("id"); + if (id == null) { + return null; + } + return Ints.tryParse(id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java new file mode 100644 index 0000000000000000000000000000000000000000..e54f9c612b5a948bd103cd3fba34977e2b91e7ec --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Propose.java @@ -0,0 +1,38 @@ +package im.conversations.android.xmpp.model.jmi; + +import com.google.common.collect.ImmutableList; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; +import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import im.conversations.android.annotation.XmlElement; + +import java.util.List; + +@XmlElement +public class Propose extends JingleMessage { + + public Propose() { + super(Propose.class); + } + + public List getDescriptions() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + // TODO create proper extension for description + for (final Element child : getChildren()) { + if ("description".equals(child.getName())) { + final String namespace = child.getNamespace(); + if (Namespace.JINGLE_APPS_FILE_TRANSFER.contains(namespace)) { + builder.add(FileTransferDescription.upgrade(child)); + } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + builder.add(RtpDescription.upgrade(child)); + } else { + builder.add(GenericDescription.upgrade(child)); + } + } + } + return builder.build(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java new file mode 100644 index 0000000000000000000000000000000000000000..e71206fd619b09378c35ba0fd450e227a15545eb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Reject.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Reject extends JingleMessage { + + public Reject() { + super(Reject.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java b/src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java new file mode 100644 index 0000000000000000000000000000000000000000..7c507156d85d9e7653d9c61b0261187f92ef7beb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/Retract.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Retract extends JingleMessage { + + public Retract() { + super(Retract.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java b/src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..9ce640b1f9619957a7127e4e6ce325f3d2c3c65a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/jmi/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JINGLE_MESSAGE) +package im.conversations.android.xmpp.model.jmi; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/End.java b/src/main/java/im/conversations/android/xmpp/model/mam/End.java new file mode 100644 index 0000000000000000000000000000000000000000..757ed60c69995b98183bee3ccb22324214baa533 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/End.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class End extends Extension { + public End() { + super(End.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java b/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java new file mode 100644 index 0000000000000000000000000000000000000000..534072647563b9f55b1e9df190a3de8d309fc8af --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Fin extends Extension { + + public Fin() { + super(Fin.class); + } + + public boolean isComplete() { + return this.getAttributeAsBoolean("complete"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java b/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java new file mode 100644 index 0000000000000000000000000000000000000000..9f05e08fc21fc33bc708294497b09a0549697395 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Metadata extends Extension { + + public Metadata() { + super(Metadata.class); + } + + public Start getStart() { + return this.getExtension(Start.class); + } + + public End getEnd() { + return this.getExtension(End.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Query.java b/src/main/java/im/conversations/android/xmpp/model/mam/Query.java new file mode 100644 index 0000000000000000000000000000000000000000..d8f701d91de17b8e3f00da00bcb9dcfb8019fee7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Query.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Query extends Extension { + + public Query() { + super(Query.class); + } + + public void setQueryId(final String id) { + this.setAttribute("queryid", id); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Result.java b/src/main/java/im/conversations/android/xmpp/model/mam/Result.java new file mode 100644 index 0000000000000000000000000000000000000000..253499756ba26bf1387fd4191fa126d4f1ec53b9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Result.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.forward.Forwarded; + +@XmlElement +public class Result extends Extension { + + public Result() { + super(Result.class); + } + + public Forwarded getForwarded() { + return this.getExtension(Forwarded.class); + } + + public String getId() { + return this.getAttribute("id"); + } + + public String getQueryId() { + return this.getAttribute("queryid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/Start.java b/src/main/java/im/conversations/android/xmpp/model/mam/Start.java new file mode 100644 index 0000000000000000000000000000000000000000..9ff84b2564062b368ac48e093f1d2ac7a8b77884 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/Start.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Start extends Extension { + + public Start() { + super(Start.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java b/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..1aa4982e6af857936aa753c81aa602de71b57050 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mam/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MESSAGE_ARCHIVE_MANAGEMENT) +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java b/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java new file mode 100644 index 0000000000000000000000000000000000000000..be31df35d6b0259c9391d96f45c6ad8cadc9a81d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/Displayed.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Displayed extends Extension { + + public Displayed() { + super(Displayed.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java b/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java new file mode 100644 index 0000000000000000000000000000000000000000..08161af709d9747f6c0dc58ece892232dd39e281 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/Markable.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceiptRequest; + +@XmlElement +public class Markable extends DeliveryReceiptRequest { + + public Markable() { + super(Markable.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/Received.java b/src/main/java/im/conversations/android/xmpp/model/markers/Received.java new file mode 100644 index 0000000000000000000000000000000000000000..7007cd176271e4a46ab335e4b7c4e70409caa24e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/Received.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceipt; + +@XmlElement +public class Received extends DeliveryReceipt { + + public Received() { + super(Received.class); + } + + public void setId(String id) { + this.setAttribute("id", id); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java b/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..950963d4f00924052c6b4007cf343b7f52da65df --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/markers/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CHAT_MARKERS) +package im.conversations.android.xmpp.model.markers; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; 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 new file mode 100644 index 0000000000000000000000000000000000000000..9f5275371c3564ee52d1825bc67fda4327d36514 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/mds/Displayed.java @@ -0,0 +1,12 @@ +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; + +@XmlElement(namespace = Namespace.MDS_DISPLAYED) +public class Displayed extends Extension { + public Displayed() { + super(Displayed.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java b/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java new file mode 100644 index 0000000000000000000000000000000000000000..6502a16e72a41aea353c8d0467d38c6d22455c2a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java @@ -0,0 +1,9 @@ +package im.conversations.android.xmpp.model.muc; + +public enum Affiliation { + OWNER, + ADMIN, + MEMBER, + OUTCAST, + NONE; +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/History.java b/src/main/java/im/conversations/android/xmpp/model/muc/History.java new file mode 100644 index 0000000000000000000000000000000000000000..e09210e60277c22491ae60940543a77d4e3ab523 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/History.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class History extends Extension { + + public History() { + super(History.class); + } + + public void setMaxChars(final int maxChars) { + this.setAttribute("maxchars", maxChars); + } + + public void setMaxStanzas(final int maxStanzas) { + this.setAttribute("maxstanzas", maxStanzas); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java b/src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java new file mode 100644 index 0000000000000000000000000000000000000000..33da7b9af38b4083739aba6f0dd7a004d522b8cf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/MultiUserChat.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class MultiUserChat extends Extension { + + public MultiUserChat() { + super(MultiUserChat.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/Role.java b/src/main/java/im/conversations/android/xmpp/model/muc/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..9e9d3d165666b4f310b809adfb2ee1c764623a39 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/Role.java @@ -0,0 +1,8 @@ +package im.conversations.android.xmpp.model.muc; + +public enum Role { + MODERATOR, + VISITOR, + PARTICIPANT, + NONE +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/package-info.java b/src/main/java/im/conversations/android/xmpp/model/muc/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..41d652f204e92fd1e77bd5ff447efa959c4af9f2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MUC) +package im.conversations.android.xmpp.model.muc; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..7ff712aeaba976c687928c61c8255b4fb19967b1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/Item.java @@ -0,0 +1,58 @@ +package im.conversations.android.xmpp.model.muc.user; + +import android.util.Log; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.muc.Affiliation; +import im.conversations.android.xmpp.model.muc.Role; + +import java.util.Locale; + +@XmlElement +public class Item extends Extension { + + + public Item() { + super(Item.class); + } + + public Affiliation getAffiliation() { + final var affiliation = this.getAttribute("affiliation"); + if (Strings.isNullOrEmpty(affiliation)) { + return Affiliation.NONE; + } + try { + return Affiliation.valueOf(affiliation.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG,"could not parse affiliation "+affiliation); + return Affiliation.NONE; + } + } + + public Role getRole() { + final var role = this.getAttribute("role"); + if (Strings.isNullOrEmpty(role)) { + return Role.NONE; + } + try { + return Role.valueOf(role.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + Log.d(Config.LOGTAG,"could not parse role "+ role); + return Role.NONE; + } + } + + public String getNick() { + return this.getAttribute("nick"); + } + + public Jid getJid() { + return this.getAttributeAsJid("jid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java new file mode 100644 index 0000000000000000000000000000000000000000..5496c3ef204cdc2ef51de90f1b60285608408768 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/MucUser.java @@ -0,0 +1,27 @@ +package im.conversations.android.xmpp.model.muc.user; + +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement(name = "x") +public class MucUser extends Extension { + + public static final int STATUS_CODE_SELF_PRESENCE = 110; + + public MucUser() { + super(MucUser.class); + } + + public Item getItem() { + return this.getExtension(Item.class); + } + + public Collection getStatus() { + return Collections2.filter( + Collections2.transform(getExtensions(Status.class), Status::getCode), + Objects::nonNull); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java new file mode 100644 index 0000000000000000000000000000000000000000..0706585af418b6f18c561b86e0688a7987815ff6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/Status.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.muc.user; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Status extends Extension { + + public Status() { + super(Status.class); + } + + public Integer getCode() { + return this.getOptionalIntAttribute("code").orNull(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java b/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..f5bfcaeda0112563ec3a67faff002586518ebe65 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/muc/user/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.MUC_USER) +package im.conversations.android.xmpp.model.muc.user; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java b/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java new file mode 100644 index 0000000000000000000000000000000000000000..e9a98512823681750c9bdd5c79c8507ea2b9ce23 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/nick/Nick.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.nick; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.NICK) +public class Nick extends Extension { + + public Nick() { + super(Nick.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java b/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java new file mode 100644 index 0000000000000000000000000000000000000000..29ffc739f6b419b388929fdb53666aa5ec39e6e6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/occupant/OccupantId.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.occupant; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.OCCUPANT_ID) +public class OccupantId extends Extension { + + public OccupantId() { + super(OccupantId.class); + } + + public String getId() { + return Strings.emptyToNull(this.getAttribute("id")); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java b/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java new file mode 100644 index 0000000000000000000000000000000000000000..b324332a93828be7faa2aa5e7e8806e076e29e2f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/oob/OutOfBandData.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.oob; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class OutOfBandData extends Extension { + + public OutOfBandData() { + super(OutOfBandData.class); + } + + public String getURL() { + final URL url = this.getExtension(URL.class); + return url == null ? null : Strings.emptyToNull(url.getContent()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/URL.java b/src/main/java/im/conversations/android/xmpp/model/oob/URL.java new file mode 100644 index 0000000000000000000000000000000000000000..008b084800de7fb8fc65d2ef102154cdfb823bd3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/oob/URL.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.oob; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "url") +public class URL extends Extension { + + public URL() { + super(URL.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java b/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..aec4dee2466834735e150cce4569a1aac5fe4e33 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/oob/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.OOB) +package im.conversations.android.xmpp.model.oob; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java b/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java new file mode 100644 index 0000000000000000000000000000000000000000..d3d4b391034608fbcc7b3e52757f0b69403c5962 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pars/PreAuth.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.pars; + +import im.conversations.android.annotation.XmlElement; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.PARS) +public class PreAuth extends Extension { + + public PreAuth() { + super(PreAuth.class); + } + + public void setToken(final String token) { + this.setAttribute("token", token); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java b/src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java new file mode 100644 index 0000000000000000000000000000000000000000..43e4e2354b5e6be25564deb883dabab1287b7808 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pgp/Encrypted.java @@ -0,0 +1,14 @@ +package im.conversations.android.xmpp.model.pgp; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x", namespace = Namespace.PGP_ENCRYPTED) +public class Encrypted extends Extension { + + public Encrypted() { + super(Encrypted.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java b/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java new file mode 100644 index 0000000000000000000000000000000000000000..c75413972f521366f46f542d222f0d851cd7219c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pgp/Signed.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.pgp; + +import eu.siacs.conversations.xml.Namespace; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x",namespace = Namespace.PGP_SIGNED) +public class Signed extends Extension { + + + public Signed() { + super(Signed.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java b/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java new file mode 100644 index 0000000000000000000000000000000000000000..7f8f1c3a0505d65182c3e7ea05ecef51c8f20f19 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/ping/Ping.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.ping; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(namespace = Namespace.PING) +public class Ping extends Extension { + + public Ping() { + super(Ping.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..dbf2c3c239a6a9f2009c25fb2e28bb8e96e4e56f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Item.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.xmpp.model.Extension; + +public interface Item { + + T getExtension(final Class clazz); + + String getId(); +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..ceb1931ca35cbb1048e5c66f14cfe5ea9eff2303 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Items.java @@ -0,0 +1,52 @@ +package im.conversations.android.xmpp.model.pubsub; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.pubsub.event.Retract; +import java.util.Collection; +import java.util.Map; +import java.util.NoSuchElementException; + +public interface Items { + + Collection getItems(); + + String getNode(); + + Collection getRetractions(); + + default Map getItemMap(final Class clazz) { + final ImmutableMap.Builder builder = ImmutableMap.builder(); + for (final Item item : getItems()) { + final var id = item.getId(); + final T extension = item.getExtension(clazz); + if (extension == null || Strings.isNullOrEmpty(id)) { + continue; + } + builder.put(id, extension); + } + return builder.buildKeepingLast(); + } + + default T getItemOrThrow(final String id, final Class clazz) { + final var map = getItemMap(clazz); + final var item = map.get(id); + if (item == null) { + throw new NoSuchElementException( + String.format("An item with id %s does not exist", id)); + } + return item; + } + + default T getFirstItem(final Class clazz) { + final var map = getItemMap(clazz); + return Iterables.getFirst(map.values(), 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/PubSub.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java new file mode 100644 index 0000000000000000000000000000000000000000..a4fc1ee8ecc2f36ad5cc087373594fc4a6db3cb7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/PubSub.java @@ -0,0 +1,64 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.pubsub.event.Retract; +import java.util.Collection; + +@XmlElement(name = "pubsub") +public class PubSub extends Extension { + + public PubSub() { + super(PubSub.class); + } + + public Items getItems() { + return this.getExtension(ItemsWrapper.class); + } + + @XmlElement(name = "items") + public static class ItemsWrapper extends Extension implements Items { + + public ItemsWrapper() { + super(ItemsWrapper.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public Collection getItems() { + return this.getExtensions(Item.class); + } + + public Collection getRetractions() { + return this.getExtensions(Retract.class); + } + + public void setNode(String node) { + this.setAttribute("node", node); + } + + public void setMaxItems(final int maxItems) { + this.setAttribute("max_items", maxItems); + } + } + + @XmlElement(name = "item") + public static class Item extends Extension + implements im.conversations.android.xmpp.model.pubsub.Item { + + public Item() { + super(Item.class); + } + + @Override + public String getId() { + return this.getAttribute("id"); + } + + public void setId(String itemId) { + this.setAttribute("id", itemId); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java new file mode 100644 index 0000000000000000000000000000000000000000..7a384f5489745dad052c06c0d52806c2acc8a4e5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Publish.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Publish extends Extension { + + public Publish() { + super(Publish.class); + } + + public void setNode(String node) { + this.setAttribute("node", node); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..ec94f0604df8e042030d3ff4d7dbeaceebe08a75 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/PublishOptions.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.pubsub; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.NodeConfiguration; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; + +@XmlElement +public class PublishOptions extends Extension { + + public PublishOptions() { + super(PublishOptions.class); + } + + public static PublishOptions of(NodeConfiguration nodeConfiguration) { + final var publishOptions = new PublishOptions(); + publishOptions.addExtension(Data.of(Namespace.PUBSUB_PUBLISH_OPTIONS, nodeConfiguration)); + return publishOptions; + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java new file mode 100644 index 0000000000000000000000000000000000000000..309381197e0a5f226bba63fd80b7b3077ed03e60 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/Retract.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Retract extends Extension { + + public Retract() { + super(Retract.class); + } + + public void setNode(String node) { + this.setAttribute("node", node); + } + + public void setNotify(boolean notify) { + this.setAttribute("notify", notify ? 1 : 0); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java new file mode 100644 index 0000000000000000000000000000000000000000..a1c81a659d8c397d0fa977544fd2f89a2f0cfa59 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/PubSubError.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.pubsub.error; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public abstract class PubSubError extends Extension { + + private PubSubError(Class clazz) { + super(clazz); + } + + @XmlElement + public static class PreconditionNotMet extends PubSubError { + + public PreconditionNotMet() { + super(PreconditionNotMet.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..49d45f8c59e55b0022880a4e6609bf093fffe966 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/error/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB_ERROR) +package im.conversations.android.xmpp.model.pubsub.error; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; 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 new file mode 100644 index 0000000000000000000000000000000000000000..1e180c460053ee0bb74cbefa753c86b63f175e2f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Event.java @@ -0,0 +1,56 @@ +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.pubsub.Items; +import java.util.Collection; + +@XmlElement +public class Event extends Extension { + + public Event() { + super(Event.class); + } + + public Items getItems() { + return this.getExtension(ItemsWrapper.class); + } + + public Purge getPurge() { + return this.getExtension(Purge.class); + } + + @XmlElement(name = "items") + public static class ItemsWrapper extends Extension implements Items { + + public ItemsWrapper() { + super(ItemsWrapper.class); + } + + public String getNode() { + return this.getAttribute("node"); + } + + public Collection getItems() { + return this.getExtensions(Item.class); + } + + public Collection getRetractions() { + return this.getExtensions(Retract.class); + } + } + + @XmlElement(name = "item") + public static class Item extends Extension + implements im.conversations.android.xmpp.model.pubsub.Item { + + public Item() { + super(Item.class); + } + + @Override + public String getId() { + return this.getAttribute("id"); + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..64550e0b77f1ca33d57e2b501204995545bfcac6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Purge.java @@ -0,0 +1,16 @@ +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 Purge() { + super(Purge.class); + } + + public String getNode() { + return this.getAttribute("node"); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..139a49522c48cb74aa9b4469768baf43305fb446 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/Retract.java @@ -0,0 +1,16 @@ +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 Retract() { + super(Retract.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..223345c68b18021198477ec4168c2ef4cc0ddd9b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/event/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB_EVENT) +package im.conversations.android.xmpp.model.pubsub.event; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java new file mode 100644 index 0000000000000000000000000000000000000000..53b987f53c9c7fddbb2c3766e3731bd104722a67 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/Configure.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; + +@XmlElement +public class Configure extends Extension { + + public Configure() { + super(Configure.class); + } + + public void setNode(final String node) { + this.setAttribute("node", node); + } + + public Data getData() { + return this.getExtension(Data.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java new file mode 100644 index 0000000000000000000000000000000000000000..c3a61e6195750b2c42524d3e5309aa004918b083 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/PubSubOwner.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "pubsub") +public class PubSubOwner extends Extension { + + public PubSubOwner() { + super(PubSubOwner.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d3ecb89aa94dbbaba381de987db16a19d03c3747 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/owner/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB_OWNER) +package im.conversations.android.xmpp.model.pubsub.owner; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java b/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..a68a021fd5cbc339dd345d8e0121d44fe0f71541 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/pubsub/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.PUBSUB) +package im.conversations.android.xmpp.model.pubsub; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java b/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java new file mode 100644 index 0000000000000000000000000000000000000000..1d854a83a4def96a087612a5bf66621a22e20961 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/Reaction.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.reactions; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Reaction extends Extension { + + public Reaction() { + super(Reaction.class); + } + + public Reaction(final String reaction) { + this(); + setContent(reaction); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java b/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java new file mode 100644 index 0000000000000000000000000000000000000000..ec3ae989176bc25d1cef26cafa8137c8ef707d46 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/Reactions.java @@ -0,0 +1,36 @@ +package im.conversations.android.xmpp.model.reactions; + +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Reactions extends Extension { + + public Reactions() { + super(Reactions.class); + } + + public Collection getReactions() { + return Collections2.filter( + Collections2.transform(getExtensions(Reaction.class), Reaction::getContent), + r -> Objects.nonNull(Strings.nullToEmpty(r))); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setId(String id) { + this.setAttribute("id", id); + } + + public static Reactions to(final String id) { + final var reactions = new Reactions(); + reactions.setId(id); + return reactions; + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java b/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..bdb8a8dca23d7ba4a17dba9c09089793959780b2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/reactions/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.REACTIONS) +package im.conversations.android.xmpp.model.reactions; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; 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 new file mode 100644 index 0000000000000000000000000000000000000000..71fe922c158adeb678b30e6181047329e5dc0784 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/Received.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.receipts; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceipt; + +@XmlElement +public class Received extends DeliveryReceipt { + + public Received() { + super(Received.class); + } + + public void setId(String id) { + this.setAttribute("id", id); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java b/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java new file mode 100644 index 0000000000000000000000000000000000000000..684477af354890b8cc9ef7487e4850926925b8f6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/Request.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.receipts; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.DeliveryReceiptRequest; + +@XmlElement +public class Request extends DeliveryReceiptRequest { + + public Request() { + super(Request.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java b/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..8e3de2cad52e6ee3300ac4ea0e284cce1b8c40c0 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/receipts/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.DELIVERY_RECEIPTS) +package im.conversations.android.xmpp.model.receipts; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java b/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java new file mode 100644 index 0000000000000000000000000000000000000000..cd22f2a3a8427cd0d58ff511a6daa2d5525215c3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Instructions.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.xmpp.model.Extension; + +public class Instructions extends Extension { + + public Instructions() { + super(Instructions.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Password.java b/src/main/java/im/conversations/android/xmpp/model/register/Password.java new file mode 100644 index 0000000000000000000000000000000000000000..9da687c213e2cc26dfbc6eaec8710a84a8ff71f7 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Password.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.xmpp.model.Extension; + +public class Password extends Extension { + + public Password() { + super(Password.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Register.java b/src/main/java/im/conversations/android/xmpp/model/register/Register.java new file mode 100644 index 0000000000000000000000000000000000000000..4a48bd8d15283ab5d2f6cda5f0a782735d84cbf2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Register.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import org.jxmpp.jid.parts.Localpart; + +@XmlElement(name = "query") +public class Register extends Extension { + + public Register() { + super(Register.class); + } + + public void addUsername(final Localpart username) { + this.addExtension(new Username()).setContent(username.toString()); + } + + public void addPassword(final String password) { + this.addExtension(new Password()).setContent(password); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Remove.java b/src/main/java/im/conversations/android/xmpp/model/register/Remove.java new file mode 100644 index 0000000000000000000000000000000000000000..bbd327bfd66130779102cadb4817602e1979c40a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Remove.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.xmpp.model.Extension; + +public class Remove extends Extension { + + public Remove() { + super(Remove.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/Username.java b/src/main/java/im/conversations/android/xmpp/model/register/Username.java new file mode 100644 index 0000000000000000000000000000000000000000..bc93581b66937b358324193a00b9e4e85907aa8b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/Username.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Username extends Extension { + + public Username() { + super(Username.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/register/package-info.java b/src/main/java/im/conversations/android/xmpp/model/register/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..9e7a3e8f33d5fc7afe2eef429b23023e94e755c5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/register/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.REGISTER) +package im.conversations.android.xmpp.model.register; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Group.java b/src/main/java/im/conversations/android/xmpp/model/roster/Group.java new file mode 100644 index 0000000000000000000000000000000000000000..9f36efae7c7d6defa57a3b142a55725e2ae386df --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Group.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.roster; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Group extends Extension { + + public Group() { + super(Group.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java new file mode 100644 index 0000000000000000000000000000000000000000..0a2e0ef54add741e8141d333dc9d21e510e309ec --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java @@ -0,0 +1,61 @@ +package im.conversations.android.xmpp.model.roster; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +@XmlElement +public class Item extends Extension { + + public static final List RESULT_SUBSCRIPTIONS = + Arrays.asList(Subscription.NONE, Subscription.TO, Subscription.FROM, Subscription.BOTH); + + public Item() { + super(Item.class); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } + + public String getItemName() { + return this.getAttribute("name"); + } + + public boolean isPendingOut() { + return "subscribe".equalsIgnoreCase(this.getAttribute("ask")); + } + + public Subscription getSubscription() { + final String value = this.getAttribute("subscription"); + try { + return value == null ? null : Subscription.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + + public Collection getGroups() { + return Collections2.filter( + Collections2.transform(getExtensions(Group.class), Element::getContent), + Objects::nonNull); + } + + public enum Subscription { + NONE, + TO, + FROM, + BOTH, + REMOVE + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java new file mode 100644 index 0000000000000000000000000000000000000000..616f6ae0b68cd6a13fdc9096d18df47f5852a8fb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.roster; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "query", namespace = Namespace.ROSTER) +public class Query extends Extension { + + public Query() { + super(Query.class); + } + + public void setVersion(final String rosterVersion) { + this.setAttribute("ver", rosterVersion); + } + + public String getVersion() { + return this.getAttribute("ver"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java b/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..eea0703fd6f0ea886d484914a512398c1b6713d5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/roster/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.ROSTER) +package im.conversations.android.xmpp.model.roster; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/After.java b/src/main/java/im/conversations/android/xmpp/model/rsm/After.java new file mode 100644 index 0000000000000000000000000000000000000000..90179bff0a1af8f678196858701e4df7f1a7916f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/After.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class After extends Extension { + + public After() { + super(After.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java new file mode 100644 index 0000000000000000000000000000000000000000..c3c6ac1a89a3c07b1b10e4d77d25d39b06602ab4 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Before extends Extension { + + public Before() { + super(Before.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java new file mode 100644 index 0000000000000000000000000000000000000000..c54f9d5e08fc4f8980712bfbebaed0a4d0e5f49e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.rsm; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Count extends Extension { + + public Count() { + super(Count.class); + } + + public Integer getCount() { + final var content = getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } else { + return Ints.tryParse(content); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/First.java b/src/main/java/im/conversations/android/xmpp/model/rsm/First.java new file mode 100644 index 0000000000000000000000000000000000000000..b976632e43eb993c3b06bf8e4231748f1c1190c1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/First.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class First extends Extension { + + public First() { + super(First.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java new file mode 100644 index 0000000000000000000000000000000000000000..01d53e07304c62e7846fc70414f146e1bceb2cbd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Last extends Extension { + + public Last() { + super(Last.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java new file mode 100644 index 0000000000000000000000000000000000000000..06908be8b8c761284c65762541794ef7f1e83757 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Max extends Extension { + + public Max() { + super(Max.class); + } + + public void setMax(final int max) { + this.setContent(String.valueOf(max)); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java b/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java new file mode 100644 index 0000000000000000000000000000000000000000..6f428565c47e7aef7fe9d013d64ef524b963e87d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java @@ -0,0 +1,55 @@ +package im.conversations.android.xmpp.model.rsm; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.Page; +import im.conversations.android.xmpp.Range; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Set extends Extension { + + public Set() { + super(Set.class); + } + + public static Set of(final Range range, final Integer max) { + final var set = new Set(); + if (range.order == Range.Order.NORMAL) { + final var after = set.addExtension(new After()); + after.setContent(range.id); + } else if (range.order == Range.Order.REVERSE) { + final var before = set.addExtension(new Before()); + before.setContent(range.id); + } else { + throw new IllegalArgumentException("Invalid order"); + } + if (max != null) { + set.addExtension(new Max()).setMax(max); + } + return set; + } + + public Page asPage() { + final var first = this.getExtension(First.class); + final var last = this.getExtension(Last.class); + + final var firstId = first == null ? null : first.getContent(); + final var lastId = last == null ? null : last.getContent(); + if (Strings.isNullOrEmpty(firstId) || Strings.isNullOrEmpty(lastId)) { + throw new IllegalStateException("Invalid page. Missing first or last"); + } + return new Page(firstId, lastId, this.getCount()); + } + + public boolean isEmpty() { + final var first = this.getExtension(First.class); + final var last = this.getExtension(Last.class); + return first == null && last == null; + } + + public Integer getCount() { + final var count = this.getExtension(Count.class); + return count == null ? null : count.getCount(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java b/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..c00fd37c9bd4f6a8544e25651780b4365c6b75cc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.RESULT_SET_MANAGEMENT) +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java new file mode 100644 index 0000000000000000000000000000000000000000..e9dd801f233ef6db23d3781b6724a6ab1e536c3a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Auth.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.sasl; + +import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationRequest; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Auth extends AuthenticationRequest { + + public Auth() { + super(Auth.class); + } + + @Override + public void setMechanism(final SaslMechanism mechanism) { + this.setAttribute("mechanism", mechanism.getMechanism()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java new file mode 100644 index 0000000000000000000000000000000000000000..1db7029b502ed50541e4fabd8b5fb0424f4f2f32 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Failure.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationFailure; + +@XmlElement +public class Failure extends AuthenticationFailure { + public Failure() { + super(Failure.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java new file mode 100644 index 0000000000000000000000000000000000000000..e23087d8924afb6de4747b58c012527da85bbcfc --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanism.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Mechanism extends Extension { + + public Mechanism() { + super(Mechanism.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java new file mode 100644 index 0000000000000000000000000000000000000000..7612ba3583e0e7a3576198f682d556f083366b65 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Mechanisms.java @@ -0,0 +1,29 @@ +package im.conversations.android.xmpp.model.sasl; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.StreamFeature; + +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Mechanisms extends AuthenticationStreamFeature { + + + public Mechanisms() { + super(Mechanisms.class); + } + + public Collection getMechanisms() { + return getExtensions(Mechanism.class); + } + + public Collection getMechanismNames() { + return Collections2.filter(Collections2.transform(getMechanisms(), Element::getContent), Objects::nonNull); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Response.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Response.java new file mode 100644 index 0000000000000000000000000000000000000000..5e2ab626e1ca067204e9bdb25fd19b1a15c72d9a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Response.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Response extends StreamElement { + + public Response() { + super(Response.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/SaslError.java b/src/main/java/im/conversations/android/xmpp/model/sasl/SaslError.java new file mode 100644 index 0000000000000000000000000000000000000000..54a26709bc37b03c15cd6c8076ce7d982f96a74d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/SaslError.java @@ -0,0 +1,89 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +public class SaslError extends Extension { + + private SaslError(final Class clazz) { + super(clazz); + } + + @XmlElement + public static class Aborted extends SaslError { + public Aborted() { + super(Aborted.class); + } + } + + @XmlElement + public static class AccountDisabled extends SaslError { + public AccountDisabled() { + super(AccountDisabled.class); + } + } + + @XmlElement + public static class CredentialsExpired extends SaslError { + public CredentialsExpired() { + super(CredentialsExpired.class); + } + } + + @XmlElement + public static class EncryptionRequired extends SaslError { + public EncryptionRequired() { + super(EncryptionRequired.class); + } + } + + @XmlElement + public static class IncorrectEncoding extends SaslError { + public IncorrectEncoding() { + super(IncorrectEncoding.class); + } + } + + @XmlElement + public static class InvalidAuthzid extends SaslError { + public InvalidAuthzid() { + super(InvalidAuthzid.class); + } + } + + @XmlElement + public static class InvalidMechanism extends SaslError { + public InvalidMechanism() { + super(InvalidMechanism.class); + } + } + + @XmlElement + public static class MalformedRequest extends SaslError { + public MalformedRequest() { + super(MalformedRequest.class); + } + } + + @XmlElement + public static class MechanismTooWeak extends SaslError { + public MechanismTooWeak() { + super(MechanismTooWeak.class); + } + } + + @XmlElement + public static class NotAuthorized extends SaslError { + + public NotAuthorized() { + super(NotAuthorized.class); + } + } + + @XmlElement + public static class TemporaryAuthFailure extends SaslError { + public TemporaryAuthFailure() { + super(TemporaryAuthFailure.class); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/Success.java b/src/main/java/im/conversations/android/xmpp/model/sasl/Success.java new file mode 100644 index 0000000000000000000000000000000000000000..d7323e4789e7afebd17ed31b95070e4e6a710397 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/Success.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.sasl; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Success extends StreamElement { + + + public Success() { + super(Success.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java b/src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..3b0de4f4a8ba4a30ce6852b0b2de9c983113b0ff --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.SASL) +package im.conversations.android.xmpp.model.sasl; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java new file mode 100644 index 0000000000000000000000000000000000000000..ac99ed346d49d0c15f3601b772f28c5be07431fb --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authenticate.java @@ -0,0 +1,19 @@ +package im.conversations.android.xmpp.model.sasl2; + +import eu.siacs.conversations.crypto.sasl.SaslMechanism; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationRequest; + +@XmlElement +public class Authenticate extends AuthenticationRequest { + + public Authenticate() { + super(Authenticate.class); + } + + @Override + public void setMechanism(final SaslMechanism mechanism) { + this.setAttribute("mechanism", mechanism.getMechanism()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java new file mode 100644 index 0000000000000000000000000000000000000000..ad26d37e6ea9d06fb8f3363fa3a39d1e5de435d2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Authentication.java @@ -0,0 +1,30 @@ +package im.conversations.android.xmpp.model.sasl2; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationStreamFeature; +import im.conversations.android.xmpp.model.StreamFeature; + +import java.util.Collection; +import java.util.Objects; + +@XmlElement +public class Authentication extends AuthenticationStreamFeature { + public Authentication() { + super(Authentication.class); + } + + public Collection getMechanisms() { + return getExtensions(Mechanism.class); + } + + public Collection getMechanismNames() { + return Collections2.filter(Collections2.transform(getMechanisms(), Element::getContent), Objects::nonNull); + } + + public Inline getInline() { + return this.getExtension(Inline.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java new file mode 100644 index 0000000000000000000000000000000000000000..e29ae7dea3ced429d4e25d17f1fe1954017b5d98 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/AuthorizationIdentifier.java @@ -0,0 +1,28 @@ +package im.conversations.android.xmpp.model.sasl2; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class AuthorizationIdentifier extends Extension { + + + public AuthorizationIdentifier() { + super(AuthorizationIdentifier.class); + } + + public Jid get() { + final var content = getContent(); + if ( Strings.isNullOrEmpty(content)) { + return null; + } + try { + return Jid.ofEscaped(content); + } catch (final IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java new file mode 100644 index 0000000000000000000000000000000000000000..2594f5874b09baaadd100486d8e3b492db949f41 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Device.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Device extends Extension { + + public Device() { + super(Device.class); + } + + public Device(final String device) { + this(); + this.setContent(device); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java new file mode 100644 index 0000000000000000000000000000000000000000..bb0e327d9b28e624bbaa6ec6863c6dc8e2672fc5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Failure.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.AuthenticationFailure; + +@XmlElement +public class Failure extends AuthenticationFailure { + + public Failure() { + super(Failure.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java new file mode 100644 index 0000000000000000000000000000000000000000..6a6ad0dd8b1fa526a5015eb0dcaca4d475586504 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Inline.java @@ -0,0 +1,34 @@ +package im.conversations.android.xmpp.model.sasl2; + +import com.google.common.collect.Collections2; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.fast.Fast; +import im.conversations.android.xmpp.model.fast.Mechanism; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +@XmlElement +public class Inline extends Extension { + + public Inline() { + super(Inline.class); + } + + public Fast getFast() { + return this.getExtension(Fast.class); + } + + public Collection getFastMechanisms() { + final var fast = getFast(); + final Collection mechanisms = + fast == null ? Collections.emptyList() : fast.getExtensions(Mechanism.class); + return Collections2.filter( + Collections2.transform(mechanisms, Element::getContent), Objects::nonNull); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java new file mode 100644 index 0000000000000000000000000000000000000000..d0a615777be37c9ba979e4db03879954ebe95a1e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Mechanism.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Mechanism extends Extension { + + public Mechanism() { + super(Mechanism.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java new file mode 100644 index 0000000000000000000000000000000000000000..91f1b7dab6a6e2dc48c40d8e9638085b9ed4404b --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Response.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Response extends StreamElement { + + public Response() { + super(Response.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java new file mode 100644 index 0000000000000000000000000000000000000000..8685ed3ff63285ecba2e012f0294cf6016c41017 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Software.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Software extends Extension { + + public Software() { + super(Software.class); + } + + public Software(final String software) { + this(); + this.setContent(software); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java new file mode 100644 index 0000000000000000000000000000000000000000..17673b35a7035d6229e1d808d336839b05b43aa3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/Success.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.sasl2; + +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Success extends StreamElement { + + + public Success() { + super(Success.class); + } + + public Jid getAuthorizationIdentifier() { + final var id = this.getExtension(AuthorizationIdentifier.class); + if (id == null) { + return null; + } + return id.get(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java new file mode 100644 index 0000000000000000000000000000000000000000..bb2a0c68cfae29d8fc02c2eed963b3fe82075b61 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/UserAgent.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.sasl2; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class UserAgent extends Extension { + + public UserAgent() { + super(UserAgent.class); + } + + public UserAgent(final String userAgentId) { + this(); + this.setAttribute("id", userAgentId); + } + + public void setSoftware(final String software) { + this.addExtension(new Software(software)); + } + + public void setDevice(final String device) { + this.addExtension(new Device(device)); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java b/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..10a61d1098dcaded964807e1610ec2d1f3b46336 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sasl2/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.SASL_2) +package im.conversations.android.xmpp.model.sasl2; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java b/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java new file mode 100644 index 0000000000000000000000000000000000000000..5cafc8c1d41a53281c2e27cde0503ced3c0d880a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Ack.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "a") +public class Ack extends StreamElement { + + public Ack() { + super(Ack.class); + } + + public Ack(final int sequence) { + super(Ack.class); + this.setAttribute("h", sequence); + } + + public Optional getHandled() { + return this.getOptionalIntAttribute("h"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java b/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java new file mode 100644 index 0000000000000000000000000000000000000000..9b80a93baa0f565c05b2cbe7df7146cf396397dd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Enable.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Enable extends StreamElement { + + public Enable() { + super(Enable.class); + this.setAttribute("resume", "true"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java b/src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java new file mode 100644 index 0000000000000000000000000000000000000000..b900d435cd81b47e0e36b2a94c1efc01dc2e03ff --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Enabled.java @@ -0,0 +1,35 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; +import com.google.common.base.Strings; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Enabled extends StreamElement { + + public Enabled() { + super(Enabled.class); + } + + public boolean isResume() { + return this.getAttributeAsBoolean("resume"); + } + + public String getLocation() { + return this.getAttribute("location"); + } + + public Optional getResumeId() { + final var id = this.getAttribute("id"); + if (Strings.isNullOrEmpty(id)) { + return Optional.absent(); + } + if (isResume()) { + return Optional.of(id); + } else { + return Optional.absent(); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Failed.java b/src/main/java/im/conversations/android/xmpp/model/sm/Failed.java new file mode 100644 index 0000000000000000000000000000000000000000..1e15bfe6c0b46fa97d2ddc4b7bcbe4243ed19217 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Failed.java @@ -0,0 +1,17 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Failed extends StreamElement { + public Failed() { + super(Failed.class); + } + + public Optional getHandled() { + return this.getOptionalIntAttribute("h"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Request.java b/src/main/java/im/conversations/android/xmpp/model/sm/Request.java new file mode 100644 index 0000000000000000000000000000000000000000..ad1de61bc827995471cde86b6d437cf81ad3db1c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Request.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "r") +public class Request extends StreamElement { + + public Request() { + super(Request.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java b/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java new file mode 100644 index 0000000000000000000000000000000000000000..e47b19966ca7b344296c6580797d639a7996ff83 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Resume.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Resume extends StreamElement { + + public Resume() { + super(Resume.class); + } + + public Resume(final String id, final int sequence) { + super(Resume.class); + this.setAttribute("previd", id); + this.setAttribute("h", sequence); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java b/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java new file mode 100644 index 0000000000000000000000000000000000000000..eb240745fd4b1402e96c838ef71a868f1d7dcf68 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.sm; + +import com.google.common.base.Optional; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Resumed extends StreamElement { + + public Resumed() { + super(Resumed.class); + } + + public Optional getHandled() { + return this.getOptionalIntAttribute("h"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java b/src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java new file mode 100644 index 0000000000000000000000000000000000000000..48103755a14d5fc335808ff76972e57e02d26048 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/StreamManagement.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.sm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; + +@XmlElement(name = "sm") +public class StreamManagement extends StreamFeature { + + public StreamManagement() { + super(StreamManagement.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java b/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..dd2e036fcc2527fbca9acd8283189f9852f4b133 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/sm/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STREAM_MANAGEMENT) +package im.conversations.android.xmpp.model.sm; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java new file mode 100644 index 0000000000000000000000000000000000000000..9f94400c32692b9a734c5d503e98d3151738d532 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Iq.java @@ -0,0 +1,77 @@ +package im.conversations.android.xmpp.model.stanza; + +import com.google.common.base.Strings; + +import eu.siacs.conversations.xml.Element; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.error.Error; + +import java.util.Locale; + +@XmlElement +public class Iq extends Stanza { + + public static Iq TIMEOUT = new Iq(Type.TIMEOUT); + + public Iq() { + super(Iq.class); + } + + public Iq(final Type type) { + super(Iq.class); + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + + // TODO get rid of timeout + public enum Type { + SET, + GET, + ERROR, + RESULT, + TIMEOUT + } + + public Type getType() { + return Type.valueOf( + Strings.nullToEmpty(this.getAttribute("type")).toUpperCase(Locale.ROOT)); + } + + @Override + public boolean isInvalid() { + final var id = getId(); + if (Strings.isNullOrEmpty(id)) { + return true; + } + return super.isInvalid(); + } + + // Legacy methods that need to be refactored: + + public Element query() { + final Element query = findChild("query"); + if (query != null) { + return query; + } + return addChild("query"); + } + + public Element query(final String xmlns) { + final Element query = query(); + query.setAttribute("xmlns", xmlns); + return query(); + } + + public Iq generateResponse(final Iq.Type type) { + final var packet = new Iq(type); + packet.setTo(this.getFrom()); + packet.setId(this.getId()); + return packet; + } + + public String getErrorCondition() { + final Error error = getError(); + final var condition = error == null ? null : error.getCondition(); + return condition == null ? null : condition.getName(); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..9b12bffb38f25467b5b4880829b7ce0f358fa858 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Message.java @@ -0,0 +1,64 @@ +package im.conversations.android.xmpp.model.stanza; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.LocalizedContent; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.jabber.Body; + +import java.util.Locale; + +@XmlElement +public class Message extends Stanza { + + public Message() { + super(Message.class); + } + + public Message(Type type) { + this(); + this.setType(type); + } + + public LocalizedContent getBody() { + return findInternationalizedChildContentInDefaultNamespace("body"); + } + + public Type getType() { + final var value = this.getAttribute("type"); + if (value == null) { + return Type.NORMAL; + } else { + try { + return Type.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + } + + public void setType(final Type type) { + if (type == null || type == Type.NORMAL) { + this.removeAttribute("type"); + } else { + this.setAttribute("type", type.toString().toLowerCase(Locale.ROOT)); + } + } + + public void setBody(final String text) { + this.addExtension(new Body(text)); + } + + public void setAxolotlMessage(Element axolotlMessage) { + removeChild(findChild("body")); + prependChild(axolotlMessage); + } + + public enum Type { + ERROR, + NORMAL, + GROUPCHAT, + HEADLINE, + CHAT + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java new file mode 100644 index 0000000000000000000000000000000000000000..129660b000cd1565956f10703afe828599eb8bf8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.stanza; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; + +@XmlElement +public class Presence extends Stanza implements EntityCapabilities { + + public Presence() { + super(Presence.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java new file mode 100644 index 0000000000000000000000000000000000000000..82a8ce3dfbfa1f225e6030ace9f746cdff09c9cd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java @@ -0,0 +1,74 @@ +package im.conversations.android.xmpp.model.stanza; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xmpp.InvalidJid; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.error.Error; + +public abstract class Stanza extends StreamElement { + + protected Stanza(final Class clazz) { + super(clazz); + } + + public Jid getTo() { + return this.getAttributeAsJid("to"); + } + + public Jid getFrom() { + return this.getAttributeAsJid("from"); + } + + public String getId() { + return this.getAttribute("id"); + } + + public void setId(final String id) { + this.setAttribute("id", id); + } + + public void setFrom(final Jid from) { + this.setAttribute("from", from); + } + + public void setTo(final Jid to) { + this.setAttribute("to", to); + } + + public Error getError() { + return this.getExtension(Error.class); + } + + public boolean isInvalid() { + final var to = getTo(); + final var from = getFrom(); + if (to instanceof InvalidJid || from instanceof InvalidJid) { + return true; + } + return false; + } + + public boolean fromServer(final Account account) { + final Jid from = getFrom(); + return from == null + || from.equals(account.getDomain()) + || from.equals(account.getJid().asBareJid()) + || from.equals(account.getJid()); + } + + public boolean toServer(final Account account) { + final Jid to = getTo(); + return to == null + || to.equals(account.getDomain()) + || to.equals(account.getJid().asBareJid()) + || to.equals(account.getJid()); + } + + public boolean fromAccount(final Account account) { + final Jid from = getFrom(); + return from != null && from.asBareJid().equals(account.getJid().asBareJid()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java b/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..d12fe56dbe53003ddbd8a9d81bc960d1cea69da9 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.JABBER_CLIENT) +package im.conversations.android.xmpp.model.stanza; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Active.java b/src/main/java/im/conversations/android/xmpp/model/state/Active.java new file mode 100644 index 0000000000000000000000000000000000000000..15970bc5bf5eb6ea9d7bf21d319d22d2d637ddaf --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Active.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Active extends ChatStateNotification { + + public Active() { + super(Active.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java b/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java new file mode 100644 index 0000000000000000000000000000000000000000..642ed519d15a34e6d368086ecf0f49d53303c1a2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/ChatStateNotification.java @@ -0,0 +1,10 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.xmpp.model.Extension; + +public abstract class ChatStateNotification extends Extension { + + protected ChatStateNotification(Class clazz) { + super(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Composing.java b/src/main/java/im/conversations/android/xmpp/model/state/Composing.java new file mode 100644 index 0000000000000000000000000000000000000000..9871952e0de7d631364f53f31e8e289c052e39ba --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Composing.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Composing extends ChatStateNotification { + + public Composing() { + super(Composing.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Gone.java b/src/main/java/im/conversations/android/xmpp/model/state/Gone.java new file mode 100644 index 0000000000000000000000000000000000000000..a0a74e788c99fa10810afa00f6d11b70538d52ea --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Gone.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Gone extends ChatStateNotification { + + public Gone() { + super(Gone.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java b/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java new file mode 100644 index 0000000000000000000000000000000000000000..4a3670308ac0067d55cb4c6e0009d98514765382 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Inactive.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Inactive extends ChatStateNotification { + + public Inactive() { + super(Inactive.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/Paused.java b/src/main/java/im/conversations/android/xmpp/model/state/Paused.java new file mode 100644 index 0000000000000000000000000000000000000000..f97f3e5045b54e4a318cdf5843766f6175bb40da --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/Paused.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlElement; + +@XmlElement +public class Paused extends ChatStateNotification { + + public Paused() { + super(Paused.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/state/package-info.java b/src/main/java/im/conversations/android/xmpp/model/state/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..a0cc97debfa49a575c48b8c6f5dd53086a75b52e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/state/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CHAT_STATES) +package im.conversations.android.xmpp.model.state; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/Features.java b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java new file mode 100644 index 0000000000000000000000000000000000000000..0597c2241cfb809a46d7747ce309fc01a01fe15c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/Features.java @@ -0,0 +1,33 @@ +package im.conversations.android.xmpp.model.streams; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; +import im.conversations.android.xmpp.model.StreamFeature; +import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.sm.StreamManagement; + +@XmlElement +public class Features extends StreamElement implements EntityCapabilities { + public Features() { + super(Features.class); + } + + public boolean streamManagement() { + return hasStreamFeature(StreamManagement.class); + } + + public boolean invite() { + return this.hasChild("register", Namespace.INVITE); + } + + public boolean clientStateIndication() { + return this.hasChild("csi", Namespace.CSI); + } + + + public boolean hasStreamFeature(final Class clazz) { + return hasExtension(clazz); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java b/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..56900532c47a7a502beada11ecb22577a0583a07 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STREAMS) +package im.conversations.android.xmpp.model.streams; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java b/src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java new file mode 100644 index 0000000000000000000000000000000000000000..3e2cf454c9c3df9ec8b6b6b71e74648daaddc8e8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/Proceed.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.tls; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement +public class Proceed extends StreamElement { + + public Proceed() { + super(Proceed.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/Required.java b/src/main/java/im/conversations/android/xmpp/model/tls/Required.java new file mode 100644 index 0000000000000000000000000000000000000000..60f4652ba8b6293b6ecbb610a37e75c5815b0d4a --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/Required.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.tls; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Required extends Extension { + public Required() { + super(Required.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java b/src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java new file mode 100644 index 0000000000000000000000000000000000000000..337371c7b6aef6fef4b2d69faa1725091db20bd2 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/StartTls.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.tls; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "starttls") +public class StartTls extends StreamElement { + public StartTls() { + super(StartTls.class); + } + + public boolean isRequired() { + return hasExtension(Required.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/tls/package-info.java b/src/main/java/im/conversations/android/xmpp/model/tls/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..de3ed3ecdbc1274997673d6aef30d8042ab78540 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/tls/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.TLS) +package im.conversations.android.xmpp.model.tls; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlPackage; \ No newline at end of file diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java b/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java new file mode 100644 index 0000000000000000000000000000000000000000..31a93962104ef834cae83978ce89d65946fb61b8 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.unique; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class OriginId extends Extension { + + public OriginId() { + super(OriginId.class); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..23b0fdcac823de630719bf14f8c47a559a2fbcfd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/unique/StanzaId.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.unique; + +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class StanzaId extends Extension { + + public StanzaId() { + super(StanzaId.class); + } + + public Jid getBy() { + return this.getAttributeAsJid("by"); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java b/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..31209ee24f25dc21e2c57aaad97c54fb38ae6066 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/unique/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.STANZA_IDS) +package im.conversations.android.xmpp.model.unique; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Get.java b/src/main/java/im/conversations/android/xmpp/model/upload/Get.java new file mode 100644 index 0000000000000000000000000000000000000000..5fad9afd409fe0d21d3f8a17cc68c7f7a22d5de1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Get.java @@ -0,0 +1,22 @@ +package im.conversations.android.xmpp.model.upload; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import okhttp3.HttpUrl; + +@XmlElement +public class Get extends Extension { + + public Get() { + super(Get.class); + } + + public HttpUrl getUrl() { + final var url = this.getAttribute("url"); + if (Strings.isNullOrEmpty(url)) { + return null; + } + return HttpUrl.parse(url); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Header.java b/src/main/java/im/conversations/android/xmpp/model/upload/Header.java new file mode 100644 index 0000000000000000000000000000000000000000..00546d0d984f66542270ac645f9bf094035f74b1 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Header.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Header extends Extension { + + public Header() { + super(Header.class); + } + + public String getHeaderName() { + return this.getAttribute("name"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Put.java b/src/main/java/im/conversations/android/xmpp/model/upload/Put.java new file mode 100644 index 0000000000000000000000000000000000000000..1b52a495c551b4acb34778f09c45aff11999ed12 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Put.java @@ -0,0 +1,27 @@ +package im.conversations.android.xmpp.model.upload; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import java.util.Collection; +import okhttp3.HttpUrl; + +@XmlElement +public class Put extends Extension { + + public Put() { + super(Put.class); + } + + public HttpUrl getUrl() { + final var url = this.getAttribute("url"); + if (Strings.isNullOrEmpty(url)) { + return null; + } + return HttpUrl.parse(url); + } + + public Collection
getHeaders() { + return this.getExtensions(Header.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Request.java b/src/main/java/im/conversations/android/xmpp/model/upload/Request.java new file mode 100644 index 0000000000000000000000000000000000000000..bbf8a98c1632aca15b6f39015448b1b3a611777d --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Request.java @@ -0,0 +1,24 @@ +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Request extends Extension { + + public Request() { + super(Request.class); + } + + public void setFilename(String filename) { + this.setAttribute("filename", filename); + } + + public void setSize(long size) { + this.setAttribute("size", size); + } + + public void setContentType(String type) { + this.setAttribute("content-ype", type); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/Slot.java b/src/main/java/im/conversations/android/xmpp/model/upload/Slot.java new file mode 100644 index 0000000000000000000000000000000000000000..df90157812be59d01d54ceca65ee47b7d9e764ff --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/Slot.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Slot extends Extension { + + public Slot() { + super(Slot.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/upload/package-info.java b/src/main/java/im/conversations/android/xmpp/model/upload/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..e4ccf3d8dc5087ad0c9eb589ba05a2f5ad9ec1ca --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/upload/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.HTTP_UPLOAD) +package im.conversations.android.xmpp.model.upload; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java b/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java new file mode 100644 index 0000000000000000000000000000000000000000..273dcfb25f8265d127db82c9d837a175e0d026ce --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/BinaryValue.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.ByteContent; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "BINVAL") +public class BinaryValue extends Extension implements ByteContent { + + public BinaryValue() { + super(BinaryValue.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java new file mode 100644 index 0000000000000000000000000000000000000000..92adc6831c5346cc15a1d2a5663341e29497e2c5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/Photo.java @@ -0,0 +1,11 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "PHOTO") +public class Photo extends Extension { + public Photo() { + super(Photo.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java new file mode 100644 index 0000000000000000000000000000000000000000..20a6949775b4348d1c703d3362e570ff98b4f07c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/VCard.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "vCard") +public class VCard extends Extension { + + public VCard() { + super(VCard.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java b/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..7ee576ca27cc6113a5e8ab2a3dad72c1e8aa2339 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.VCARD_TEMP) +package im.conversations.android.xmpp.model.vcard; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java new file mode 100644 index 0000000000000000000000000000000000000000..cb1f86d053eeba029c5339922bb63f614ed35f32 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/Photo.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Photo extends Extension { + + public Photo() { + super(Photo.class); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java new file mode 100644 index 0000000000000000000000000000000000000000..0be3f94b9fef7428b4977ebde3cdb728f55b1e7e --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/VCardUpdate.java @@ -0,0 +1,21 @@ +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement(name = "x") +public class VCardUpdate extends Extension { + + public VCardUpdate() { + super(VCardUpdate.class); + } + + public Photo getPhoto() { + return this.getExtension(Photo.class); + } + + public String getHash() { + final var photo = getPhoto(); + return photo == null ? null : photo.getContent(); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java b/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..efed1536072f01dbe4279eafffe010740dda8ee6 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/vcard/update/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.VCARD_TEMP_UPDATE) +package im.conversations.android.xmpp.model.vcard.update; + +import im.conversations.android.annotation.XmlPackage; +import eu.siacs.conversations.xml.Namespace; diff --git a/src/main/java/im/conversations/android/xmpp/model/version/Version.java b/src/main/java/im/conversations/android/xmpp/model/version/Version.java new file mode 100644 index 0000000000000000000000000000000000000000..7cbd5d22a805a46e2763d7425502d83e862790bd --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/version/Version.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.version; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; +import eu.siacs.conversations.xml.Namespace; + +@XmlElement(name = "query", namespace = Namespace.VERSION) +public class Version extends Extension { + + public Version() { + super(Version.class); + } + + public void setSoftwareName(final String name) { + this.addChild("name").setContent(name); + } + + public void setVersion(final String version) { + this.addChild("version").setContent(version); + } + + public void setOs(final String os) { + this.addChild("os").setContent(os); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..bc8097fda6620d6f7a1e51cab4e529fb837eab28 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -0,0 +1,90 @@ +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 { + + + private final XmppConnectionService service; + private final Account account; + + public BindProcessor(XmppConnectionService service, Account account) { + this.service = service; + this.account = account; + } + + @Override + public void run() { + final XmppConnection connection = account.getXmppConnection(); + service.cancelAvatarFetches(account); + final boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true); + final boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, connection.getFeatures().httpUpload(0)); + if (loggedInSuccessfully || gainedFeature) { + service.databaseBackend.updateAccount(account); + } + + if (loggedInSuccessfully) { + if (!TextUtils.isEmpty(account.getDisplayName())) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing"); + service.publishDisplayName(account); + } + } + + account.getRoster().clearPresences(); + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.clear(); + } + synchronized (account.inProgressConferencePings) { + account.inProgressConferencePings.clear(); + } + service.getJingleConnectionManager().notifyRebound(account); + service.getQuickConversationsService().considerSyncBackground(false); + + + connection.fetchRoster(); + + if (connection.getFeatures().bookmarks2()) { + service.fetchBookmarks2(account); + } else if (!connection.getFeatures().bookmarksConversion()) { + service.fetchBookmarks(account); + } + + if (connection.getFeatures().mds()) { + service.fetchMessageDisplayedSynchronization(account); + } else { + Log.d(Config.LOGTAG,account.getJid()+": server has no support for mds"); + } + final boolean flexible = connection.getFeatures().flexibleOfflineMessageRetrieval(); + final boolean catchup = service.getMessageArchiveService().inCatchup(account); + final boolean trackOfflineMessageRetrieval; + if (flexible && catchup && connection.isMamPreferenceAlways()) { + trackOfflineMessageRetrieval = false; + connection.sendIqPacket(IqGenerator.purgeOfflineMessages(), (packet) -> { + if (packet.getType() == Iq.Type.RESULT) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully purged offline messages"); + } + }); + } else { + trackOfflineMessageRetrieval = true; + } + service.sendPresence(account); + connection.trackOfflineMessageRetrieval(trackOfflineMessageRetrieval); + if (service.getPushManagementService().available(account)) { + service.getPushManagementService().registerPushTokenOnServer(account); + } + service.connectMultiModeConversations(account); + service.syncDirtyContacts(account); + + service.getUnifiedPushBroker().renewUnifiedPushEndpointsOnBind(account); + + } +} diff --git a/src/main/res/drawable/ic_new_releases_24dp.xml b/src/main/res/drawable/ic_new_releases_24dp.xml index dd9a4b95d7b299264b79cc66471c06c68f48b523..b0ffc3f4273ed4d81c23ad8c1c87c598d5906612 100644 --- a/src/main/res/drawable/ic_new_releases_24dp.xml +++ b/src/main/res/drawable/ic_new_releases_24dp.xml @@ -1,5 +1,10 @@ - - + + diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 07431bfe2bd86225d527d0a2ee737e8d08ef89e4..9de506d56dca7cd23cc54b13ad78cc9bc9f9ff33 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -107,8 +107,8 @@ android:id="@+id/hostname_layout" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="0.7" android:layout_marginEnd="4sp" + android:layout_weight="0.7" android:hint="@string/account_settings_hostname"> + android:visibility="gone" + tools:visibility="visible"> + android:visibility="gone" + tools:visibility="visible"> + + + + + + + + + + + @@ -272,8 +303,7 @@ android:id="@+id/server_info_pep" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -294,8 +324,7 @@ android:id="@+id/server_info_blocking" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -316,8 +345,7 @@ android:id="@+id/server_info_sm" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -338,8 +366,7 @@ android:id="@+id/server_info_external_service" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -360,8 +387,7 @@ android:id="@+id/server_info_roster_version" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -382,8 +408,7 @@ android:id="@+id/server_info_carbons" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -404,8 +429,7 @@ android:id="@+id/server_info_mam" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -426,8 +450,7 @@ android:id="@+id/server_info_csi" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" tools:ignore="RtlHardcoded" /> @@ -449,8 +472,7 @@ android:id="@+id/server_info_push" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" - android:paddingLeft="4dp" + android:layout_gravity="end" android:textAppearance="?textAppearanceBodyMedium" /> @@ -459,7 +481,6 @@ android:layout_height="wrap_content"> + + + + + + + + + + + + + + + @@ -728,6 +790,7 @@ android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/delete_pgp_key" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_delete_24dp" android:visibility="visible" /> @@ -774,7 +837,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="?attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/copy_omemo_clipboard_description" + android:contentDescription="@string/show_qr_code" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_qr_code_24dp" android:visibility="visible" /> @@ -802,7 +865,8 @@ android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginBottom="@dimen/activity_vertical_margin" - android:visibility="gone"> + android:visibility="gone" + tools:visibility="visible"> @@ -356,6 +357,7 @@ android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/edit_nick" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_edit_24dp" /> @@ -383,6 +385,7 @@ android:layout_centerVertical="true" android:layout_gravity="center_horizontal" android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/change_notification_settings" android:padding="@dimen/image_button_padding" android:src="@drawable/ic_notifications_24dp" /> diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml index a6c7c1fc01c068ccebbd08afa4c189ae8f856a5e..0eb4dd516ca6e8af33a5682fe388476650859e1e 100644 --- a/src/main/res/layout/activity_publish_profile_picture.xml +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -45,7 +45,8 @@ + android:layout_height="@dimen/publish_avatar_size" + android:contentDescription="@string/your_avatar_tap_to_select_new_avatar" /> + android:indeterminateTint="?colorPrimary" /> diff --git a/src/main/res/layout/item_message_content.xml b/src/main/res/layout/item_message_content.xml index 492e1618d88f3af7dbef69ecd3931759b32977f6..aa79f772635504cc43ff90dca85e7a9d03102064 100644 --- a/src/main/res/layout/item_message_content.xml +++ b/src/main/res/layout/item_message_content.xml @@ -85,6 +85,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" + android:layout_marginHorizontal="10dp" android:divider="@android:color/transparent" android:dividerHeight="0dp"> diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index e459de8014030c0c23d3f574b3b865de4ec8a2c1..1160b46e208ccc521bb6d7a76aa145724cf195ee 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -528,7 +528,6 @@ تم نسخ الرسالة إلى الحافظة رسالة الرسائل الخاصة معطلة - التطبيقات المُؤمَّنَة تقبُّل الشهادات المجهولة ؟ إنّ شهادة الخادوم غير مُوقَّعَة مِن طرف هيئة شهادات معروفة. بالرغم مِن ذلك هل تريد مواصلة الإتصال ؟ @@ -635,7 +634,6 @@ يرجى إدخال الكلمة السرية للحساب مشغول خيارات أخرى - للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف تطبيق Conversations إلى قائمة التطبيقات المحميّة. إنشاء الحسابات غير مدعومة مِن طرف الخادم خادمك لا يدعم نشر الصور الرمزية XEP-0215: استكشاف خدمة خارجية diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 7488665649d9b27025853c640a4a01b364e738b1..0f1a5262fe363969c1d08480d616e0db3de389e3 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -663,8 +663,6 @@ Съобщението е копирано Съобщение Личните съобщения са изключени - Защитени приложения - Ако искате да продължите да получавате известия дори когато екранът е заключен, трябва да добавите „Conversations“ към списъка от защитени приложения. Приемане на непознатия сертификат? Сървърният сертификат не е подписан от познат център за сертификация. Приемане на несъвпадащото име на сървъра? diff --git a/src/main/res/values-bn-rIN/strings.xml b/src/main/res/values-bn-rIN/strings.xml index f87d6df06f2df2e3c5a63ada249abe466e490db4..19fc85dd23718bf2664eb533db0692391db76c58 100644 --- a/src/main/res/values-bn-rIN/strings.xml +++ b/src/main/res/values-bn-rIN/strings.xml @@ -26,8 +26,8 @@ এক মিনিট আগে %d মিনিট আগে - %dটাই কথোপকথন পড়া বাকি - %dকথোেকথন পড়া হয়নি + %dটি চ্যাট পড়া হয়নি + %dটি চ্যাট পড়া হয়নি পাঠানো হচ্ছে... অপেক্ষা করুন, সাঙ্কেতিক সন্দেশ পঠিত হচ্ছে... @@ -39,7 +39,7 @@ নির্ধারক অংশগ্রহণকারী অতিথি - আপনি কি আপনার পরিচিতি তালিকা থেকে %s-কে অপসারণ করতে চান? এই যোগাযোগের সাথে কথোপকথনগুলি সরানো হবে না। + আপনি কি আপনার পরিচিতিদের তালিকা থেকে %s কে মুছে ফেলতে চান? এই পরিচিতির চ্যাট মোছা হবে না। %s-কে বার্তা পাঠানো থেকে ব্লক করতে চান? আপনি কি %s-কে আনব্লক করতে চান এবং তাদের আপনাকে বার্তা পাঠানোর অনুমতি দিতে চান? ব্যক্তিটিকে ব্লক্ করা হয়েছে @@ -74,7 +74,7 @@ ছবিগুলি পাঠানোর জন্য তৈরী করা হচ্ছে ফাইলগুলো শেয়ার করা হচ্ছে, অপেক্ষা করুন প্রতিলিপি মুছে ফেলা যাক - Conversation-এর সব প্রতিলিপি মুছে ফেলা যাক + চ্যাটের ইতিহাস মুছুন এই কথোপকথনের সবকটি বার্তাই কি মুছে ফেলতে চান? \n‌ \nসতর্ক থাকবেন: সার্ভার বা অন্য যন্ত্রে থাকা বার্তা কিন্তু অপরিআর্তিতই থাকবে। @@ -104,4 +104,12 @@ ব্যক্তিগত গ্রুপ চ্যাট তৈরি করুন পাবলিক চ্যানেল তৈরি করা যাক বর্তমান চ্যানেলগুলির মধ্যে থেকে খোঁজা যাক + আপনি কি %s-এর জন্য বুকমার্কটি মুছে দিতে চান? + %s থেকে সবাইকে ব্লক করতে চান? + আপনার XMPP একাউন্ট থেকে স্ট্যাক ট্রেস গুলি পাঠালে %1$s-এর উন্নয়নে সাহায্য হয়। + আপনি কি %s-এর জন্য বুকমার্কটি মুছে দিতে এবং ওটার চ্যাট আর্কাইভ করে দিতে চান? + শেয়ার করুন এদের সঙ্গে… + চ্যাট আর্কাইভ করুন + নতুন চ্যাট + %s থেকে সবাইকে আনব্লক করতে চান? \ No newline at end of file diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index 4c5955e9da60f5ef57a8676793ea9b1d20e71632..ada5eff7f3395a63d0a4ca96c29a56a6e0e418d0 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -647,9 +647,6 @@ Missatge copiat al portapapers Missatge Els missatges privats estan desactivats - Aplicacions protegides - Per continuar rebent notificacions, fins i tot quan la pantalla està apagada, heu -d\'afegir Converses a la llista d\'aplicacions protegides. Voleu acceptar un certificat desconegut? El certificat del servidor no està signat per una autoritat de certificació coneguda. Voleu acceptar el nom del servidor associat? diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index f8008bfe73d1d1a64f740d1053a441e56d0fe094..e777a53a1e5123abe671d70d3855afa85dbce44f 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -26,9 +26,9 @@ před minutou před %d minutami - %d nepřečtená konverzace - %d nepřečtené konverzace - %d nepřečtených konverzací + %d nepřečtený chat + %d nepřečtené chaty + %d nepřečtených chatů odesílám… Dešifrování zprávy. Chvíli strpení… @@ -40,7 +40,7 @@ Moderátor Účastník Návštěvník - Přejete si odstranit %s ze seznamu kontaktů? Předešlé rozhovory nebudou odstraněny. + Přejete si odstranit %s ze seznamu kontaktů? Předešlé chaty s tímto kontaktem nebudou odstraněny. Chcete zablokovat příjem zpráv od %s? Chcete odblokovat příjem zpráv od %s? Zablokovat všechny kontakty z %s? @@ -78,7 +78,7 @@ Připravuji odeslání obrázků Sdílení souborů. Chvíli strpení… Smazat historii - Smaže historii konverzací + Smazat historii chatů Opravdu chcete smazat všechny zprávy v této konverzace? \n \nVarováníToto neovlivní zprávy uložené na jiných zařízeních či serverech. @@ -174,7 +174,7 @@ Skutečně chcete odstranit Váš současný veřejný OpenPGP klíč?\nVaše kontakty Vám nebudou moci nadále posílat zprávy šifrované pomocí OpenPGP. OpenPGP veřejný klíč zveřejněn. Povolit účet - Opravdu chcete svůj účet smazat? Smazáním Vašeho účtu dojde k vymazání celé Vaší historie konverzací + Opravdu chcete svůj účet smazat? Smazáním Vašeho účtu dojde k vymazání celé Vaší chatové historie Nahrát hlas Adresa XMPP Blokovat XMPP adresu @@ -272,7 +272,7 @@ Ignorovat Varování: Odeslání bez povolení vzájemného informování o změně stavu může způsobit nečekané potíže.\n\nJděte do \"Detaily kontaktu\" a ověřte nastavení aktualizace stavu. Zabezpečení - Povolit opravu zpráv + Oprava zpráv Povolí kontaktům zpětné upravování jejich zpráv Expertní nastavení S tímto zacházejte velmi opatrně @@ -461,8 +461,8 @@ Při ztišeném vyzvánění označí váš stav jako \"nedostupný\" Vibrační mód brát stejně jako tichý Při nastavení pouze na vibrace označí váš stav jako \"nedostupný\" - Rozšířená nastavení připojení - Zobrazovat nastavení hostname a port při vytváření účtu + Jméno hostitele a port + Zobrazit rozšířené možnosti připojení při nastavování účtu xmpp.server.cz Přihlásit se pomocí certifikátu Nelze analyzovat certifikát @@ -500,10 +500,10 @@ Povolit %1$s přístup k externímu úložišti Povolit %1$s přístup ke kameře Synchronizovat s kontakty - %1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty. -\nU kontaktů se pak zobrazí celé jméno a avatar. + %1$s zpracovává Váš seznam kontaktů místně na Vašem zařízení za účelem zobrazení jmen a profilových obrázků odpovídajících kontaktů na XMPP. \n -\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server. +\n +\nŽádná data kontaktů nikdy neopouštějí Vaše zařízení! Upozorňovat na všechny zprávy Upozornit pouze, když mě někdo zmíní Upozornění vypnuta @@ -523,7 +523,7 @@ Toto pole je vyžadováno Opravit zprávu Odeslat opravenou zprávu - Tento osobní otisk byl již bezpečně ověřen. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu. + Tento osobní otisk byl již označen jako důvěryhodný. Ťuknutím na \"Hotovo\" pouze potvrzujete, že %s je členem tohoto skupinového chatu. Tento účet byl vypnut Bezpečnostní chyba: Neplatný přístup k souboru! Nebyla nalezena aplikace umožňující sdílení URI @@ -554,8 +554,8 @@ Krátký Střední Dlouhý - Informovat o používání - Tato možnost dává vědět Vašim kontaktům, kdy používáte Conversations + Naposledy spatřen + Umožnit svým kontaktům vidět, kdy jste naposledy použili aplikaci Soukromí Vzhled Vybrat paletu barev @@ -601,7 +601,7 @@ Slepě důvěřovat před ověřením Důvěřovat novým zařízením neověřených kontaktů, ale požadovat ruční potvrzení nových zařízení u ověřených kontaktů. Nedůvěryhodný - Neplatný 2D kód + Neplatný QR kód Vyčistit složku dočasných souborů (užitých aplikací fotoaparátu) Vyčistit dočasné soubory Vyčistit soukromé úložiště @@ -679,8 +679,6 @@ Zpráva zkopírována do schránky Zpráva Soukromé zprávy jsou zakázány - Chráněné aplikace - Abyste mohli dostávat upozornění i při vypnuté obrazovce, musíte přidat Conversations mezi chráněné aplikace. Přijmout neznámý certifikát? Certifikát není podepsaný žádnou známou certifikační autoritou. Přijmout nesouhlasící jméno serveru? @@ -698,14 +696,14 @@ Nelze získat seznam zařízení Nelze získat šifrovací klíče Tip: V některých případech může být řešení vzájemné přidání kontaktů do seznamu kontaktů. - Opravdu chcete vypnout OMEMO šifrování pro tuto konverzaci? + Opravdu chcete vypnout OMEMO šifrování pro tento chat? \nTím umožníte správci Vašeho serveru číst Vaše zprávy. Zároveň to však může být jediný způsob, jak komunikovat s kontakty, které používají zastaralé verze klientů. Vypnout hned Koncept: OMEMO šifrování OMEMO bude vždy použito k šifrování zpráv v jednotlivých konverzacích i v soukromých skupinách. - OMEMO bude použito jako výchozí pro nové konverzace. - OMEMO bude nutné zapnout ručně pro každou novou konverzaci. + OMEMO bude použito jako výchozí pro nové chaty. + OMEMO bude nutné zapnout ručně pro každý nový chat. Vytvořit zástupce Zapnuto jako výchozí Vypnuto jako výchozí @@ -724,14 +722,14 @@ Povolit %1$s přístup k mikrofonu Prohledat zprávy GIF - Zobrazit konverzaci + Zobrazit chat Plugin pro sdílení pozice Použít Plugin pro sdílení pozice namísto interní mapy Kopírovat webovou adresu Kopírovat XMPP adresu HTTP sdílení souborů pro S3 Přímé vyhledávání - Na úvodní obrazovce otevřít klávesnici a umístit kurzor do vyhledávacího pole + Na obrazovce \'Nový chat\' otevřít klávesnici a umístit kurzor do vyhledávacího pole Avatar skupinového chatu Hostitel nepodporuje avatary pro skupinový chat Pouze vlastník může změnit avatar skupinového chatu @@ -782,19 +780,19 @@ Ověřit %s %s.]]> Poslali jsme Vám další SMS se 6místným kódem. - Prosím, vložte 6místný pin. + Prosím, vložte 6místný PIN. Poslat SMS znovu Poslat SMS znovu (%s) Chvíli strpení (%s) - zpět - Automaticky vložen pravděpodobný pin ze schránky. - Prosím, vložte svůj 6místný pin. + Zpět + Automaticky vložen pravděpodobný PIN ze schránky. + Prosím, vložte svůj 6místný PIN. Opravdu si přejete přerušit registraci? Ano Ne Ověřuji… - Pin, který jste zadali, je nesprávný. - Pin, který jsme Vám poslali, vypršel. + Zadaný PIN je nesprávný. + Platnost PINu, který jsme Vám poslali, vypršela. Neznámá chyba sítě. Neznámá odpověď serveru. Nebylo možné se připojit k serveru. @@ -917,8 +915,8 @@ Odepnout shora GPX trasa Nebylo možné opravit zprávu - Všechny konverzace - Tato konverzace + Všechny chaty + Tento chat Váš avatar Avatar uživatele %s Šifrováno pomocí OMEMO @@ -1001,4 +999,93 @@ Nebyla nalezena žádná XMPP adresa Smazat avatar Dočasné selhání autentizace + Audiokniha + Odhlášen + Používáte neověřená zařízení. Naskenujte QR kód vašich dalších zařízení, abyste provedli ověření a zabránili tak aktivním MITM útokům. + Odesílat zprávy o pádu + Vítejte v Quicksy! + Chybí povolení k telefonnímu hovoru + Kód neobsahuje otisky k tomuto chatu. + Funkce Objevování kanálů využívá službu třetí strany nazvanou <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Použitím této funkce odešlete Vaši IP adresu a hledané fráze do této služby. Pro více informací si přečtěte <a href=https://search.jabber.network/privacy>Zásady ochrany osobních údajů</a>. + Pokoušíte se naimportovat zálohu v zastaralém formátu + Push server + Odhlásit se + Žádný (deaktivováno) + Odmítnout + Odstranit účet ze serveru + Nahlásit spam a blokovat jeho odesílatele + Rozhraní + Smazat a archivovat chat + Zahájit chat + Nebyl vybrán žádný klientský certifikát! + Služba oznámení pro další aplikace kompatibilní s UnifiedPush + Oznámení + Velikost souborů, komprese obrázků, kvalita videa + Časová lhůta, zvuk, vibrace, neznámí uživatelé + Automatické stahování + Vzhled + Světlý/tmavý vzhled + Povolit snímky obrazovky + Zobrazit obsah aplikace v přepínači aplikací a povolit snímky obrazovky + Koncové šifrování + Certifikační autority + Požadovat channel binding + Důvěřovat systémovým CA certifikátům + Oznámení o psaní, naposledy spatřen, dostupnost + Zásady ochrany osobních údajů + Propojení se seznamem kontaktů není k dispozici + Quicksy žádá o souhlas s použitím Vašich dat + Přejete si odstranit záložku pro %s a archivovat chat? + Nastavit příznak \"autojoin\" při vstupu či opuštění skupiny a reagovat na změny provedené jinými klienty. + Odhlásili jste se z tohoto účtu + Znovu připojit k jinému hostiteli + Žádost o SMS… + Neobnovujte zálohy, které jste sami nevytvořili! + Přepnout na chat + Hovory nejsou dostupné při použití Toru + Distributor UnifiedPush + XMPP účet + Účet, který bude použit pro přijímání zpráv push. + Skrýt oznámení + Nahlásit spam + Zabezpečení + E2E šifrování, slepá důvěra před ověřením, detekce MITM + Channel binding může detekovat některé útoky typu machine-in-the-middle + Téma, barvy, snímky obrazovky, vstup + Sdílet s… + Barevné bubliny chatu + Odlišit barvy odeslaných a přijatých zpráv + Dynamické barvy + Systémové barvy (Material You) + Přidat další stopy? + Přejete si odstranit záložku pro %s? + Nebylo možné odstranit účet ze serveru + Přihlásit se + Váš kontakt používá neověřená zařízení. Naskenujte QR kód kontaktu, abyste provedli ověření a zabránili tak aktivním MITM útokům. + Klávesnice + Archivovat chat + Nový chat + Poté smazat chat + Odeslat šifrovanou zprávu + Příslušné chaty archivovány. + Kontakt není dostupný + Chat archivován + Neplatný uživatelský vstup + Integrace hovorů není k dispozici! + Název hostitele a port, Tor, objevování kanálů + Název hostitele a port, Tor + Vytvořit jednou, naplánovat další + Pokud je funkce distribuce UnifiedPush povolena, spolehlivé a na baterii nenáročné XMPP spojení bude využíváno k probouzení jiných aplikací kompatibilních s UnifiedPush, jako jsou např. Tusky, Ltt.rs, FluffyChat a další. + Aplikace + Interakce + Na tomto zařízení + Vytvořit jednorázovou zálohu + Opakovaná záloha + Oznámení na celou obrazovku + Nepodporovaná operace + Povolit této aplikaci zobrazit oznámení o příchozích hovorech přes celou obrazovku, když je zařízení uzamčeno. + Pozvánky od neznámých + Přijímat pozvánky do skupinových chatů od neznámých kontaktů + Velké písmo + Zvětšit písmo v chatových bublinách \ No newline at end of file diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index aa3093a5d1ba08d6baa7205a5c97d4e3a1672f87..3267d95d6325d050b3805d0d9d1ee65aa3dcfd4c 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -673,8 +673,6 @@ Besked kopieret til udklipsholder Besked Private beskeder er deaktiveret - Beskyttet apps - For at modtage underretninger, selv når skærmen er slukket, skal du tilføje Conversations til listen over beskyttede apps. Accepter ukendt certifikat? Serverens certifikat er ikke underskrevet af en kendt Certifikat Autoritet. Accepter fejlbehæftet servernavn? diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index e3d57b98a8c96fb28066c99ca2dc32fd55f8b989..6498679bc41556fce7b06c0fa67797043e4792d5 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -674,8 +674,6 @@ Nachricht in die Zwischenablage kopiert Nachricht Private Nachrichten sind deaktiviert - Geschützte Apps - Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Conversations zur Liste der geschützten Apps hinzufügen. Unbekanntes Zertifikat akzeptieren? Das Serverzertifikat wurde nicht von einer bekannten Zertifizierungsstelle signiert. Nicht übereinstimmenden Servernamen akzeptieren? @@ -1071,4 +1069,24 @@ Nicht unterstützte Operation Erlaube dieser App, Benachrichtigungen über eingehende Anrufe anzuzeigen, die den gesamten Bildschirm einnehmen, wenn das Gerät gesperrt ist. Einmalige Sicherung erstellen + Private Nachrichten erlauben + Video konnte nicht deaktiviert werden. + OpenPGP-Schlüssel löschen + Benachrichtigungseinstellungen ändern + Anruf erfolgt über ein kabelgebundenes Headset. + Anruf erfolgt über Bluetooth. + Kamera wechseln + Video ist aktiviert. Zum Deaktivieren antippen. + Nickname bearbeiten + Anruf erfolgt über den oberen Lautsprecher. + Aufruf erfolgt über den unteren Lautsprecher. + Dein Profilbild. Antippen, um ein neues Profilbild aus der Galerie auszuwählen. + Video ist deaktiviert. Zum Aktivieren antippen. + Anruf erfolgt über den oberen Lautsprecher. Antippen, um zum unteren Lautsprecher zu wechseln. + Konfiguration ändern + Name und Thema bearbeiten + Anruf erfolgt über den unteren Lautsprecher. Antippen, um zum oberen Lautsprecher zu wechseln. + XEP-0386: Bind 2 + XEP-0388: Extensible SASL Profile + Anmeldeverfahren \ No newline at end of file diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index fc75c92652b0412432679804a749cd89d43588de..99f7207e06c9ef12a7dece19e02defcebf4bddc3 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -664,8 +664,6 @@ Το μήνυμα αντιγράφηκε στο πρόχειρο Μήνυμα Τα ιδιωτικά μηνύματα είναι απενεργοποιημένα - Προστατευμένες εφαρμογές - Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι σβηστή, χρειάζεται να προσθέσετε το Conversations στον κατάλογο με τις προστατευμένες εφαρμογές. Αποδοχή άγνωστου πιστοποιητικού; Το πιστοποιητικό του διακομιστή δεν είναι υπογεγραμμένο από κάποια γνωστή Αρχή Πιστοποίησης. Αποδοχή αναντίστοιχου ονόματος διακομιστή; diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 99e35215aaed3881ffbc950e6d36a9b1bc434cda..bf1619d95c2ddb22a371fd8578c44e1b6f9cf829 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -683,8 +683,6 @@ Mensaje copiado en el portapapeles Mensaje Los mensajes privados están deshabilitados - Aplicaciones protegidas - Para seguir recibiendo notificaciones, aunque la pantalla esté apagada, tienes que añadir Conversations a la lista de aplicaciones protegidas. ¿Aceptar certificado desconocido? El certificado del servidor no está firmado por una Autoridad Certificadora conocida. ¿Aceptar nombre del servidor no coincidente? @@ -1030,7 +1028,7 @@ Compartir con… Archivar chat Chat nuevo - Guardar este chat + Eliminar este chat después Chat guardado Unirse a Conversation Burbujas de chat de colores @@ -1085,4 +1083,5 @@ Operación no soportada Notificaciones a pantalla completa Permite que esta aplicación muestre notificaciones de llamadas entrantes que ocupan toda la pantalla cuando el dispositivo está bloqueado. + Permitir mensajes privados \ No newline at end of file diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/main/res/values-et/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 5ad24330f45a7891f90654b96c1d9e32bae12136..7b03bfb16bac0b1c976fb66a8aa318374e31692a 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -541,8 +541,6 @@ Mezua arbelera kopiatu da Mezua Mezu pribatuak ezgaituta daude - Babestutako aplikazioak - Jakinarazpenak jasotzen jarraitu nahi naduzu, baita pantaila itzalita dagoenean ere, Conversations babestutako aplikazioen zerrendan gehitu behar duzu. Ziurtagiri ezezaguna onartu? Zerbitzariaren ziurtagiria ez dago ezaguna den Ziurtagiri jaulkitzaile batez sinatuta. Bat ez datorren zerbitzari izena onartu? diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index 14c5b5246a9c0120b6e4bd82c433be8a9205f244..b3f8bd568ad6748f0ea6baaa73d9d3f52195bfb1 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -672,14 +672,12 @@ بارکد دوبعدی نامعتبر بگذارید همه دیگران را دعوت کنند نصب Orbot - برنامه‌های محافظت‌شده کش را خالی کن کپی اثر انگشت دیجیتال اعلان‌های شناور انتخاب حساب این گفتگوی گروهی خصوصی هیچ عضوی ندارد. بازیابی پشتیبان - برای دریافت اعلان‌ها حتی وقتی که صفحه خاموش است باید این برنامه را به فهرست برنامه‌های محافظت‌شده بیفزایید. بارکد را به کمک دوربین اسکن کنید برای تنظیم نام دکمهٔ ویرایش را به‌کار ببرید. همیشه برای گفتگوهای تکی و گروهای خصوصی OMEMO به کار برود. diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 4a605e58e0b8e1a5ed81567ebd040980f898cdc4..41b7d6b578eebe8bd6954fa9f578730329c1c3b9 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -638,8 +638,6 @@ Viesti kopioitu leikepöydälle Viesti Yksityisviestit on poistettu käytöstä - Suojatut sovellukset - Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Conversations pitää lisätä suojattujen sovellusten luetteloon. Hyväksytäänkö tuntematon varmenne? Palvelimen varmenne ei ole luotetun myöntäjän allekirjoittama. Hyväksytäänkö eriävä palvelimen nimi? diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 91de8b2b603075fe5e60dd5c322886343499d61b..ac1fd4a39b1a7f723ba44c7204dcf4f3334373c5 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -5,7 +5,7 @@ Gérer le compte Détails du contact Détails du groupe - Détails du canal + Détails du salon Ajouter un compte Modifier le nom Ajouter au carnet d\'adresses @@ -14,14 +14,14 @@ Débloquer le contact Bloquer le domaine Débloquer le domaine - Bloquer le participant - Débloquer le participant + Bloquer le·a participant·e + Débloquer le·a participant·e Gestion des comptes Paramètres Choisir un contact Choisir les contacts Partager via le compte - Bloquer la liste + Comptes bloqués À l\'instant Il y a 1 minute Il y a %d minutes @@ -35,11 +35,11 @@ Message chiffré avec OpenPGP Cet identifiant est déjà utilisé Identifiant non valide - Administrateur + Administrateur·ice Propriétaire - Modérateur - Participant - Visiteur + Modérateur·ice + Participant·e + Visiteur·ice Voulez-vous supprimer %s de votre liste de contacts ? Les conversations associées à ce contact ne seront pas supprimées. Voulez-vous bloquer %s pour l\'empêcher de vous envoyer des messages ? Voulez-vous débloquer %s et lui permettre de vous envoyer des messages ? @@ -95,15 +95,15 @@ Envoyer en clair Échec du déchiffrement. Avez-vous la bonne clé privée ? OpenKeychain - OpenKeychain pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.\n\nOpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.\n\n(Veuillez redémarrer %1$s après l\'installation de l\'application.)]]> + %1$s utilise <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.<br><br>OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.<br><br><small>(Veuillez redémarrer %1$s après l\'installation de l\'application.)</small> Redémarrer Installer Veuillez installer OpenKeychain Proposition… Patientez… - Aucune clé OpenPGP trouvée. + Aucune clé OpenPGP trouvée Impossible de chiffrer vos messages car votre contact n\'a pas communiqué sa clé publique.\n\nDemandez-lui de configurer OpenPGP. - Aucune clé OpenPGP n\'a été trouvée. + Aucune clé OpenPGP n\'a été trouvée Impossible de chiffrer votre message car vos contacts ne communiquent pas leur clé publique.\n\nDemandez-leur de configurer OpenPGP. Général Accepter les fichiers @@ -137,7 +137,7 @@ Demander les màj de disponibilité Choisir une image Prendre une photo - Accepter par avance les demandes de publication. + Accepter par avance les demandes de publication Le fichier choisi n\'est pas une image Impossible de convertir l\'image Impossible de trouver le fichier @@ -171,7 +171,7 @@ OMEMO Supprimer Désactiver temporairement - Publier un avatar + Publier une image de profil Publier la clé publique OpenPGP Supprimer la clé publique OpenPGP Êtes-vous sûr de vouloir supprimer votre clé publique OpenPGP de votre annonce de présence ?\nVos contacts ne pourront plus vous envoyer de message chiffrés avec OpenPGP. @@ -181,10 +181,10 @@ Enregistrer un son Adresse XMPP Bloquer l\'adresse XMPP - nom@exemple.com + nom@example.com Mot de passe Ce n\'est pas une adresse XMPP valide - Plus de mémoire disponible. L\'image est trop volumineuse. + Plus de mémoire disponible. L\'image est trop volumineuse Voulez-vous ajouter %s à votre carnet d\'adresses ? Infos sur le serveur XEP-0313 : MAM @@ -208,7 +208,7 @@ en ligne hier en ligne il y a %d jours Message chiffré. Veuillez installer OpenKeychain pour le déchiffrer. - Nouveaux messages chiffrés avec OpenPGP détectés. + Nouveaux messages chiffrés avec OpenPGP détectés ID de clé OpenPGP Empreinte OMEMO v\\Empreinte OMEMO @@ -228,18 +228,18 @@ Sélectionner Le contact existe déjà Rejoindre - canal@conference.example.com/surnom - canal@conference.example.com + salon@conference.example.com/surnom + salon@conference.example.com Enregistrer comme favori Supprimer le favori Détruire le groupe - Détruire le canal + Détruire le salon Voulez-vous vraiment détruire ce groupe ?\n\nAvertissement : le groupe sera complètement supprimé du serveur. - Êtes-vous sûr de vouloir détruire ce canal public \? -\n -\nAttention : Le canal sera totalement supprimé du serveur. + Êtes-vous sûr de vouloir détruire ce salon public ? +\n +\nAttention : Le salon sera totalement supprimé du serveur. Impossible de détruire le groupe - Impossible de détruire le canal + Impossible de détruire le salon Modifier le sujet du groupe Sujet Rejoindre le groupe… @@ -251,13 +251,13 @@ %1$s+%2$d autres ont tout lu jusqu\'ici Tout le monde a lu jusqu\'ici Publier - Appuyer sur l\'avatar pour choisir une image depuis la galerie + Appuyer sur l\'image de profil pour choisir une image depuis la galerie Mise à jour… Le serveur a rejeté votre publication Impossible de convertir votre image - Impossible de stocker l\'avatar sur le disque + Impossible de stocker l\'image de profil sur le disque (Un appui long réinitialise le paramètre) - Votre serveur ne supporte pas la publication d\'avatars + Votre serveur ne supporte pas la publication d\'image de profil chuchoté pour %s Envoyer un message privé à %s @@ -280,13 +280,13 @@ Correction des messages Permet à vos contacts d\'éditer leurs messages rétroactivement Paramètres avancés - À utiliser avec précaution. + À utiliser avec précaution À propos de %s Heures tranquilles Heure de début Heure de fin Activer les heures tranquilles - Les notifications seront muettes pendant les heures tranquilles. + Les notifications seront muettes pendant les heures tranquilles Autres Empreinte OMEMO copiée dans le presse-papier Vous êtes bannis de ce groupe @@ -298,7 +298,7 @@ avec le compte %s hébergé sur %s Vérification de %s sur l\'hôte HTTP - Vous n\'êtes pas connecté. Essayez plus tard. + Vous n\'êtes pas connecté. Essayez plus tard Vérification de la taille de %s Vérification de la taille de %1$s sur %2$s Options du message @@ -310,15 +310,15 @@ URL copiée dans le presse-papier Adresse XMPP copiée dans le presse-papiers Message d\'erreur copié dans le presse-papier - adresse internet + adresse web Scanner le QR code Montrer le QR code - Afficher la liste des contacts bloqués + Afficher la liste des comptes bloqués Détails du compte Confirmer Réessayer Service au premier plan - Évite que le système ne ferme votre connexion. + Évite que le système ne ferme votre connexion Créer une sauvegarde La sauvegarde sera stockée dans %s Création des fichiers de sauvegarde @@ -344,13 +344,13 @@ Aucune application disponible pour ouvrir le lien Aucune application disponible pour afficher le contact Tags dynamiques - Afficher des tags en lecture seule en dessous des contacts. + Afficher des tags en lecture seule en dessous des contacts Activer les notifications Serveur de groupe non trouvé Impossible de créer le groupe - Avatar du compte + Image de profil du compte Copier l\'empreinte OMEMO dans le presse-papier - Régénérer l\'empreinte OMEMO + Régénérer la clé OMEMO Supprimer les appareils Êtes-vous sûr de vouloir supprimer les autres appareils de l\'annonce OMEMO ? Ils s\'annonceront de nouveau à leur prochaine connexion, mais ils peuvent ne pas recevoir les messages envoyés entre temps. Aucune clé utilisable n\'est disponible pour ce contact. \nImpossible d\'obtenir de nouvelles clés depuis le serveur. Pourrait-il y avoir un problème avec le serveur de votre contact ? @@ -372,26 +372,26 @@ Hors ligne Banni Membre - Mode expert + Mode avancé Accorder des privilèges aux membres Révoquer les privilèges des membres - Accorder des privilèges d\'administrateur - Révoquer des privilèges d\'administrateur + Accorder des privilèges d\'administrateur·ice + Révoquer des privilèges d\'administrateur·ice Accorder des privilèges de propriétaire Révoquer les privilèges du propriétaire Supprimer du groupe - Supprimer du canal + Supprimer du salon Impossible de changer l\'affiliation de %s Bannir du groupe - Bannir du canal - Vous essayez de supprimer %s d\'un canal public. La seule façon de le faire est de bannir cet utilisateur pour toujours. + Bannir du salon + Vous essayez de supprimer %s d\'un salon public. La seule façon de le faire est de bannir cet·te utilisateur·ice pour toujours. Bannir maintenant Impossible de changer le rôle de %s - Configuration du groupe - Configuration du canal public + Configuration du groupe privé + Configuration du salon public Privé, membres uniquement Rendre les adresses XMPP visibles à tout le monde - Rendre le canal modéré + Rendre le salon modéré Vous ne participez pas Options du groupe modifiées ! Impossible de modifier les options du groupe @@ -404,14 +404,14 @@ Touche Entrée pour envoyer Utilisez la touche Entrée pour envoyer un message. Vous pourrez toujours utiliser la combinaison Ctrl+Entrée pour envoyer un message, même si cette option est désactivée. Afficher la touche Entrée - Remplacer la touche Émoticônes par la touche Entrée. + Remplacer la touche Émoticônes par la touche Entrée audio vidéo image document PDF Application Android Contact - L\'avatar a été publié ! + L\'image de profil a été publiée ! %s en cours d\'envoi En train de proposer un(e) %s Cacher contacts hors-ligne @@ -426,11 +426,11 @@ Aucune application trouvée pour afficher le lieu Position Quitter le groupe privé - Quitte le canal public + Quitte le salon public Ne pas utiliser les CAs système - Tous les certificats doivent être approuvés manuellement. + Tous les certificats doivent être approuvés manuellement Retirer les certificats - Supprimer les certificats approuvés manuellement. + Supprimer les certificats approuvés manuellement Aucun certificat approuvé manuellement Retirer les certificats Supprimer la sélection @@ -485,7 +485,7 @@ Votre appareil ne supporte pas la sélection de certificats client ! Connexion Connexion via Tor - Rediriger toutes les connexions vers le réseau Tor. Nécessite Orbot. + Rediriger toutes les connexions vers le réseau Tor. Nécessite Orbot Nom d\'hôte Port Adresse du serveur (ou .onion) @@ -524,18 +524,18 @@ Ce champ est requis Corriger le message Envoyer le message corrigé - Vous avez déjà fait confiance à l\'empreinte de cette personne pour accorder votre confiance. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe. + Vous avez déjà fait confiance à l\'empreinte de cette personne. En sélectionnant « Terminé », vous confirmez simplement que %s fait partie de ce groupe. Vous avez désactivé ce compte Erreur de sécurité : accès invalide au fichier ! Aucune application disponible pour partager l\'URI Partager l\'URI avec… Accepter et continuer Nous vous guiderons tout au long du processus de création d\'un compte sur conversations.im. -\nLorsque vous sélectionnerez conversations.im en tant que fournisseur, vous pourrez communiquer avec les utilisateurs d\'autres fournisseurs en leur fournissant votre adresse XMPP complète. +\nLorsque vous sélectionnerez conversations.im en tant que fournisseur, vous pourrez communiquer avec les utilisateur·ices d\'autres fournisseurs en leur fournissant votre adresse XMPP complète. Votre adresse XMPP complète sera : %s Créer un compte Utiliser votre propre fournisseur - Choisissez votre nom d\'utilisateur + Choisissez votre nom d\'utilisateur·ice Gérer l\'état de disponibilité manuellement Définir votre disponibilité lors de l\'édition de votre statut. Message de statut @@ -548,7 +548,7 @@ Les optimisations de batterie ne peuvent pas être désactivées sur votre appareil Échec de l\'inscription : Réessayer plus tard Échec de l\'inscription : le mot de passe n\'est pas assez fort - Choisir les participants + Choisir les participant·es Création d\'un groupe… Inviter à nouveau Désactiver @@ -568,7 +568,7 @@ Ordinateur Smartphone Tablette - Navigateur Internet + Navigateur web Console Paiement requis Autoriser à accéder à internet @@ -583,7 +583,7 @@ Effacer les identités OMEMO Régénérer vos clés OMEMO. Tous vos contacts devront vous vérifier à nouveau. À n\'utiliser qu\'en dernier recours. Supprimer les clés sélectionnées - Vous devez être connecté pour publier votre avatar. + Vous devez être connecté pour publier votre image de profil. Afficher le message d\'erreur Message d\'erreur Économiseur de données activé @@ -645,16 +645,16 @@ %d mois %d mois - Suppression messages auto + Suppression automatique des messages Efface automatiquement de cet appareil les messages plus anciens que l\'intervalle choisi. Chiffrement du message en cours Aucun message récupéré, en raison de la configuration de rétention de l\'appareil. Compression de la vidéo en cours Contact bloqué. - Notifications d\'inconnus - Notifier les messages et appels reçus d\'inconnus. - Message d\'un inconnu reçu - Bloquer l\'inconnu + Notifications d\'inconnu·es + Notifier les messages et appels reçus d\'inconnu·es. + Message d\'un·e inconnu·e reçu + Bloquer l\'inconnu·e Bloquer le domaine entier En ligne actuellement Nouvelle tentative de déchiffrement @@ -676,8 +676,6 @@ Message copié dans le presse-papier Message Les messages privés sont désactivés - Applications protégées - Pour recevoir les notifications, même lorsque l\'écran est éteint, vous devez ajouter Conversations à la liste des applications protégées. Accepter les certificats inconnus ? Le certificat du serveur n\'est pas signé par une Autorité de Certification connue. Accepter un nom de serveur qui ne correspond pas ? @@ -688,7 +686,7 @@ La lecture d\'un QR code nécessite l\'accès à l\'appareil photo Faire défiler l\'écran jusqu\'en bas Faire défiler l\'écran jusqu\'en bas après avoir envoyé un message - Modifier le message de l\'état + Modifier le message d\'état Modifier le message de statut Désactiver le chiffrement %1$s n\'est pas capable d\'envoyer un message chiffré à %2$s. Ceci peut être lié au fait que votre contact utilise un serveur obsolète ou un client qui ne gère par OMEMO. @@ -709,7 +707,7 @@ Le message n\'était pas chiffré pour cet appareil. Échec de déchiffrement du message OMEMO. annuler - Le partage de positionnement est désactivé. + Le partage de positionnement est désactivé Figer la position Débloquer la position Copier la position @@ -726,14 +724,14 @@ Voir la conversation Plugin de partage de localisation Utilisez le plugin Share Location à la place de la carte intégrée - Copier l\'adresse internet + Copier l\'adresse web Copier l\'adresse XMPP Partage de fichier HTTP pour S3 Recherche directe Lors de l\'ajout de conversations, afficher le clavier et placer le curseur sur le champ de recherche - Avatar du groupe - Le serveur ne prend pas en charge les avatars pour les groupes - Seul le propriétaire peut changer l\'avatar d\'un groupe + Image de profil du groupe + Le serveur ne prend pas en charge les images de profil pour les groupes + Seul le propriétaire peut changer l\'image de profil d\'un groupe Nom du contact Surnom Nom @@ -759,7 +757,7 @@ Importance, son, vibration Compression vidéo Voir les média - Participants + Participant·es Navigateur de média Fichier omis en raison d\'une violation de la sécurité. Qualité des vidéos @@ -801,10 +799,10 @@ Impossible d\'établir une connexion sécurisée. Impossible de trouver le serveur. Une erreur est survenue pendant le traitement de votre requête. - Entrée utilisateur incorrecte + Entrée utilisateur·ice incorrecte Temporairement indisponible. Réessayez plus tard. Pas de connexion réseau. - Veuillez réessayer dans%s + Veuillez réessayer dans %s Vous êtes à taux limité Trop de tentatives Vous utilisez une version obsolète de cette application. @@ -817,8 +815,8 @@ Rejeter la demande Installer Orbot Démarrer Orbot - Aucune application de marché installée. - Ce canal rendra votre adresse XMPP publique + Aucun magasin d\'applications installée. + Ce salon rendra votre adresse XMPP publique e-book Original (non compressé) Ouvrir avec… @@ -827,48 +825,48 @@ Restaurer la sauvegarde Restaurer Entrez votre mot de passe pour que le compte %s restaure la sauvegarde. - N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte du périphérique d’origine. + N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte de l\'appareil d’origine. Impossible de restaurer la sauvegarde. Impossible de déchiffrer la sauvegarde. Le mot de passe est-il correct ? Sauvegarde & restauration Entrez l\'adresse XMPP Créer un groupe - Rejoindre le canal public + Rejoindre le salon public Créer un groupe privé - Créer un canal public - Nom du canal + Créer un salon public + Nom du salon Adresse XMPP - Veuillez donner un nom au canal + Veuillez donner un nom au salon Veuillez renseigner une adresse XMPP Ceci est une adresse XMPP. Veuillez renseigner un nom. - Création d\'un canal public… - Ce canal existe déjà - Vous avez rejoint un canal existant - Impossible de sauvegarder la configuration du canal + Création d\'un salon public… + Ce salon existe déjà + Vous avez rejoint un salon existant + Impossible de sauvegarder la configuration du salon Autoriser quiconque à éditer le sujet Permettre à quiconque d\'inviter d\'autres personnes N\'importe qui peut éditer le sujet. Les propriétaires peuvent éditer le sujet. - Les administrateurs peuvent modifier le sujet. + Les administrateur·ices peuvent modifier le sujet. Les propriétaires peuvent inviter d\'autres personnes. N\'importe qui peut inviter d\'autres personnes. - Les adresses XMPP sont visibles par les administrateurs. + Les adresses XMPP sont visibles par les administrateur·ices. Les adresses XMPP sont visibles par tous. - Ce canal public n\'a pas de participants. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP. - Ce groupe privé n\'a aucun participant. + Ce salon public n\'a pas de participant·es. Invitez vos contacts ou utilisez le bouton de partage pour distribuer son adresse XMPP. + Ce groupe privé n\'a aucun·e participant·e. Gérer les privilèges - Rechercher des participants + Rechercher des participant·es Fichier trop volumineux Joindre - Découverte des canaux - Recherche des canaux + Découverte des salons + Recherche des salons Violation possible de la confidentialité ! J\'ai déjà un compte Ajouter un compte existant Enregistrer un nouveau compte Ceci ressemble à une adresse de domaine Ajouter quand même - Ceci ressemble à une adresse de canal + Ceci ressemble à une adresse de salon Partager les fichiers de sauvegardes Sauvegarder les conversations Événement @@ -877,13 +875,13 @@ Ce compte a déjà été configuré Veuillez saisir le mot de passe pour ce compte Impossible de réaliser cette action - Rejoindre le canal public… + Rejoindre le salon public… L\'application de partage n\'a pas accordé la permission d\'accéder à ce fichier. - + Salons et groupes de discussion jabber.network Serveur local - La plupart des utilisateurs devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP. - Méthode de découverte des canaux + La plupart des utilisateur·ices devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP. + Méthode de découverte des salons Sauvegarde À propos Veuillez activer votre compte @@ -928,8 +926,8 @@ Impossible de corriger le message Toutes les conversations Cette conversation - Votre avatar - Avatar pour %s + Votre image de profil + Image de profil pour %s Chiffré avec OMEMO Chiffré avec OpenPGP Non chiffré @@ -951,7 +949,7 @@ Échec lors de la livraison Plus d\'options Aucune application trouvée - Inviter à Conversations + Inviter sur Conversations Impossible de lire l\'invitation Le serveur ne prend pas en charge la génération d\'invitations Aucun compte actif ne prend en charge cette fonctionalité @@ -995,7 +993,7 @@ Synchroniser les favoris Activer \"Rejoindre automatiquement\" en entrant ou sortant d\'un groupe et réagir aux modifications apportées par d\'autres clients. graphique vectoriel - Appel entrant (%s) - %s + Appel entrant (%s) · %s Appel sortant · %s %1$d appel manqué de %2$s @@ -1008,7 +1006,7 @@ Impossible de supprimer le compte du serveur Document texte Échec temporaire de l\'authentification - Supprimer l\'avatar + Supprimer l\'image de profil Vous essayez d\'importer un format de fichier de sauvegarde obsolète Livre audio Distributeur UnifiedPush @@ -1016,7 +1014,7 @@ Signaler un spam Politique de confidentialité Quicksy vous demande votre consentement pour utiliser vos données - Signaler un spam et bloquer son auteur + Signaler un spam et bloquer son auteur·ice Déconnecté S\'identifier Vous vous êtes déconnecté⸱e de ce compte @@ -1034,11 +1032,11 @@ Thème clair/sombre Autoriser les captures d\'écran Chiffrement de bout en bout - Afficher le contenu de l\'application dans le sélecteur d\'applications et autorise les captures d\'écran + Afficher le contenu de l\'application dans le sélecteur d\'applications et autoriser les captures d\'écran Autorités de certification Faire confiance aux certificats de l\'autorité de certification du système - Oblige la liaison du canal - La liaison du canal peut permettre de détecter les attaques de l\'homme du milieu + Oblige la liaison du salon + La liaison du salon peut permettre de détecter les attaques de l\'homme du milieu Clavier Créer une sauvegarde ponctuelle Créer une sauvegarde récurrente @@ -1051,7 +1049,7 @@ Rejoignez la Conversation Chiffrement de bout en bout, confiance aveugle avant vérification, détection des attaques de l\'homme du milieu Système d\'exploitation - Opération pas encore supportée + Opération non supportée Notifications d\'utilisation En agissant en tant que distributeur UnifiedPush, la connexion XMPP persistante, fiable et économique en batterie sera utilisée pour communiquer avec d\'autres applications compatibles comme Tusky, Ltt.rs, FluffyChat et d\'autres encore. Notifications en plein écran @@ -1062,18 +1060,18 @@ Afficher un fond différent pour distinguer les messages envoyés et reçus Couleurs dynamiques Couleurs systèmes (Material You) - La découverte de canaux utilise un service de tierce partie appelé <a href=https://search.jabber.network>search.jabber.network</a>.<br><br> Utiliser cette fonctionnalité va transmettre votre adresse IP et votre recherche à ce service. Voir leur <a href=https://search.jabber.network/privacy>Politique de confidentialité</a> pour plus d\'informations. + La découverte de salons utilise un service tier appelé <a href=https://search.jabber.network>search.jabber.network</a>.<br><br> Utiliser cette fonctionnalité va transmettre votre adresse IP et votre recherche à ce service. Voir leur <a href=https://search.jabber.network/privacy>Politique de confidentialité</a> pour plus d\'informations. Archiver la conversation - Archiver cette conversation + Supprimer cette conversation ensuite Envoyer un message chiffré Conversation archivée Se rendre à la conversation Votre contact utilise un appareil qui n\'est pas encore vérifié. Scannez son QR code pour effectuer une vérification et éviter une attaque de l\'homme du milieu. - Discuter + Lancer une discussion Aucun certificat client sélectionné ! Thème, couleurs, captures d\'écran, saisie Sécurité - Relai de notifications pour les applications de tierce partie compatibles avec UnifiedPush + Relai de notifications pour les applications tierces compatibles avec UnifiedPush Notifications Taille de fichier, compression des images, qualité des vidéos Période sans notifications, sonnerie, vibration, inconnus @@ -1084,12 +1082,32 @@ Connection au serveur Notifications d\'écriture, dernière connexion, disponibilité Nom d\'hôte et port, Tor - Nom d\'hôte et port, Tor et découverte des canaux + Nom d\'hôte et port, Tor et découverte des salons Application Interaction Sur l\'appareil - Invitations d\'inconnus - Accepter les invitations aux conversations de groupes provenant d\'inconnus + Invitations d\'inconnu·es + Accepter les invitations aux conversations de groupes provenant d\'inconnu·es Grande police Augmenter la taille de la police dans les bulles de message + Autoriser les messages privés + Votre image de profil. Tapotez pour sélectionner une nouvelle image de profil depuis la galerie. + Impossible de désactiver la vidéo. + XEP-0388 : Extensible SASL Profile + Éditer nom et sujet + Modifier la configuration + Modifier les paramètres de notification + L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur. + L\'appel passe par les écouteurs. + XEP-0386 : Bind 2 + Éditer le pseudo + Supprimer la clé OpenPGP + L\'appel passe par le bluetooth. + Changer de caméra + La vidéo est activée. Tapotez pour la désactiver. + La vidéo est désactivée. Tapotez pour l\'activer. + L\'appel passe par le casque filaire + L\'appel passe par le haut-parleur. Tapotez pour passer sur les écouteurs. + L\'appel passe par le haut-parleur. + Mécanisme de connexion diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index a5fb86ce5b6ca0d2d3e88c197d294c8cf806c5b7..8a38babf075c8ad837ee0be7b70b4d76adf8d353 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -676,8 +676,6 @@ Mensaxe copiada ao portapapeis Mensaxe As mensaxes privadas están desactivadas - Apps protexidos - Para seguir recibindo notificacións, incluso cando a pantalla está apagada, tes que engadir Conversations á lista de apps protexidas. ¿Aceptar certificado descoñecido? O certificado do servidor non está asinado por unha autoridade de certificación coñecida. ¿Aceptar un nome de servidor que non coincida? @@ -938,7 +936,7 @@ Volver á chamada activa Non se puido activar a cámara Fixar enriba - Desafixar de enriba + Non fixar enriba Ruta GPX No se pode correxir a mensaxe Todos os chats @@ -1071,4 +1069,24 @@ Permitir que a app mostre a notificación de chamada entrante a pantalla completa cando o dispositivo está bloqueado. Crear única, Programar recurrentes Crear unha copia de apoio + Permitir mensaxes privadas + O teu avatar. Toca para escoller un novo avatar desde a galería. + Non se puido desactivar o vídeo. + Editar nome + Eliminar chave OpenPGP + Editar nome e tema + Cambiar a configuración + Cambiar axustes das notificacións + A chamada está usando o auricular, toca para usar o altofalante. + A chamada está usando auriculares. + A chamada usa auriculares con cable + A chamada usa os altofalandes. Toca para cambiar a auriculares. + A chamada está usando o altofalante. + A chamada está usando bluetooth. + Cambiar de cámara + Video activado. Toca para desactivar. + Video desactivado. Toca para activar. + XEP-0388: Extensible SASL Profile + Método de acceso + XEP-0386: Bind 2 diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index 62d7e51a139ac2931b1f50157b6b5e0e43a0391e..328a6a251af9e213bdd51ac3126aaf37a607d25d 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -631,8 +631,6 @@ Üzenet a vágólapra másolva Üzenet A személyes üzenetek le vannak tiltva - Védett alkalmazások - Ha akkor is szeretne értesítéseket kapni, amikor a kijelző ki van kapcsolva, hozzá kell adnia a Conversations alkalmazást a védett alkalmazások listájához. Elfogadja az ismeretlen tanúsítványt? A kiszolgáló tanúsítványa nincs aláírva egy ismert hitelesítésszolgáltató által. Elfogadja a nem egyező kiszolgálónevet? diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 1a91bedb4f3ea445a472e55ed68851cd090d80aa..64853f2f4f4f5367e3d93814a759c11f1fb251b0 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -683,8 +683,6 @@ Messaggio copiato negli appunti Messaggio I messaggi privati sono disattivati - App protette - Per ricevere notifiche anche quando lo schermo è spento, devi aggiungere Conversations all\'elenco delle app protette. Accetti il certificato sconosciuto? Il certificato del server non è firmato da una Certificate Authority nota. Accetti il nome del server non corrispondente? @@ -1024,7 +1022,7 @@ Condividi con … Archivia la chat Nuova chat - Archivia questa chat + Elimina chat in seguito Chat archiviata Unisciti a Conversation Messaggi di chat colorati @@ -1077,4 +1075,29 @@ Autorità di certificazione Caratteri grandi Aumenta la dimensione dei caratteri nei messaggi + Inviti da estranei + Accetta inviti a chat di gruppo da estranei + Crea una volta sola, Programma ricorrenze + Backup ricorrente + Notifiche a schermo intero + Operazione non supportata + Consenti messaggi privati + Consenti a questa app di mostrare notifiche di chiamate in arrivo che occupano l\'intero schermo quando il dispositivo è bloccato. + Crea backup una volta + Il tuo avatar. Tocca per selezionare un nuovo avatar dalla galleria. + Impossibile disattivare il video. + Modifica nick + Elimina chiave OpenPGP + Modifica nome e argomento + Cambia configurazione + Cambia impostazioni di notifica + La chiamata sta usando gli auricolari. Tocca per passare agli altoparlanti. + La chiamata sta usando gli auricolari. + La chiamata sta usando cuffie cablate + La chiamata sta usando gli altoparlanti. Tocca per passare agli auricolari. + La chiamata sta usando gli altoparlanti. + La chiamata sta usando il bluetooth. + Il video è attivo. Tocca per disattivarlo. + Il video è disattivato. Tocca per attivarlo. + Cambia fotocamera diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index fac78242f02a3f710180febe5517a51114ee6d8f..e6eacad5c301850d9b8ca2a346660aecf81e8bc1 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -9,28 +9,28 @@ アカウントを追加 名前を編集 アドレス帳に追加 - 名簿から削除 + 連絡先リストから削除 連絡先をブロック - 連絡先のブロックを解除 + 連絡先をブロック解除 ドメインをブロック - ドメインのブロックを解除 + ドメインをブロック解除 参加者をブロック - 参加者のブロックを解除 + 参加者をブロック解除 アカウントを管理 設定 連絡先を選択 連絡先を選択 アカウントで共有 - ブロック一覧 + ブロックリスト ちょうど今 - 1分前 - %d分前 + 1 分前 + %d 分前 - %d件の未読の会話 + 未読の会話 %d 件 送信中… メッセージを復号しています。しばらくお待ちください… - OpenPGPで暗号化されたメッセージ + OpenPGP で暗号化されたメッセージ ニックネームはすでに使用されています ニックネームが正しくありません 管理者 @@ -663,8 +663,6 @@ メッセージをクリップボードにコピーしました メッセージ 非公開メッセージを無効化しました - 保護されたアプリ - 画面がオフになっているときでも通知を受信し続けるには、保護されたアプリの一覧に Conversations を追加する必要があります。 未知の証明書を受け入れますか? サーバー証明書が既知の認証局によって署名されていません。 不一致のサーバー名を受け入れますか? @@ -992,24 +990,24 @@ 連絡先は未検証のデバイスを使用しています。 QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。 未検証のデバイスを使用しています。他のデバイスで QR コードをスキャンして検証を実行し、アクティブな MITM 攻撃を阻止してください。 電話をかける権限がありません - %s のブックマークを削除して会話を保管しますか ? + %s のブックマークを削除して会話をアーカイブしますか ? %s のブックマークを削除しますか ? 通話の統合は利用できません。 連絡先は利用できません - 削除して会話を保管 + 削除して会話をアーカイブ クラッシュ報告を送信 - 対応する会話は保管されました。 + 対応する会話はアーカイブされました。 クライアント証明書が選択されていません。 ホスト名とポート番号、Tor このデバイスで 猶予期間、着信音、バイブレーション、見知らぬ人 端末間暗号化、検証前の無条件の信頼、MITM検出 - 会話を保管 + 会話をアーカイブ 新しい会話 - この会話を保管 + その後の会話を削除 システムの配色 (Material You) 暗号化メッセージを送信 - 会話は保管されました + 会話はアーカイブされました 多彩な会話の吹き出し 会話に切り替え インターフェース @@ -1044,4 +1042,10 @@ 全画面通知 端末がロックされているとき、このアプリが全画面を占める着信通知を表示することを許可する。 UnifiedPush互換サードパーティアプリの通知中継 + 対応されていない動作 + 通知の種類、最終確認日時、在席状況 + 非公開メッセージを許可 + 繰り返し作成 + 一回限りもしくは繰り返し計画を作成 + 一回限りのバックアップを作成 diff --git a/src/main/res/values-nb-rNO/strings.xml b/src/main/res/values-nb-rNO/strings.xml index 1da6a9b0b96bd487131ada2b709ed2aa57d6f829..572a16495dc61791cff346a456a11ad19551d8a8 100644 --- a/src/main/res/values-nb-rNO/strings.xml +++ b/src/main/res/values-nb-rNO/strings.xml @@ -448,8 +448,6 @@ Melding kopiert til utklippstavle Melding Private meldinger er skrudd av - Beskyttede programmer - For å motta merknader, selv når skjermen er skrudd av, må du legge til Conversations i listen over beskyttede programmer. Vis plasseringsdata Gruppesludringsnavn Opprett gruppesludring diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index f7a2868edeacdfc23a95f0a711c6538e59f76913..b5f725e12ed352c858bc5a824ef29151aece4f20 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -39,7 +39,7 @@ Moderator Deelnemer Bezoeker - Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon zullen niet worden verwijderd. + Wil je %s uit je contactenlijst verwijderen? De gesprekken met deze contactpersoon worden niet verwijderd. Wil je alle berichten van %s blokkeren? Wil je %s deblokkeren en er weer berichten van kunnen ontvangen? Alle contacten van %s blokkeren? @@ -66,8 +66,8 @@ Door crashrapportages via uw XMPP account te sturen help je de ontwikkeling van %1$s. Nu versturen Niet opnieuw vragen - Verbinding maken met account mislukt - Verbinden met meerdere accounts mislukt + Verbinding met account mislukt + Verbinding met meerdere accounts mislukt Tik hier op om accounts te beheren Bestand bijvoegen Wil je dit ontbrekende contact toevoegen aan je contactenlijst? @@ -79,7 +79,9 @@ Geschiedenis wissen Gespreksgeschiedenis wissen Bestand verwijderen - Weet je zeker dat je dit bestand wil verwijderen?\n\nWaarschuwing: Dit zal kopieën van dit bericht die opgeslagen zijn op andere apparaten of servers niet verwijderen. + Weet je zeker dat je dit bestand wil verwijderen? +\n +\nWaarschuwing: Dit zal kopieën van dit bericht die opgeslagen zijn op andere apparaten of servers niet verwijderen. Apparaat kiezen Verstuur onversleuteld bericht Verstuur bericht @@ -92,7 +94,7 @@ Herstarten Installeren Gelieve OpenKeychain te installeren - bezig met aanbieden… + Aanbieden… wachten… Geen OpenPGP-sleutel gevonden Geen OpenPGP-sleutels gevonden @@ -125,7 +127,7 @@ Foto nemen Op voorhand toestemming verlenen voor abonneren Het bestand dat je gekozen hebt is geen afbeelding - Kon de afbeelding niet converteren + Kan afbeeldingsbestand niet converteren Bestand niet gevonden Algemene I/O-fout. Misschien is er geen opslagruimte meer beschikbaar? Onbekend @@ -155,8 +157,8 @@ Avatar publiceren OpenPGP-publieke sleutel publiceren OpenPGP-publieke sleutel verwijderen - Weet je zeker dat je je publieke OpenPGP-sleutel uit je aankondiging van aanwezigheid wilt verwijderen\? -\nJe contacten zullen je geen versleutelde OpenPGP-berichten meer kunnen sturen. + Weet je zeker dat je jouw publieke OpenPGP-sleutel uit je aankondiging van aanwezigheid wilt verwijderen? +\nJouw contacten zullen je geen versleutelde OpenPGP-berichten meer kunnen sturen. OpenPGP-publieke sleutel gepubliceerd. Account inschakelen Stem opnemen @@ -232,8 +234,8 @@ Tik op avatar om een foto uit de galerij te kiezen Publiceren… De server weigerde de publicatie van je afbeelding - Kon de afbeelding niet converteren - Fout bij opslaan van avatar + Kan de afbeelding niet converteren + Kan avatar niet op schijf opslaan (Of hou lang ingedrukt om de oorspronkelijke terug te zetten) Je server ondersteunt de publicatie van avatars niet gefluisterd @@ -251,7 +253,7 @@ Nu aanvragen Negeren Beveiliging - Berichtcorrectie toestaan + Berichtcorrectie Sta je contacten toe hun berichten na het versturen te verbeteren Instellingen voor experts Wees voorzichtig met deze instellingen @@ -284,8 +286,8 @@ XMPP-adres gekopieerd naar klembord Foutmelding gekopieerd naar klembord webadres - 2D-streepjescode scannen - 2D-streepjescode tonen + QR-code scannen + QR-code weergeven Geblokkeerde contacten weergeven Accountgegevens Bevestigen @@ -294,10 +296,10 @@ Belet het besturingssysteem je verbinding te onderbreken Back-up creëren Back-upbestanden worden opgeslagen in %s - Bezig met creëren van back-upbestanden... + Back-upbestanden aanmaken Je back-up is opgeslagen De back-upbestanden zijn opgeslagen in %s - Bezig met herstellen van back-up... + Back-up terugzetten Je back-up is hersteld Vergeet niet om de account in te schakelen. Bestand kiezen @@ -324,7 +326,7 @@ Geen verdere geschiedenis op server Bijwerken… Wachtwoord gewijzigd! - Kon wachtwoord niet wijzigen + Kan wachtwoord niet wijzigen Wachtwoord wijzigen Huidig wachtwoord Nieuw wachtwoord @@ -345,11 +347,11 @@ Eigenaarsprivileges intrekken Verwijderen uit groepsgesprek Verwijderen uit kanaal - Kon aansluiting niet wijzigen + Kan de aansluiting van %s niet wijzigen Verbannen uit groepsgesprek Verbannen uit kanaal Nu verbannen - Kon rol van %s niet wijzigen + Kan rol van %s niet wijzigen Instellingen voor privégroep Instellingen voor openbaar kanaal Privé, enkel leden @@ -357,7 +359,7 @@ Kanaal modereren Je neemt geen deel Gespreksopties aangepast! - Kon gespreksopties niet aanpassen + Kan groepschatopties niet wijzigen Nooit Voor onbepaalde duur Sluimeren @@ -374,8 +376,8 @@ Android-applicatie Contact Avatar is gepubliceerd! - Bezig met versturen van %s - Bezig met aanbieden van %s + Versturen %s + Aanbieden %s Offline contacten verbergen %s is aan het typen… %s is gestopt met typen @@ -411,15 +413,15 @@ Dit is geen geldige gebruikersnaam Downloaden mislukt: server niet gevonden Downloaden mislukt: bestand niet gevonden - Downloaden mislukt: kon geen verbinding maken met host - Download mislukt: kon bestand niet schrijven + Downloaden mislukt: kan geen verbinding maken met host + Download mislukt: kan bestand niet schrijven Tor-netwerk niet beschikbaar Bindingsfout Gebroken Aanwezigheid Trillen behandelen als stille modus - Uitgebreide verbindingsinstellingen - Toon hostnaam- en poortinstellingen bij instellen van een account + Hostname & poort + Uitgebreide verbindingsinstellingen weergeven bij het instellen van een account xmpp.voorbeeld.be Archiefvoorkeuren Voorkeuren voor archief aan serverzijde @@ -443,7 +445,7 @@ %d berichten Laad meer berichten - Synchroniseer met contacten + Integratie met contactenlijst Melding bij alle berichten Melding enkel wanneer aangesproken Meldingen uitgeschakeld @@ -510,13 +512,13 @@ Vingerafdruk kopiëren Geverifieerde vingerafdrukken Gebruik de camera om de streepjescode van een contact te scannen - De sleutels worden opgehaald. Even geduld. + Wacht tot de sleutels zijn opgehaald Delen als streepjescode Delen als XMPP-URI Delen als HTTP-link Blindelings vertrouwen vóór verificatie Onvertrouwd - Ongeldige 2D-streepjescode + Ongeldige QR-code Cache wissen Privéopslag wissen Privéopslag waar bestanden worden bijgehouden wissen (de bestanden kunnen opnieuw gedownload worden van de server) @@ -548,7 +550,7 @@ %d maand - %d maand + %d maanden Automatisch berichten verwijderen Verwijder automatisch berichten van dit apparaat ouder dan de ingestelde tijdsperiode. @@ -578,12 +580,10 @@ Bericht gekopieerd naar klembord Bericht Privéberichten zijn uitgeschakeld - Beschermde apps - Om meldingen te blijven ontvangen, zelfs wanneer het scherm uit staat, moet je Conversations toevoegen aan de lijst met beschermde apps. Onbekend certificaat aanvaarden? Het servercertificaat is niet ondertekend door een gekende certificaatautoriteit. Verkeerde servernaam aanvaarden? - De server kon niet authenticeren als ‘%s’. Het certificaat is enkel geldig voor: + Kan de server niet verifiëren als ‘%s’. Het certificaat is alleen geldig voor: Wil je toch verbinding maken? Certificaatgegevens: Eenmalig @@ -600,7 +600,7 @@ Ontwerp: OMEMO-versleuteling OMEMO zal altijd gebruikt worden voor één-op-één- privégroepsgesprekken. - OMEMO zal standaard gebruikt worden voor nieuwe gesprekken. + OMEMO wordt standaard gebruikt voor nieuwe gesprekken. OMEMO zal uitdrukkelijk ingeschakeld moeten worden voor nieuwe gesprekken. Snelkoppeling aanmaken Standaard aan @@ -627,7 +627,7 @@ XMPP-adres kopiëren Bestanden delen via HTTP voor S3 Rechtstreeks zoeken - Open het toetsenbord op het scherm ‘Gesprek starten’ en plaats de cursor in het zoekveld + Open op het scherm \'Nieuwe chat\' het toetsenbord en plaats de cursor in het zoekveld Gespreksafbeelding Host ondersteunt geen gespreksafbeeldingen Enkel de eigenaar kan de gespreksafbeelding wijzigen @@ -670,18 +670,18 @@ %s verifiëren %s.]]> We hebben je nóg een sms gestuurd met 6-cijferige code. - Voer de 6-cijferige code hieronder in. + Voer hieronder de 6-cijferige code in. Sms opnieuw versturen Sms opnieuw versturen (%s) Even geduld (%s) - terug + Terug Mogelijke pincode is automatisch van het klembord geplakt. Voer je 6-cijferige code in. Weet je zeker dat je de registratieprocedure wilt stopzetten? Ja Nee - Bezig met verifiëren… - Bezig met aanvragen van sms… + Verifiëren… + SMS aanvragen… De ingevoerde code is onjuist. De toegestuurde code is verlopen. Onbekende netwerkfout. @@ -708,7 +708,7 @@ e-boek Origineel (zonder compressie) Openen met… - Conversations-profielafbeelding + Conversations profielafbeelding Kies een account Back-up herstellen Herstellen @@ -725,7 +725,7 @@ Voer een naam in voor het kanaal Voer een XMPP-adres in Dit is een XMPP-adres. Voer een naam in. - Bezig met creëren van openbaar kanaal... + Openbaar kanaal aanmaken… Dit kanaal bestaat al Je hebt deelgenomen aan een bestaand kanaal Iedereen mag het onderwerp aanpassen @@ -767,7 +767,7 @@ Je microfoon is niet beschikbaar Je kunt slechts één gesprek tegelijk voeren. Terug naar lopend gesprek - Kon camera niet wisselen + Kan camera niet wisselen Bovenaan vastzetten Bovenaan losmaken Kon bericht niet corrigeren @@ -784,8 +784,8 @@ Bekijk %1$d deelnemers - Een bericht kon niet worden afgeleverd - Sommige berichten konden niet worden afgeleverd + Een bericht kan niet worden afgeleverd + Sommige berichten kunnen niet worden afgeleverd Mislukte afleveringen Meer opties @@ -797,9 +797,312 @@ Kan video niet schakelen. Onversleuteld document Accountregistraties zijn niet ondersteund - Door crashrapportages te versturen help je de ontwikkeling + Door foutrapportages te versturen help je de ontwikkeling Inloggen met certificaat Chat archiveren Kan niet verbinden met OpenKeychain Chat starten + Kan back-up niet herstellen. + Kan back-up niet decoderen. Is het wachtwoord correct? + Incompatibele client + Uitgelogd + Stuur crashrapporten + Kan certificaat niet verwerken + Kan archiveringsvoorkeuren niet ophalen + Kan oproep niet verbinden + Wil je de bladwijzer voor %s verwijderen en de gesprekken archiveren? + Waarschuwing: Dit verzenden zonder updates van wederzijdse aanwezigheid kan onverwachte problemen veroorzaken. +\n +\nGa naar “Contactgegevens ” om je aanwezigheidsabonnementen te verifiëren. + Bladwijzers synchroniseren + Stel de vlag “autojoin ” in bij het invoeren of verlaten van een MUC en reageer op wijzigingen die door andere clients zijn aangebracht. + Klaar om bestand te delen + Bestand verwijderd + Kan geen tijdelijk bestand aanmaken + Delen met… + Chat daarna verwijderen + Wil je de bladwijzer voor %s verwijderen? + %1$s gebruikt <b>OpenKeychain</b> om berichten te versleutelen en te decoderen en het beheer van je openbare sleutels. <br>< br>Het is gelicentieerd onder GPLv3+ en beschikbaar op F-Droid en Google Play<br><br><small>(Start %1$s hierna opnieuw.)</small> + Kan het bericht niet versleutelen omdat de contactpersoon hun openbare sleutel niet aankondigt. +\n +\nVraag de contactpersoon om OpenPGP in te stellen. + Nieuwe chat + Kan geen groepschat aanmaken + Kan coderingssleutels niet ophalen + Weet je zeker dat je je account wilt verwijderen? Als je je account verwijdert, wordt je hele chatgeschiedenis gewist + Kan deze actie niet uitvoeren + Wil je alle berichten in deze chat verwijderen? +\n +\nWaarschuwing: Dit heeft geen invloed op berichten die zijn opgeslagen op andere apparaten of servers. + Kan opname niet starten + Kan opname niet opslaan + Kan kanaalconfiguratie niet opslaan + Kan het bericht niet versleutelen omdat de contactpersonen hun openbare sleutel niet aankondigen. +\n +\nVraag de hen om OpenPGP in te stellen. + Kan account niet bijwerken + Kan apparaatlijst niet ophalen + Kan geen verbinding maken met de server. + Kan account niet van server verwijderen + kan het bestand niet delen + Er zijn geen bruikbare sleutels beschikbaar voor deze contactpersoon. +\nKan geen nieuwe sleutels van de server halen. Misschien is er iets mis met de server van de contactpersoon? + De app die je gebruikte om deze afbeelding te selecteren, bood niet genoeg rechten om het bestand te lezen. +\n +\nGebruik een andere bestandsbeheerder om een afbeelding te kiezen. + Vraag eerst aanwezigheidsupdates aan bij je contactpersoon. +\n +\nDit wordt gebruikt om te bepalen welke chat-app jouw contactpersoon gebruikt. + Verstuur versleuteld bericht + Kennisgevingsgeluid + Kennisgevingsgeluid voor nieuwe berichten + Ringtone voor inkomende oproepen + De tijdsduurmeldingen worden gedempt na het detecteren van activiteit op een van je andere apparaten. + App-inhoud verbergen in de app-schakelaar en schermopnames blokkeren + De app die je gebruikte om dit bestand te delen, bood onvoldoende rechten. + Aanmelding wordt niet ondersteund door server + XEP-0215: External Service Discovery + Versleuteld bericht. Installeer OpenKeychain om het te decoderen. + Groepsberichten + Je hebt deze groepschat verlaten om technische redenen + gehost op %s + Kan server niet vinden. + Kan geen veilige verbinding tot stand brengen. + Knop “Verzenden” vervangen door snelle actie + %1$s heeft de groepschat verlaten + Groepschats doorzoeken + CAPTCHA vereist + Onvertrouwde certificaatketen + Geen app om koppeling te openen + Geen app om contact te bekijken + vector-afbeelding + multimediabestand + Audioboek + Download mislukt: Ongeldig bestand + Afwezig wanneer het apparaat is vergrendeld + Afwezig melden als het apparaat vergrendeld is + Bezet melden als het apparaat in stille modus is + Tekst gedeeld met %s + Er zijn geen bruikbare sleutels beschikbaar voor dit contact. +\nZorg ervoor dat jullie allebei een aanwezigheidsabonnement hebben. + Je probeert %s te verwijderen van een publiek kanaal. De enige manier om dat te doen, is door die gebruiker voor altijd te verbannen. + Bestand gedeeld met %s + Afbeelding gedeeld met %s + Afbeeldingen gedeeld met %s + Server- of .onion-adres + Gebruik de Enter-toets om een bericht te verzenden. Je kunt altijd Ctrl + Enter gebruiken om een bericht te verzenden, zelfs als deze optie is uitgeschakeld. + Bezet melden als het apparaat op trillen staat + Weet je zeker dat je alle andere apparaten uit de OMEMO-aankondiging wilt verwijderen? De volgende keer dat je apparaten verbinding maken, zullen ze zichzelf opnieuw aankondigen, maar ontvangen ze mogelijk geen berichten die ondertussen zijn verzonden. + De server is niet verantwoordelijk voor dit domein + Chat gearchiveerd + Geen toestemming om te bellen + Je hebt de vingerafdruk van deze persoon al vertrouwd. Door \"Gereed\" te selecteren, bevestig je alleen dat %s deel uitmaakt van deze groepschat. + Doe mee aan het gesprek + Welkom bij Quicksy! + Akkoord en doorgaan + Quicksy vraagt toestemming om je gegevens te gebruiken + Bezet in stille modus + Verleen %1$s toegang tot externe opslag + Verleen %1$s toegang tot de camera + Tip: Gebruik \'Bestand kiezen\' in plaats van \'Afbeelding kiezen\' om afzonderlijke afbeeldingen ongecomprimeerd te verzenden, ongeacht deze instelling. + Jouw apparaat maakt gebruik van zware batterij-optimalisaties voor %1$s, wat kan leiden tot vertraagde meldingen of zelfs berichtverlies. +\nHet wordt aanbevolen om deze uit te schakelen. + Jouw apparaat maakt gebruik van zware batterij-optimalisaties voor %1$s, wat kan leiden tot vertraagde meldingen of zelfs berichtverlies. +\n +\nJe wordt nu gevraagd om deze uit te schakelen. + Je bent afgemeld bij dit account + Beveiligingsfout: Ongeldige bestandstoegang! + Er is geen app om URI te delen + Laat je contacten zien wanneer je de app voor het laatst hebt gebruikt + Licht + Donker + Toestemming verlenen om internet te gebruiken + Meld dit XMPP-adres voor spammen. + Je OMEMO-sleutels opnieuw genereren. Al je contacten zullen je opnieuw moeten verifiëren. Gebruik dit alleen als laatste redmiddel. + Jouw besturingssysteem verhindert %1$s om toegang te krijgen tot internet op de achtergrond. Om meldingen van nieuwe berichten te ontvangen, moet je %1$s onbeperkte toegang toestaan wanneer \"Databesparing\" is ingeschakeld. +\n%1$s zal zich nog steeds inspannen om gegevens op te slaan waar mogelijk. + Jouw apparaat biedt geen ondersteuning voor het uitschakelen van gegevensbesparing voor %1$s. + Je hebt alle OMEMO-sleutels die je bezit geverifieerd + Vertrouw nieuwe apparaten van niet-geverifieerde contacten, maar bevestig nieuwe apparaten onmiddellijk handmatig voor geverifieerde contacten. + Cachemap opschonen (gebruikt door camera-app) + Bijbehorende chats gearchiveerd. + Meldingen van berichten en oproepen van vreemden. + Kleurrijke chatbubbels + Duidelijke achtergrondkleuren voor verzonden en ontvangen berichten + Dynamische kleuren + Systeemkleuren (Material You) + Lopende oproepen + Gemiste oproepen + Doorgaan + Geen app om website te openen + Let op-meldingen weergeven + Er is een handleiding opgezet voor het aanmaken van een account op conversations.im. +\nWanneer je conversations.im als provider kiest, kun je communiceren met gebruikers van andere providers door hen jouw volledige XMPP-adres te geven. + %1$s verwerkt jouw contactenlijst lokaal, op jouw apparaat, om u de namen en profielfoto\'s te tonen voor overeenkomende contacten op XMPP. +\n +\nGegevens uit de contacten lijst blijven te allen tijde op jouw apparaat! + De barcode bevat geen vingerafdrukken voor deze chat. + Weet je zeker dat je de verificatie van dit apparaat wilt verwijderen? +\nDit apparaat en berichten ervan worden gemarkeerd als \'Niet vertrouwd\'. + %1$s kan geen versleutelde berichten verzenden naar %2$s. Dit kan te wijten zijn aan het feit dat jouw contact een verouderde server of client gebruikt die OMEMO niet aankan. + Laatst gezien + Opnieuw verbinding maken op een andere host + Blindelings vertrouwde OMEMO-sleutels, wat betekent dat ze iemand anders kunnen zijn of dat iemand kan hebben ingetikt. + Je staat op het punt om de OMEMO-sleutels van je eigen account te verifiëren. Dit is alleen veilig als je deze link hebt gevolgd van een betrouwbare bron waar alleen jij deze link had kunnen publiceren. + Inkomende oproepen + Probeer geen back-ups terug te zetten die je niet zelf hebt aangemaakt! + Verleen %1$s toegang tot de microfoon + Deze meldingscategorie wordt gebruikt om een permanente melding weer te geven die aangeeft dat %1$s actief is. + Oproepen + Mislukte leveringen + Instellingen voor berichtmeldingen + Meldingsinstellingen voor inkomende oproepen + Geen (gedeactiveerd) + Account van server verwijderen + Melding verbergen + Lopend videogesprek + Oproep opnieuw verbinden + Inkomende oproep (%s) · %s + Uitgaande oproep + Uitgaande oproep · %s + Back-up + Apparaten ontdekken + Gaat over + Storing met app + Probleem met verificatie + Ophangen + Lopend gesprek + Persoon is niet beschikbaar + Verbinding verbroken + Ingetrokken oproep + Uitgaande oproep (%s) · %s + Gemiste oproep · %s + Gemiste oproep + Audiogesprek + Hulp + Tijdelijke authenticatiefout + Geen XMPP-adres gevonden + Avatar verwijderen + Oproepen zijn uitgeschakeld bij gebruik van Tor + Overschakelen naar video + Verzoek om overschakeling naar video afwijzen + UnifiedPush Distributor + XMPP-account + Het account waarmee pushberichten worden ontvangen. + Push-server + Weigeren + + %1$d gemiste oproep van %2$s + %1$d gemiste oproepen van %2$s + + Oproepen + Schakel een account in + Inkomende video-oproep + Overschakelen naar videogesprek? + Extra sporen toevoegen? + Verbinden + Verbonden + Opnieuw verbinden + Overschakelen naar chat + Afsluiten + Oproep aannemen + Oproep beëindigen + Antwoorden + Afwijzen + GPX-route + Inloggen + Uitloggen + + %1$d gemiste oproep van %2$d contactpersoon + %1$d gemiste oproepen van %2$d contactpersonen + + De meeste gebruikers moeten ‘jabber.network’ kiezen voor betere suggesties uit het hele openbare XMPP-ecosysteem. + Tor uitschakelen voor oproepen + Video-oproep opnieuw verbinden + Server biedt geen ondersteuning voor het aanmaken van uitnodigingen + Je probeert een verouderd back-upbestandsformaat te importeren + De app voor delen heeft geen toestemming gegeven voor toegang tot dit bestand. + Groepschats en kanalen + jabber.network + Channel discovery-methode + Inkomende oproep + Inkomende oproep + + %d gemiste oproep + %d gemiste oproepen + + Versleuteld met OpenPGP + Antwoordbericht opnemen + Contactpersoon toevoegen, groepschat aanmaken of erbij aansluiten, of kanalen ontdekken + Uitstelperiode, Beltoon, Trilling, Vreemden + Verzenden + Ontvangen + Automatisch downloaden + Uiterlijk + Licht/donker thema + Schermopnames toestaan + App-inhoud weergeven in app-schakelaar en schermopnames toestaan + End-to-end-versleuteling + Certificaatautoriteiten + CA-certificaten van het systeem vertrouwen + E2E-versleuteling, Blind vertrouwen voor verificatie, MITM-detectie + Eenmalige back-up aanmaken + Herhaalde back-up + Eenmalig aanmaken, Herhaald inplannen + Meldingen op volledig scherm + Contactlijstintegratie is niet beschikbaar + Notification relay for UnifiedPush compatible third party apps + Spam rapporteren + Privéberichten toestaan + Accepteer uitnodigingen om chats van vreemden te groeperen + Uitnodigingen van vreemden + Groot lettertype + Vergroot de lettergrootte in berichtbubbels + Je gebruikt niet-geverifieerde apparaten. Scan de QR-code op jouw andere apparaten om verificatie uit te voeren en actieve MITM-aanvallen te belemmeren. + Toestaan dat deze app inkomende oproepmeldingen weergeeft op het volledige scherm wanneer het apparaat is vergrendeld. + Jouw contactpersoon maakt gebruik van niet-geverifieerde apparaten. Scan hun QR-code om verificatie uit te voeren en actieve MITM-aanvallen te belemmeren. + Oproepintegratie niet beschikbaar! + In de rol van een UnifiedPush Distributor wordt de permanente, betrouwbare en batterijvriendelijke XMPP-verbinding gebruikt om andere UnifiedPush-compatibele apps als Tusky, Ltt.rs, FluffyChat etc. uit de slaapstand te halen. + Bestandsgrootte, Beeldcompressie, Videokwaliteit + Hostnaam & poort, Tor, Channel Discovery + Channel Discovery gebruikt een externe dienst <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Bij het gebruik van deze functie, worden jouw IP-adres en zoektermen naar deze dienst verzonden. Lees de <a href=https://search.jabber.network/privacy>Privacy Policy</a> voor meer informatie. + Spam rapporteren en spammer blokkeren + Privacybeleid + Geen clientcertificaat geselecteerd! + Interface + Beveiliging + Meldingen + Thema, Kleuren, Schermopnames, Invoer + Chat archiveren & verwijderen + Kanaalbinding vereisen + Kanaalbinding kan sommige machine-in-the-middle-aanvallen detecteren + Server-verbinding + Besturingssysteem + Typemeldingen, Laatst gezien, Beschikbaarheid + Hostnaam & poort, Tor + Toetsenbord + Meldingen van betrokkenheid + Toepassing + Interactie + Op apparaat + Niet-ondersteunde actie + Jouw avatar. Tik om een nieuwe avatar uit de galerij te selecteren. + Video kan niet worden uitgeschakeld. + Bijnaam bewerken + OpenPGP-sleutel verwijderen + Naam en onderwerp bewerken + Configuratie wijzigen + Meldingsinstellingen wijzigen + Oproep maakt gebruik van luidspreker. Tik om over te schakelen naar de oortelefoon. + Oproep maakt gebruik van een oortelefoon. + Oproep maakt gebruik van bedrade headset + Oproep maakt gebruik van luidspreker. + Oproep maakt gebruik van bluetooth. + Camera omdraaien + Video is ingeschakeld. Tik om uit te schakelen. + Oproep maakt gebruik van een oortelefoon. Tik om over te schakelen naar de luidspreker. + Video is uitgeschakeld. Tik om in te schakelen. + XEP-0388: Extensible SASL Profile + Inlogmethode + XEP-0386: Bind 2 \ No newline at end of file diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 24171039c860ab91601eca80dce6223d04c6522a..9ce7c0cdf2c04220d9dc29b8202aa00d2b9c67b5 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -9,7 +9,7 @@ Dodaj konto Edytuj nazwę Dodaj do kontaktów - Usuń z rostera + Usuń z rostera Zablokuj kontakt Odblokuj kontakt Zablokuj domenę @@ -190,16 +190,16 @@ Brak pamięci. Obraz jest za duży Czy chcesz dodać %s do listy kontaktów? Informacje o serwerze - XEP-0313: MAM - XEP-0280: Kopie wiadomości - XEP-0352: Wskaźnik stanu klienta - XEP-0191: Polecenia Blokujące - XEP-0237: Roster Versioning - XEP-0198: Zarządzanie Strumieniem - XEP-0215: Wykrywanie Zewnętrznych Usług - XEP-0163: PEP (Awatary / OMEMO) - XEP-0363: Przesyłanie plików przez HTTP - XEP-0357: Push + XEP-0313: zarządzanie archiwami wiadomości + XEP-0280: kopie wiadomości + XEP-0352: wskazywanie stanu klienta + XEP-0191: polecenia blokujące + XEP-0237: wersjonowanie rostera + XEP-0198: zarządzanie strumieniem + XEP-0215: wykrywanie zewnętrznych usług + XEP-0163: protokół osobistych zdarzeń (awatary/OMEMO) + XEP-0363: wysyłanie plików przez HTTP + XEP-0357: powiadomienia Push dostępny niedostępny Brak informacji o kluczu publicznym @@ -696,8 +696,6 @@ Wiadomość skopiowana do schowka Wiadomość Prywatne wiadomości są wyłączone - Aplikacje chronione - Aby otrzymywać powiadomienia nawet kiedy ekran jest wyłączony musisz dodać Conversations do listy chronionych aplikacji. Zaakceptować nieznany certyfikat? Certyfikat serwera nie jest podpisany przez znany Urząd Certyfikacji. Czy zaakceptować niepasującą nazwę serwera? @@ -1046,7 +1044,7 @@ Czy chcesz usunąć zakładkę dla %s i zarchiwizować rozmowę? Archiwizuj rozmowę Nowa rozmowa - Archiwizuj tę rozmowę + Usuń rozmowę później Kolorowe dymki rozmowy Kod kreskowy nie zawiera odcisków palca dla tej rozmowy. Powiązane rozmowy zarchiwizowane. @@ -1101,4 +1099,23 @@ Pełnoekranowe powiadomienia Nieobsługiwana operacja Pozwól tej aplikacji na pokazywanie powiadomień o przychodzącym połączeniu, które zajmują cały ekran gdy urządzenie jest zablokowane. + Pozwól na prywatne wiadomości + Edytuj nazwę + Usuń klucz OpenPGP + Edytuj nazwę i temat + Zmień konfigurację + Zmień ustawienia powiadomień + Rozmowa używa słuchawek. Dotknij aby przełączyć na głośnik. + Rozmowa używa słuchawek. + Rozmowa używa przewodowego zestawu słuchawkowego + Rozmowa używa głośnika. Dotknij aby przełączyć na słuchawki. + Rozmowa używa głośnika. + Rozmowa używa Bluetooth. + Odwróć kamerę + Wideo jest włączone. Dotknij aby wyłączyć. + Wideo jest wyłączone. Dotknij aby włączyć. + Nie udało się wyłączyć wideo. + Twój awatar. Dotknij aby wybrać nowy awatar z galerii. + XEP-0388: rozszerzalny profil SASL + XEP-0386: uproszczone nawiązywanie połączenia diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index f263d394e4b6d7bc8645d79a4af424df5b9154b5..c904970df75b46043503e760d31033b6fd445055 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -40,7 +40,7 @@ Moderador Participante Visitante - Deseja remover %s da sua lista de contatos? As conversas associadas a esse contato não serão removidas. + Gostaria de remover %s da sua lista de contatos? O chat com este contato não será removido. Deseja bloquear o recebimento de mensagens de %s? Deseja desbloquear o recebimento de mensagens de %s? Bloquear todos os contatos de %s? @@ -78,19 +78,19 @@ Preparando para enviar as imagens Compartilhando arquivos. Por favor, aguarde... Limpar o histórico - Limpa o histórico de conversas + Limpar histórico do chat Deseja excluir todas as mensagens dessa conversa? \n \nAtenção: Isso não afetará mensagens armazenadas em outros dispositivos ou servidores. Excluir arquivo Deseja realmente excluir este arquivo?\n\nAtenção: Isso não excluirá cópias deste arquivo armazenadas em outros dispositivos ou servidores. Selecione o dispositivo - Enviar mensagem não criptografada + Enviar mensagem de texto não criptografada Enviar mensagem Enviar mensagem para %s Enviar mensagem criptografada via v\\OMEMO Novo apelido em uso - Enviar descriptografada + Enviar texto não criptografada Não foi possível descriptografar. Talvez você não tenha a chave privada apropriada. OpenKeychain OpenKeychain para criptografar e descriptografar as mensagens e gerenciar suas chaves públicas.

Ele está licenciado sob a GPLv3+ e está disponível no F-Droid e na Google Play.

(Por favor reinicie o %1$s em seguida).]]>
@@ -175,7 +175,7 @@ Tem certeza que deseja remover sua chave pública OpenPGP do seu anúncio de presença?\nSeus contatos não poderão mais enviar mensagens criptografadas com o OpenPGP para você. A chave pública do OpenPGP foi publicada. Habilitar a conta - Se você excluir a sua conta todo o seu histórico de conversas será apagado + Tem certeza de que deseja excluir sua conta? Excluir sua conta apaga todo o seu histórico do chat Gravar voz Endereço XMPP Bloquear endereço XMPP @@ -678,8 +678,6 @@ A mensagem foi copiada para a área de transferência Mensagem As mensagens privadas estão desabilitadas - Apps protegidos - Para continuar recebendo notificações, mesmo com a tela apagada, você precisa adicionar o Conversations à lista de apps protegidos. Aceitar certificado desconhecido? O servidor do certificado não está assinado por uma autoridade certificadora reconhecida. Aceitar nome de servidor não correspondente? @@ -939,7 +937,7 @@ Chamada de vídeo Ajuda Seu microfone não está disponível - Você só pode ter uma chamada de cada vez + Você só pode fazer uma chamada por vez Retornar para a chamada em andamento Não foi possível trocar a câmera Fixar no topo @@ -985,4 +983,43 @@ As chamadas estão desabilitadas ao usar Tor Mudar para vídeo Recusar requisição de mudança para vídeo - \ No newline at end of file + Enviar relatórios de erro + Gostaria de remover o favorito de %s e arquivar chat? + Exclua o chat depois + Compartilhar com… + Arquivar chat + Novo chat + Enviar mensagem criptografada + Desconectado + Gostaria de remover o favorito de %s? + Operação não suportada + Deletar e arquivar chat + Seu contato tem dispositivos não verificados. Escaneie o Código QR dele para fazer uma verificação e impedir ataques MITM. + Sair + Você está usando dispositivos não verificados. Escaneie o Código QR em outro dispositivo para fazer a verificação, e impedir ataques MITM. + Trocar para o chat + Interface + Tema, Cores, Capturas de tela, Entrada + Segurança + Encriptação E2E, Confiar cegamente antes da verificação, Detecção de MITM + Notificações + Tamanho do arquivo, Compressão de imagem, Qualidade de vídeo + Confiar nos certificados CA do sistema + Política de privacidade + A integração de lista de contatos não está disponível + Entrar + Modo claro/escuro + Permitir capturas de tela + Permitir mensagens privadas + Reportar spam + Reportar e bloquear o spammer + A integração de chamada não está disponível! + Iniciar chat + Nenhum certificado de cliente selecionado! + Enviando + Recebendo + Download automático + Aparência + Conexão com o servidor + Sistema Operacional + diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index a394a13747e50ff22f91db32aff8f1803eacc5d7..8ff8f764a373126f8670d755140b4e1c8b92ed7f 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -688,8 +688,6 @@ Mesaj copiat în clipboard Mesaj Mesajele private sunt dezactivate - Aplicații protejate - Pentru a continua să primiți notificări, chiar și când ecranul este oprit, trebuie să adăugați Conversations în lista de aplicații protejate. Acceptați certificatul necunoscut? Certificatul serverului nu este semnat de o autoritate de certificare (CA) cunoscută. Acceptați numele serverului ce nu corespunde? @@ -1088,4 +1086,21 @@ Notificări pe tot ecranul Operațiune neacceptată Atunci când dispozitivul este blocat permite aplicației să arate notificările apelurilor pe tot ecranul. + Permite mesaje private + Nu s-a putut dezactiva videoul. + Avatarul dumneavoastră. Atingeți pentru a selecta un nou avatar din galerie. + Schimbă setările notificărilor + Schimbă configurația + Editare nume și subiect + Ștergere cheie OpenPGP + Editare nume + Video dezactivat. Atingeți pentru activare. + Video activat. Atingeți pentru dezactivare. + Întoarce camera + Apelul folosește Bluetooth. + Apelul folosește difuzorul. + Apelul folosește un set de căști cu fir + Apelul folosește receptorul. + Apelul folosește difuzorul. Atingeți pentru a trece la receptor. + Apelul folosește receptorul. Atingeți pentru a trece la difuzor. diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index e93016e87e6a677062844d383e23e6e071705761..694c2bb2cec38d44cb6793509e1166ad3663f06b 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -41,7 +41,7 @@ Модератор Участник Посетитель - Вы хотите удалить %s из своего списка контактов? Беседы, связанные с этим контактом, будут сохранены. + Вы хотите удалить %s из своего списка контактов? Чат с этим контактом удалён не будет. Вы хотите заблокировать дальнейшие сообщения от %s? Вы хотите разблокировать пользователя %s? Заблокировать всех пользователей домена %s? @@ -79,7 +79,7 @@ Подготовка к передаче изображений Обмен файлами. Пожалуйста, подождите… Очистить историю - Очистить историю + Очистить историю чата Вы хотите удалить все сообщения в этой беседе? \n \nВнимание: Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах. @@ -88,12 +88,12 @@ \n \nПредупреждение: Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. Выберите устройство - Нешифрованное сообщение + Отправить сообщение чистым текстом Сообщение Сообщение для %s v\\OMEMO-зашифр. сообщение Используется новое имя - Отправить в незашифрованном виде + Отправить чистый текст Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа. Установите OpenKeychain OpenKeychain для шифрования и дешифрования сообщений и управления открытыми ключами.

OpenKeychain распространяется под лицензией GPLv3+ и доступна для загрузки через F-Droid или Google Play.

(Потребуется перезапуск %1$s после установки.)]]>
@@ -177,7 +177,7 @@ Вы действительно хотите удалить ваш OpenPGP публичный ключ из опубликованных?\nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения. Публичный ключ OpenPGP опубликован. Включить аккаунт - Вы точно хотите удалить свою учётную запись? Это удалит все истории диалогов + Вы точно хотите удалить свою учётную запись? Удаление учётной записи сотрёт все истории диалогов Запись голоса XMPP-адрес Заблокировать XMPP-адрес @@ -274,7 +274,7 @@ Игнорировать Внимание: Если обновления присутствия не включены на обеих сторонах, это может привести к возникновению неожиданных проблем.\n\nПросмотрите сведения о контакте для проверки настроек обновлений присутствия. Безопасность - Разрешить исправление сообщений + Исправление сообщений Позволить контактам редактировать сообщения Расширенные настройки Пожалуйста, будьте осторожны с данными настройками @@ -464,8 +464,8 @@ Устанавливает статус \"Не беспокоить\", когда устройство в беззвучном режиме Не доступен в режиме вибрации Устанавливает статус \"Не беспокоить\", когда устройство в режиме вибрации - Расширенные настройки подключения - Показывать имя сервера и порт в настройках аккаунтов + Имя хоста и Порт + Показывать расширенные настройки соединения при настройке аккаунта xmpp.example.com Авторизироваться с помощью сертификата Не удалось прочитать сертификат @@ -558,7 +558,7 @@ Средний Длинный Оповещать других об использовании - Позволяет вашим контактам видеть, когда вы используете Conversations + Позволяет вашим контактам видеть, когда вы в последний раз использовали приложение Приватность Тема Выбрать цветовую палитру @@ -685,8 +685,6 @@ Сообщение скопировано в буфер обмена Сообщение Личные сообщения выключены - Защищенные приложения - Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Conversations в список защищенных приложений. Принять Неизвестный Сертификат? Этот сертификат сервера не подписан ни одним из известных центров сертификации. Принять несовпадающее имя сервера? @@ -704,14 +702,14 @@ Не удалось получить список устройств Не удалось получить ключи шифрования Подсказка: в некоторых случаях это может исправлено добавлением друг друга в список контактов. - Вы уверены, что хотите выключить OMEMO-шифрование для этой беседы? + Вы уверены, что хотите выключить OMEMO-шифрование для этого чата? \nЭто позволит администратору сервера читать ваши сообщения, но также это может быть единственным способом связи с людьми, использующими устаревшие клиенты. Отключить сейчас Черновик: OMEMO-шифрование OMEMO будет всегда использоваться для одиночных бесед и закрытых конференций. - OMEMO будет использоваться по умолчанию для новых бесед. - OMEMO нужно будет явно включать для новых бесед. + OMEMO будет использоваться по умолчанию для новых чатов. + OMEMO нужно будет явно включать для новых чатов. Создать ярлык Включено по умолчанию Выключено по умолчанию @@ -732,14 +730,14 @@ Предоставить %1$s разрешение на использование микрофона Поиск сообщений GIF - Посмотреть беседу + Посмотреть чат Расширение для обмена информацией о местонахождении Используйте расширение для обмена информацией о местонахождении вместо встроенной карты Копировать веб-адрес Копировать XMPP-адрес Файлообмен по HTTP для S3 Быстрый поиск - На экране \"Начать беседу\" открывать клавиатуру и ставить курсор в поле поиска + На экране \"Новая беседа\" открывать клавиатуру и ставить курсор в поле поиска Аватар конференции Сервер не поддерживает наличие аватар у конференций Только владелец может менять аватар конференции @@ -790,20 +788,20 @@ Подтвердите %s %s.]]> Мы отправили вам еще одну SMS с кодом из 6 цифр. - Пожалуйста, введите код из 6 цифр ниже. + Пожалуйста, введите ПИН-код из 6 цифр ниже. Отправьте заново SMS Отправьте заново SMS (%s) Пожалуйста, подождите (%s) - назад - Автоматически вставлен возможный код из буфера обмена. - Пожалуйста, введите ваш код из 6 цифр. + Назад + Автоматически вставлен возможный ПИН-код из буфера обмена. + Пожалуйста, введите ваш ПИН-код из 6 цифр. Вы уверены, что хотите прервать процедуру регистрации? Да Нет Подтверждение… Запрос SMS… - Введенный вами код некорректен. - Отправленный вам код просрочен. + Введенный вами ПИН-код некорректен. + Отправленный вам ПИН-код просрочен. Неизвестная ошибка сети. Неизвестный ответ от сервера. Не удалось подключиться к серверу. @@ -931,8 +929,8 @@ Открепить GPX-трек Не удалось исправить сообщение - Все беседы - Эта беседа + Все чаты + Этот чат Ваш аватар Аватар для %s Зашифровано с помощью OMEMO @@ -1039,8 +1037,8 @@ Интеграция вызовов недоступна! Нет разрешения на телефонный звонок Вы хотите удалить закладку для «%s»? - Закрыть и удалить - Вы хотите удалить закладку для %s и закрыть беседу? + Архивировать и удалить чат + Вы хотите удалить закладку для %s и архивировать чат? Отправить отчёты о вылетах Перейти к беседе Тема, цвета, снимки, ввод @@ -1057,7 +1055,7 @@ Обзор каналов использует сторонний сервис <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Эта функция передаст Ваш IP-адрес и ваш поисковый запрос этому сервису. Ознакомьтесь с его <a href=https://search.jabber.network/privacy>Политикой конфиденциальности</a> для получения подробностей. Архивировать беседу Новая беседа - Архивировать эту беседу + Затем удалить беседу Беседа архивирована Присоединиться к общению Штрих-код не содержит отпечатков для этой беседы. @@ -1086,4 +1084,16 @@ Соответствующие беседы архивированы. Доверять сертификатам системных УЦ Имя хоста и порт, Тор, обзор каналов - \ No newline at end of file + Разрешить личные сообщения + Принимать приглашения в групповые беседы от незнакомцев + Операция не поддерживается + Полноэкранные уведомления + Повторяющиеся рез. копии + Разрешить приложению показывать экраны входящих звонков при заблокированном экране. + Conversations, будучи распределителем UnifiedPush, будет использовать стойкое, надёжное и малопотребляющее подключение XMPP для пробуждения других совместимых с UnifiedPush приложений, таких как Tusky, Ltt.rs, FluffyChat и др. + Требовать привязку канала + Привязка канала может обнаружить некоторые атаки посредником + Соединение с сервером + Приглашения от незнакомцев + Уведомления о взаимодействии + diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index 41cb53a3a435e128bd83e960535df86c2b2ad673..23f71ebd7c652d87339f4ba0b2a246fce2df34b1 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -513,7 +513,6 @@ Mesazhi u kopjua në të papastër Mesazh Mesazhet private janë të çaktivizuara - Aplikacione të Mbrojtur Të pranohet Dëshmi e Panjohur\? Dëshmia e shërbyesit s’është nënshkruar prej një Autoriteti të njohur Dëshmish. Të Pranohet Emër Shërbyesi i Ngatërruar\? @@ -785,7 +784,6 @@ Mesazh Gabimi S’u krijua dot kartelë e përkohshme Kjo pajisje u verifikua - Që të vazhdoni të merrni njoftime, edhe kur ekrani juaj është i fikur, duhet të shtoni Conversations te lista e aplikacioneve të mbrojtur. Kjo duket si adresë kanali Thirrje audio Thirrje video @@ -1031,7 +1029,7 @@ Nisni fjalosje Arkivoje fjalosjen Fjalosje e re - Arkivoje këtë fjalosje + Fshije fjalosjen më pas Kodi me vija s’përmban shenja gishtash për këtë fjalosje. Kaloni te fjalosja Fjalosje e arkivuar @@ -1081,4 +1079,25 @@ Lejojeni këtë aplikacion të shfaqë njoftime për thirrje ardhëse që zënë krejt ekranin, kur pajisja është e kyçur. Veprim i pambuluar Kopjeruajtje ripërsëritëse + Krijoni një kopjeruajtje një here të vetme + Lejoni mesazhe private + Përpunoni nofkë + Përpunoni emër dhe temë + Ndryshoni formësim + Ndryshoni rregullime njoftimesh + Thirrja po përdor kufje. + Thirrja po përdor kufje me fill + Thirrja po përdor altoparlant. Prekeni, që të kalohet në kufje. + Thirrja po përdor altoparlant. + Thirrja po përdor Bluetooth. + Fshini kyç OpenPGP + S’u çaktivizua dot videoja. + Avatari juaj. Prekeni, që të përzgjidhni prej galerisë avatar të ri. + Thirrja po përdor kufje. Prekeni, që të kalohet në altoparlant. + Videoja është e aktivizuar. Prekeni, që të çaktivizohet. + Ktheni kamerën më anë tjetër + Videoja është e çaktivizuar. Prekeni, që të aktivizohet. + XEP-0386: Bind 2 + XEP-0388: Profil SASL i Zgjerueshëm + Mekanizëm hyrjesh \ No newline at end of file diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 3b3a46ab98f1451525ac63c8114d9b5e76616892..3782d92a5b785354f824200f770d037b5cdd1c66 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -687,8 +687,6 @@ Meddelandet har kopierats till urklipp Meddelande Privata meddelanden är inaktiverade - Skyddade appar - För att fortsätta ta emot aviseringar, även när skärmen är avstängd, måste du lägga till Conversations i listan över skyddade appar. Acceptera okänt certifikat\? Servercertifikatet är inte signerat av en känd certifikatutfärdare. Acceptera servernamn som inte matchar? diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml index e1de036bb53961aabb955f79e1a280c4da086cfd..32ef80fffca0b476ed33006e1b59d8c3863b30c4 100644 --- a/src/main/res/values-szl/strings.xml +++ b/src/main/res/values-szl/strings.xml @@ -699,8 +699,6 @@ Wiadōmość skopiyrowano do skrytki Wiadōmość Prywatne wiadōmości sōm zastawiōne - Aplikacyje chrōniōne - Coby dostować wiadōmości, kedy ekran je zastawiōny, musisz przidać Conversations do listy chrōniōnych aplikacyji. Zaakceptować niyznōmy certyfikat\? Certyfikat ôd serwera niy ma podpisany ôd znōmego Amtu Certyfikacyje. Zaakceptować niypasujōnce miano ôd serwera\? diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index a983d6ddfe1d9a4665242e29662014fa50b58437..e79bd75c794c03003ce3219a97cf49f05e7e712b 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -677,8 +677,6 @@ İleti panoya kopyalandı İleti Özel iletiler devre dışı bırakıldı - Korunan uygulamalar - Ekranınız kapalıyken bile bildirim almak için Conversations\'ı korunan uygulamalara eklemelisiniz. Bilinmeyen sertifikayı kabul et? Sunucu sertifikası bilinen bir Sertifika Yetkilisi tarafından imzalanmamış. Uyuşmayan Sunucu isimlerini kabul et? diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 0aa0a71f7299249e6f10e09b70ee5a3342a68d35..aba2d64d391afe73c3d717d50e6caf072158e0e9 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -163,7 +163,7 @@ OMEMO Вилучити обліковий запис Тимчасово вимкнути - Опублікувати піктограму користувача + Опублікувати аватар Опублікувати публічний ключ OpenPGP Вилучити публічний ключ OpenPGP Ви впевнені, що хочете вилучити свій публічний ключ OpenPGP з оголошення про присутність\? @@ -187,7 +187,7 @@ XEP-0237: Зміни у списку контактів XEP-0198: Керування потоком XEP-0215: Виявлення зовнішньої служби - XEP-0163: PEP (піктограми користувачів, OMEMO) + XEP-0163: PEP (аватари користувачів, OMEMO) XEP-0363: Обмін файлами через HTTP XEP-0357: Push-повідомлення так @@ -245,13 +245,13 @@ %1$s +%2$d дочитали до цього місця Усі прочитали до цього місця Опублікувати - Торкніться піктограми користувача, щоб вибрати зображення з галереї + Торкніться аватара, щоб вибрати зображення з галереї Публікація… Сервер відхилив Вашу публікацію Не вдалося конвертувати Ваше зображення - Неможливо зберегти піктограму користувача на пристрій + Неможливо зберегти аватар на пристрій (Або натисніть і тримайте, щоб скинути до значення за замовчуванням) - Ваш сервер не підтримує публікацію піктограм користувачів + Ваш сервер не підтримує публікацію аватарів шепоче %s Надіслати приватне повідомлення %s @@ -313,7 +313,7 @@ Деталі облікового запису Підтвердити Спробуйте ще - Фонова служба + Процес на передньому плані Не дає операційній системі припиняти Ваш зв\'язок Створити резервну копію Резервні копії зберігатимуться до %s @@ -344,7 +344,7 @@ Увімкнути сповіщення Не знайдено сервер групи Не вдалося створити групу - Піктограма облікового запису + Зображення облікового запису Копіювати цифровий підпис OMEMO Повторно створити ключ OMEMO Стерти пристрої @@ -366,7 +366,7 @@ Увімкнути всі облікові записи Вимкнути всі облікові записи Здійснити дію з - Учасник + Без ролі Поза мережею Вигнанець Учасник @@ -379,7 +379,7 @@ Відкликати права власника Вилучити з групи Прибрати з каналу - Неможливо змінити статус участі %s + Неможливо змінити роль %s Заборонити доступ до групи Вилучити з каналу Ви намагаєтеся вилучити %s з публічного каналу. Єдиним способом для цього є заблокувати користувача назавжди. @@ -409,7 +409,7 @@ документ PDF Програма Android Контакт - Піктограму користувача опубліковано! + Аватар опубліковано! Надсилання %s Пропозиція %s Сховати поза мережею @@ -464,10 +464,10 @@ xmpp.example.com Вхід із сертифікатом Не вдалося розпізнати сертифікат - Налаштування збереження - Налаштування збереження на стороні сервера - Отримання налаштувань збереження. Будь ласка, зачекайте… - Не вдалося отримати налаштування збереження + Налаштування архівації + Налаштування архівації на стороні сервера + Отримання налаштувань архівації. Будь ласка, зачекайте… + Не вдалося отримати налаштування архівації Потрібно розв\'язати головоломку Уведіть текст із зображення вище Ланцюжок сертифікатів не довірений @@ -571,7 +571,7 @@ Вилучити ідентифікаційні дані OMEMO Створити наново Ваші ключі OMEMO. Всі Ваші контакти будуть змушені підтвердити Вас знову. Використовуйте це, лише якщо немає іншого вибору. Вилучити вибрані ключі - Потрібно підключення, щоб можна було опублікувати піктограму користувача. + Потрібно підключення, щоб можна було опублікувати аватар. Показати повідомлення про помилку Повідомлення про помилку Увімкнено заощадження трафіку @@ -668,8 +668,6 @@ Повідомлення скопійовано Повідомлення Приватні повідомлення вимкнено - Захищені програми - Щоб отримувати сповіщення навіть коли екран погас, необхідно додати Conversations до списку захищених програм. Прийняти незнайомий сертифікат? Сертифікат сервера не підтверджено відомим центром сертифікації. Прийняти сервер з невідповідним ім\'ям? @@ -721,13 +719,13 @@ Доступ до файлів через HTTP для S3 Безпосередній пошук На екрані «Нова розмова» показувати клавіатуру та розміщувати курсор у полі пошуку - Піктограма групи - Сервер не підтримує піктограми груп - Лише власник може змінити піктограму групи + Зображення групи + Сервер не підтримує зображень груп + Лише власник може змінити зображення групи Ім\'я контакту Прізвисько Ім\'я - Назва не є обов\'язковою + Ім\'я вказувати необов\'язково Назва групи Цю групу закрито Неможливо зберегти запис @@ -880,8 +878,8 @@ Вхідний відеовиклик З\'єднання З\'єднано - Приймаю виклик - Завершую виклик + Приймання виклику + Завершення виклику Відповісти Відхилити Пошук пристроїв @@ -1020,9 +1018,9 @@ Не знайдено адреси XMPP Немає (вимкнено) Відхилити - Ваша піктограма - Піктограма для %s - Вилучити піктограму + Ваш аватар + Аватар для %s + Вилучити аватар Не вдалося обробити запрошення Створення запрошень не підтримується сервером Реєстрація облікових записів не підтримується @@ -1117,4 +1115,24 @@ Показувати сповіщення про вхідні виклики на весь екран, коли пристрій заблоковано. Створити резервну копію, запланувати повторюване резервування Створити резервну копію + Дозволити приватні повідомлення + Ваш аватар. Торкніться, щоб вибрати новий аватар із галереї. + Не вдалося вимкнути відео. + Видалити ключ OpenPGP + Змінити налаштування сповіщень + Змінити налаштування + Змінити прізвисько + Редагувати назву і тему + Виклик здійснюється через навушник. Торкніться, щоб переключитися на динамік. + Виклик здійснюється через навушник. + Виклик здійснюється за допомогою дротової гарнітури + Відео ввімкнено. Торкніться, щоб вимкнути. + Відео вимкнено. Торкніться, щоб увімкнути. + Виклик здійснюється через динамік. Торкніться, щоб переключитися на навушник. + Виклик здійснюється через динамік. + Виклик здійснюється через Bluetooth. + Розвернути камеру + Механізм авторизації + XEP-0386: Bind 2 + XEP-0388: Розширюваний профіль SASL diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 797e8f47981b024fb33a415d596382dd6a446084..1a57e14bb21e9f23e1d80b88f6f405adb123ba3c 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -660,8 +660,6 @@ Đã chép tin nhắn vào clipboard Tin nhắn Tin nhắn riêng tư bị tắt - Ứng dụng được bảo vệ - Để tiếp tục nhận các thông báo, kể cả khi màn hình đã tắt, bạn cần thêm Conversations vào danh sách các ứng dụng được bảo vệ. Chấp nhận chứng chỉ không xác định? Chứng chỉ máy chủ này không được một người có quyền chứng chỉ đã biết ký. Chấp nhận tên máy chủ không khớp? diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 8f0f568ff9f1cb60e07399f937b5fb4dac8541e5..03a06e72779ab867f9ed7a8e0d56dd1aacfd585f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -26,7 +26,7 @@ 1 分钟前 %d 分钟前 - %d 个未读聊天 + %d 个未读对话 正在发送… 解密消息中。请稍候… @@ -38,7 +38,7 @@ 主持人 参与者 访客 - 是否从联系人列表中移除 %s?将不会移除与对方的聊天。 + 是否从联系人列表中移除 %s?将不会移除与对方的对话。 是否屏蔽 %s 向您发送消息? 是否解除屏蔽 %s 并允许对方向您发送消息? 屏蔽来自 %s 的所有联系人? @@ -62,12 +62,12 @@ 保存 完成 %1$s 已崩溃 - 使用 XMPP 账号发送堆栈跟踪有助于 %1$s 的持续开发。 + 使用您的 XMPP 账号发送崩溃报告有助于 %1$s 的持续开发。 立即发送 不再询问 无法连接到账号 无法连接到多个账号 - 轻击即可管理账号 + 点击以管理您的账号 附加文件 对方不在您的联系人列表中,是否添加? 添加联系人 @@ -75,9 +75,9 @@ 正在准备发送图片 正在准备发送图片 分享文件中。请稍候… - 清空历史记录 + 清空聊天记录 清空聊天记录 - 是否要删除此聊天中的所有消息? + 是否要删除此对话中的所有消息? \n \n警告:存储在其他设备或服务器上的消息将不受影响。 删除文件 @@ -85,12 +85,12 @@ \n \n警告:存储在其他设备或服务器上此文件的副本将不会删除。 选择设备 - 发送明文消息 + 发送未加密消息 发送消息 发送消息至 %s 发送 v\\OMEMO 加密消息 正在使用新昵称 - 发送明文 + 发送未加密 解密失败。也许您没有正确的私钥。 OpenKeychain %1$s 使用 <b>OpenKeychain</b> 来加密和解密消息并管理公钥。<br><br>它在 GPLv3+ 许可证下授权并可在 F-Droid 和 Google Play 上获得。<br><br><small>(请之后重启 %1$s。)</small> @@ -123,7 +123,7 @@ 静默期 在其他设备上检测到活动之后,通知在此期间静音。 高级 - 通过发送堆栈跟踪,您正在帮助此应用的开发 + 通过发送崩溃报告,您可以帮助此应用的进一步开发 确认消息 让您的联系人知道您已收到并阅读了其消息 防止截屏 @@ -186,7 +186,7 @@ 录制语音 XMPP 地址 屏蔽 XMPP 地址 - username@example.com + 用户名@example.com 密码 此 XMPP 地址无效 空间不足。图片太大 @@ -234,7 +234,7 @@ 选择 此联系人已存在 加入 - channel@conference.example.com/nick + channel@conference.example.com/昵称 channel@conference.example.com 保存为书签 删除书签 @@ -259,7 +259,7 @@ %1$s 和其他 %2$d 人已阅读至此 每个人都已阅读至此 发布 - 轻击头像即可从图库中选择图片 + 点击头像从图库中选择图片 正在发布… 服务器拒绝了您的发布 无法转换图片 @@ -276,7 +276,7 @@ 跳过 禁用通知 启用 - 需要密码才能进入此群聊 + 进入群聊需要输入密码 输入密码 请先向您的联系人请求在线状态更新。 \n @@ -299,12 +299,12 @@ 通知将在免打扰时间内静音 其他 同步书签 - 在进入或离开多用户聊天时设置“自动加入”标志,并回应其他客户端所做的改变。 + 在进入或离开群聊时设置“自动加入”标志,并回应其他客户端所做的改变。 OMEMO 指纹已复制到剪贴板 - 此群聊已将您封禁了 + 禁止您进入此群聊 此群聊仅成员进入 资源限制 - 此群聊已将您踢出了 + 已从群聊中踢出了您 此群聊已关闭 您已不在群聊中 由于技术原因,您离开了群聊 @@ -357,7 +357,7 @@ 未找到可以打开链接的应用 未找到可以查看联系人的应用 动态标签 - 在联系人下方显示只读标签 + 在联系人下方显示动态标签汇总 启用通知 群聊服务器未找到 无法创建群聊 @@ -371,8 +371,8 @@ 此联系人没有可用的密钥。 \n请确保双方都有在线状态订阅。 出了点问题 - 正在从服务器获取历史记录 - 服务器上没有更多历史记录 + 正在从服务器获取聊天记录 + 服务器上没有更多聊天记录 正在更新… 密码已修改! 无法修改密码 @@ -399,17 +399,17 @@ 无法更改 %s 的从属关系 从群聊中封禁 从频道中封禁 - 您正试图从公开频道中移除 %s。唯一的办法就是永远封禁此用户。 + 您正试图从公开频道中移除 %s。唯一的办法就是永远封禁该用户。 立即封禁 无法更改 %s 的角色 私人群聊配置 公开频道配置 私人,仅成员进入 - 使 XMPP 地址对任何人可见 - 对频道进行审核 + 公开用户的 XMPP 地址 + 开启频道发言审核 您未参与 - 群聊选项修改成功! - 无法修改群聊选项 + 群聊配置修改成功! + 无法修改群聊配置 从不 直至另行通知 稍后提醒 @@ -685,8 +685,6 @@ 消息已复制到剪贴板 消息 已禁用私信 - 受保护的应用 - 为了在屏幕关闭时也能收到消息提醒,您需要将 Conversations 加入受保护的应用列表。 接受未知证书? 此服务器证书不是由已知的证书颁发机构签发的。 接受不匹配的服务器名称? @@ -704,14 +702,14 @@ 无法获取设备列表 无法获取密钥 提示:某些情况下,双方可以添加到联系人列表解决此问题。 - 是否确定要禁用此聊天的 OMEMO 加密? + 是否确定要禁用此对话的 OMEMO 加密? \n将允许服务器管理员读取您的消息,但可能是与使用过时客户端的用户交流的唯一方法。 立即禁用 草稿: OMEMO 加密 OMEMO 将始终用于一对一和私人群聊。 - 新聊天将默认使用 OMEMO。 - 新聊天必须明确开启 OMEMO。 + 新对话将默认使用 OMEMO。 + 新对话必须明确开启 OMEMO。 创建快捷方式 默认开启 默认关闭 @@ -732,14 +730,14 @@ 授予 %1$s 访问麦克风的权限 搜索消息 GIF - 查看聊天 + 查看对话 位置共享插件 使用位置共享插件代替内置地图 复制 web 地址 复制 XMPP 地址 用于 S3 的 HTTP 文件共享 直接搜索 - 在“新聊天”屏幕上打开键盘并将光标放在搜索栏中 + 在“新对话”屏幕上打开键盘并将光标放在搜索栏中 群聊头像 主机不支持群聊头像 只有所有者才能更改群聊头像 @@ -748,7 +746,7 @@ 名称 提供名称是可选的 群聊名 - 已解散此群聊 + 此群聊已经解散了 无法保存录制 前台服务 此通知类别用于显示永久通知,表明 %1$s 正在运行。 @@ -784,7 +782,7 @@ 电话号码 验证您的电话号码 Quicksy 将发送短信(运营商可能收费)来验证电话号码。请输入您的国家/地区代码和电话号码: - 我们将验证这个电话号码

%s

可以吗?是否编辑号码?
+ 我们将验证这个电话号码

%s

可以吗?是否编辑号码?
%s 不是有效的电话号码。 请输入您的电话号码。 搜索国家/地区 @@ -855,16 +853,16 @@ 此频道已存在 您已加入现有的频道 无法保存频道配置 - 允许任何人编辑话题 - 允许任何人邀请别人 - 任何人可以编辑话题。 + 允许参与者(非访客)编辑话题 + 允许任何参与者邀请其他用户 + 参与者(非访客)可以编辑话题。 所有者可以编辑话题。 管理员可以编辑话题。 - 所有者可以邀请别人。 - 任何人可以邀请别人。 - XMPP 地址对管理员可见。 - XMPP 地址对任何人可见。 - 此公开频道无参与者。邀请联系人或使用分享按钮分发其 XMPP 地址。 + 所有者可以邀请其他用户。 + 任何参与者可以邀请其他用户。 + 用户的 XMPP 地址仅管理员可见。 + 用户的 XMPP 地址对任何人可见。 + 此公开频道无参与者。邀请联系人或使用分享按钮分发频道的 XMPP 地址。 此私人群聊无参与者。 管理权限 搜索参与者 @@ -904,11 +902,11 @@ 添加额外轨道? 正在连接 已连接 - 正在重连 + 正在重新连接 正在接受通话 正在结束通话 - 应答 - 忽略 + 接听 + 拒接 正在发现设备 正在响铃 占线 @@ -920,8 +918,8 @@ 挂断 正在进行的通话 正在进行的视频通话 - 正在重连通话 - 正在重连视频通话 + 正在重新连接通话 + 正在重新连接视频通话 禁用 Tor 以进行通话 来电 未接来电 · %s @@ -947,8 +945,8 @@ 取消置顶 GPX 轨迹 无法更正消息 - 所有聊天 - 此聊天 + 所有对话 + 此对话 您的头像 %s 的头像 用 OMEMO 加密 @@ -1015,25 +1013,25 @@ 联系人不可用 没有拨打电话的权限 呼叫集成不可用! - 是否移除 %s 的书签并存档聊天? + 是否移除 %s 的书签并存档对话? 是否移除 %s 的书签? - 删除并存档聊天 + 删除并存档对话 频道发现使用称为 <a href=https://search.jabber.network>search.jabber.network</a> 的第三方服务。<br><br>使用此功能会将您的 IP 地址和搜索词传输到此服务。请参阅其 <a href=https://search.jabber.network/privacy>隐私政策</a> 以获取更多信息。 - 开始聊天 + 开始对话 分享至… 为已发送和已接收消息使用不同的背景颜色 未选择客户端证书! 彩色聊天气泡 动态色彩 系统色彩 (Material You) - 新聊天 - 之后删除聊天 - 聊天已存档 - 切换到聊天 - 存档聊天 - 二维码不包含此聊天的指纹。 + 新对话 + 之后删除对话 + 对话已存档 + 切换到对话 + 存档对话 + 二维码不包含此对话的指纹。 加入对话 - 相应的聊天已存档。 + 相应的对话已存档。 发送崩溃报告 安全 通知 @@ -1074,5 +1072,25 @@ 全屏通知 当设备锁定时,允许此应用显示占据全屏的来电通知。 不支持的操作 - 创建一次性、计划定期备份 + 创建一次性备份、计划定期备份 + 允许私信 + 编辑昵称 + 您的头像。点击从图库中选择新头像。 + 无法禁用视频。 + 删除 OpenPGP 密钥 + 编辑名称和话题 + 更改配置 + 更改通知设置 + 正在使用听筒进行通话。 + 正在使用有线耳机进行通话 + 正在使用扬声器进行通话。 + 正在使用蓝牙进行通话。 + 翻转摄像头 + 视频已禁用,点击以启用。 + 视频已启用,点击以禁用。 + 正在使用扬声器进行通话,点击切换到听筒。 + 正在使用听筒进行通话,点击切换到扬声器。 + 登录机制 + XEP-0386:绑定 2 + XEP-0388:可扩展 SASL 配置文件 diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index f13e2c8ced338decbe3db863643af2edbf20f478..48b0063c7f73fe6f7b7bae1b006c4cfc97f17941 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -620,7 +620,6 @@ 訊息已複製到剪貼簿 訊息 私密訊息已停用 - 受保護的應用程式 接受未知憑證? 伺服器憑證是由不明的憑證簽發單位所簽署。 接受不相符的伺服器名稱? @@ -918,7 +917,6 @@ 我們將驗證

%s

電話號碼是否正確,或者您想編輯這個號碼嗎?
您確定要移除此裝置的驗證嗎? \n此裝置和來自此裝置的訊息將被標示為「未受信任」。 - 若要在螢幕關閉時保持接收通知,您需要將 Conversations 加入受保護的應用程式清單。 由於「%s」,伺服器無法驗證,憑證僅對此有效: 您將要驗證您自己帳戶的 OMEMO 金鑰。僅有從可信來源 (僅有您能夠發布此連結) 跟隨此連結時才是安全的。 此通知群組用於顯示不應觸發任何音效的通知,例如在另一個裝置上啟用時 (寬限期)。 diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index d457da458089a6e14822aa5b3fe7868e6d568e52..486bb025f720404fadedd1bda7d7fcd622703031 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -1,24 +1,15 @@ - - @string/never - 256 KiB - 512 KiB - 1 MiB - 3.5 MiB - 5 MiB - 10 MiB - - + 0 - 262144 524288 1048576 3490000 5242880 10485760 - + 52428800 + 1800 diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml index ebadf82b5f43050eb8212d82b54ca80d7ab7fcd4..4642d59ecc6faaf1e6c13dd9f1909910a3c63547 100644 --- a/src/main/res/values/dimens.xml +++ b/src/main/res/values/dimens.xml @@ -38,8 +38,6 @@ 4dp 8dp - 1200dp - 0.12 256dp diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 5a801f4584d8f1475c61c4022c4cbcb4b3aae191..9248f172904a37bad5ffb72b2d621eec0e27ed4c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -203,6 +203,8 @@ XEP-0215: External Service Discovery XEP-0163: PEP (Avatars / OMEMO) XEP-0363: HTTP File Upload + XEP-0386: Bind 2 + XEP-0388: Extensible SASL Profile XEP-0357: Push available unavailable @@ -695,8 +697,6 @@ Message copied to clipboard Message Private messages are disabled - Protected Apps - To keep receiving notifications, even when the screen is turned off, you need to add Conversations to the list of protected apps. Accept Unknown Certificate? The server certificate is not signed by a known Certificate Authority. Accept Mismatching Server Name? @@ -974,6 +974,7 @@ All chats This chat Your avatar + Your avatar. Tap to select new avatar from gallery. Avatar for %s Encrypted with OMEMO Encrypted with OpenPGP @@ -1000,6 +1001,7 @@ No active accounts support this feature The backup has been started. You’ll get a notification once it has been completed. Unable to enable video. + Could not disable video. Plain text document Account registrations are not supported No Jabber ID found @@ -1073,4 +1075,20 @@ Unsupported operation Send later Options + Allow private messages + Edit nick + Delete OpenPGP key + Edit name and topic + Change configuration + Change notification settings + Call is using earpiece. Tap to switch to speaker. + Call is using earpiece. + Call is using wired headset + Call is using speaker. Tap to switch to earpiece. + Call is using speaker. + Call is using bluetooth. + Flip camera + Video is enabled. Tap to disable. + Video is disabled. Tap to enable. + Login mechanism diff --git a/src/main/res/xml/preferences_attachments.xml b/src/main/res/xml/preferences_attachments.xml index e10281424044f6d04a485da303f5207e625a7a74..ed1f5dbc48535691e42a91b523d0a06d85f4434d 100644 --- a/src/main/res/xml/preferences_attachments.xml +++ b/src/main/res/xml/preferences_attachments.xml @@ -22,8 +22,6 @@ { - final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); - final IqPacket packet = mXmppConnectionService.getIqGenerator().pushTokenToAppServer(getAppServer(), token, androidId); - mXmppConnectionService.sendIqPacket(account, packet, (a, response) -> { - final Data data = findResponseData(response); - if (response.getType() == IqPacket.TYPE.RESULT && data != null) { - try { - String node = data.getValue("node"); - String secret = data.getValue("secret"); - Jid jid = Jid.of(data.getValue("jid")); - if (node != null && secret != null) { - enablePushOnServer(a, jid, node, secret); - } - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - } else { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": failed to enable push. invalid response from app server " + response); - } - }); - }); + retrieveFcmInstanceToken( + token -> { + final String androidId = PhoneHelper.getAndroidId(mXmppConnectionService); + final var packet = + mXmppConnectionService + .getIqGenerator() + .pushTokenToAppServer(getAppServer(), token, androidId); + mXmppConnectionService.sendIqPacket( + account, + packet, + (response) -> { + final Data data = findResponseData(response); + if (response.getType() == Iq.Type.RESULT && data != null) { + final Jid jid; + try { + jid = Jid.ofEscaped(data.getValue("jid")); + } catch (final IllegalArgumentException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": failed to enable push. invalid jid"); + return; + } + final String node = data.getValue("node"); + final String secret = data.getValue("secret"); + if (node != null && secret != null) { + enablePushOnServer(account, jid, node, secret); + } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": failed to enable push. invalid response from app server " + + response); + } + }); + }); } - private void enablePushOnServer(final Account account, final Jid appServer, final String node, final String secret) { - final IqPacket enable = mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); - mXmppConnectionService.sendIqPacket(account, enable, (a, p) -> { - if (p.getType() == IqPacket.TYPE.RESULT) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully enabled push on server"); - } else if (p.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": enabling push on server failed"); - } - }); + private void enablePushOnServer( + final Account account, final Jid appServer, final String node, final String secret) { + final Iq enable = + mXmppConnectionService.getIqGenerator().enablePush(appServer, node, secret); + mXmppConnectionService.sendIqPacket( + account, + enable, + (p) -> { + if (p.getType() == Iq.Type.RESULT) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully enabled push on server"); + } else if (p.getType() == Iq.Type.ERROR) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": enabling push on server failed"); + } + }); } - private void retrieveFcmInstanceToken(final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { + private void retrieveFcmInstanceToken( + final OnGcmInstanceTokenRetrieved instanceTokenRetrieved) { final FirebaseMessaging firebaseMessaging; try { firebaseMessaging = FirebaseMessaging.getInstance(); @@ -79,26 +107,33 @@ public class PushManagementService { Log.d(Config.LOGTAG, "unable to get firebase instance token ", e); return; } - firebaseMessaging.getToken().addOnCompleteListener(task -> { - if (!task.isSuccessful()) { - Log.d(Config.LOGTAG, "unable to get Firebase instance token", task.getException()); - } - final String result; - try { - result = task.getResult(); - } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to get Firebase instance token due to bug in library ", e); - return; - } - if (result != null) { - instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result); - } - }); - + firebaseMessaging + .getToken() + .addOnCompleteListener( + task -> { + if (!task.isSuccessful()) { + Log.d( + Config.LOGTAG, + "unable to get Firebase instance token", + task.getException()); + } + final String result; + try { + result = task.getResult(); + } catch (Exception e) { + Log.d( + Config.LOGTAG, + "unable to get Firebase instance token due to bug in library ", + e); + return; + } + if (result != null) { + instanceTokenRetrieved.onGcmInstanceTokenRetrieved(result); + } + }); } - - public boolean available(Account account) { + public boolean available(final Account account) { final XmppConnection connection = account.getXmppConnection(); return connection != null && connection.getFeatures().sm() @@ -107,7 +142,9 @@ public class PushManagementService { } private boolean playServicesAvailable() { - return GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(mXmppConnectionService) == ConnectionResult.SUCCESS; + return GoogleApiAvailabilityLight.getInstance() + .isGooglePlayServicesAvailable(mXmppConnectionService) + == ConnectionResult.SUCCESS; } public boolean isStub() { diff --git a/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt b/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt index 7b75633e869fb601e192fa6e5d717c56347ce56e..b57fd6c1cbf47fa40776d93bf47663b51d410061 100644 --- a/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt +++ b/src/quicksy/fastlane/metadata/android/es-ES/full_description.txt @@ -1,14 +1,14 @@ -Quicksy es un programa que se ejecuta en el popular cliente Jabber/XMPP, con descubrimiento de contactos automatizado. +Quicksy es una bifurcación del popular cliente Jabber/XMPP Conversations con busqueda automático de contactos. -Regístrese con su número de teléfono y Quicksy, según los números de teléfono de su agenda, sugerirá automáticamente contactos potenciales. +Te registras con tu número de teléfono y Quicksy automáticamente, basándose en los números de teléfono de tu agenda, te sugerirá posibles contactos. -En esencia, Quicksy es un cliente Jabber completo que le permite comunicarse con cualquier usuario en cualquier servidor público. De manera similar, se puede contactar a los usuarios de Quicksy desde el extranjero simplemente agregando +phonenumber@quicksy.im a su lista de contactos. +Quicksy es un cliente Jabber completo que te permite comunicarte con cualquier usuario en cualquier servidor público. Asimismo, puedes contactar a los usuarios de Quicksy desde el exterior simplemente agregando +phonenumber@quicksy.im a tu lista de contactos. -Elimine la sincronización de contactos; la interfaz de usuario se deja intencionalmente lo más cerca posible de Conversations. Esto permite a los usuarios migrar, si lo desean, de Quicksy a Conversations sin tener que volver a aprender cómo funciona la aplicación. +Aparte de la sincronización de contactos, la interfaz de usuario se parece lo más posible a Conversations. Esto permite que los usuarios puedan migrar de Quicksy a Conversations sin tener que volver a aprender cómo funciona la aplicación. -Los contactos sugeridos consisten en otros usuarios de Quicksy y usuarios habituales de Jabber/XMPP que han proporcionado su ID de Jabber a la Lista de Quicksy (https://quicksy.im/#get-listed). +Los contactos sugeridos consisten en otros usuarios de Quicksy y usuarios habituales de Jabber/XMPP que han ingresado su ID de Jabber en el Directorio de Quicksy (https://quicksy.im/#get-listed). -NOTA: Para proporcionar (https://quicksy.im/enter/) su ID de Jabber a la Lista -Quicksy requiere una tarifa de registro aplicable única. +NOTA: Para ingresar (https://quicksy.im/enter/) tu ID de Jabber en Quicksy +Directorio se requiere un único pago de registro. -Para más detalles, lea la Política de Privacidad (https://quicksy.im/#privacy). +Lee la Política de privacidad (https://quicksy.im/#privacy) para obtener más información. diff --git a/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt b/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt index e988bbf912ec17fa1d7d57b59c39b05c3f1e92b4..91bf36ef1409fcbaf2312a0bfb02378d993efe34 100644 --- a/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt +++ b/src/quicksy/fastlane/metadata/android/es-ES/short_description.txt @@ -1 +1 @@ -Jabber/XMPP fácil de ingresar y fácil de descubrir +Jabber/XMPP fácil de usar y encuentra tus contactos diff --git a/src/quicksy/fastlane/metadata/android/fr-FR/full_description.txt b/src/quicksy/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..c3ba436ff7b81423624f4305ae87944b7c7124ca --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,14 @@ +Quicksy est un spin-off du client populaire Jabber/XMPP Conversations avec découverte automatique des contacts. + +Vous vous inscrivez avec votre numéro de téléphone et Quicksy va automatiquement — en se basant sur les numéros de votre carnet de contacts — vous suggérer des contacts. + +Sous le capot Quicksy est un client Jabber à part entière qui vous permet de communiquer avec n'importe quel utilisateur·ice sur n'importe quel serveur public fédéré. De même, les utilisateur·ices de Quicksy peuvent être contactés de l'extérieur simplement en ajoutant +numéro-de-téléphone@quicksy.im à votre liste de contacts. + +Outre la synchronisation des contacts, l'interface utilisateur·ice est délibérément aussi proche que possible de Conversations. Cela permet aux utilisateur·ices de migrer éventuellement de Quicksy vers Conversations sans avoir à réapprendre le fonctionnement de l'application. + +Les contacts suggérés inclus d'autres utilisateur·ices Quicksy et des utilisateur·ices Jabber/XMPP classiques qui ont entré leur identifiant Jabber dans le répertoire Quicksy (https://quicksy.im/#get-listed). + +NOTE : Pour ajouter (https://quicksy.im/enter/) votre identifiant Jabber dans le répertoire +Quicksy des frais d'inscription uniques sont requis. + +Lisez la politique de confidentialité (https://quicksy.im/#privacy) pour plus d'information. diff --git a/src/quicksy/fastlane/metadata/android/fr-FR/short_description.txt b/src/quicksy/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..8cd359341f41b3de294289a119ef2cb24bd94e30 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Entrée et découverte facile de Jabber / XMPP diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index 69cef954841e74a6fb668290f19c230d7f936e7b..d2af65f8346c4d0f70c5f4572d6330f08180a482 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -5,16 +5,36 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; -import android.preference.PreferenceManager; import android.util.Log; import com.google.common.collect.ImmutableMap; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.android.PhoneNumberContact; +import eu.siacs.conversations.crypto.TrustManagers; +import eu.siacs.conversations.crypto.sasl.Plain; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Entry; +import eu.siacs.conversations.http.HttpConnectionManager; +import eu.siacs.conversations.utils.AccountUtils; +import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; +import eu.siacs.conversations.utils.SmsRetrieverWrapper; +import eu.siacs.conversations.utils.TLSSocketFactory; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; + +import im.conversations.android.xmpp.model.stanza.Iq; + +import io.michaelrocks.libphonenumber.android.Phonenumber; + import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; @@ -38,7 +58,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -52,26 +71,6 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.android.PhoneNumberContact; -import eu.siacs.conversations.crypto.TrustManagers; -import eu.siacs.conversations.crypto.sasl.Plain; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Entry; -import eu.siacs.conversations.http.HttpConnectionManager; -import eu.siacs.conversations.utils.AccountUtils; -import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.utils.SmsRetrieverWrapper; -import eu.siacs.conversations.utils.TLSSocketFactory; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.Jid; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; -import io.michaelrocks.libphonenumber.android.Phonenumber; - public class QuickConversationsService extends AbstractQuickConversationsService { @@ -88,8 +87,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService private static final String BASE_URL = "https://" + API_DOMAIN; - private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id"; - private final Set mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>()); private final Set mOnVerification = Collections.newSetFromMap(new WeakHashMap<>()); @@ -308,16 +305,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService } private String getInstallationId() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service); - String id = preferences.getString(INSTALLATION_ID, null); - if (id != null) { - return id; - } else { - id = UUID.randomUUID().toString(); - preferences.edit().putString(INSTALLATION_ID, id).apply(); - return id; - } - + final var appSettings = service.getAppSettings(); + final long installationId = appSettings.getInstallationId(); + return AccountUtils.createUuid4(installationId, installationId).toString(); } private int getApiErrorCode(final Exception e) { @@ -463,15 +453,15 @@ public class QuickConversationsService extends AbstractQuickConversationsService for (final PhoneNumberContact c : contacts.values()) { entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber())); } - final IqPacket query = new IqPacket(IqPacket.TYPE.GET); + final Iq query = new Iq(Iq.Type.GET); query.setTo(syncServer); final Element book = new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries); final String statusQuo = Entry.statusQuo(contacts.values(), account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)); book.setAttribute("ver", statusQuo); query.addChild(book); mLastSyncAttempt = Attempt.create(hash); - service.sendIqPacket(account, query, (a, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { + service.sendIqPacket(account, query, (response) -> { + if (response.getType() == Iq.Type.RESULT) { final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION); if (phoneBook != null) { final List withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class); @@ -498,7 +488,7 @@ public class QuickConversationsService extends AbstractQuickConversationsService } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": phone number contact list remains unchanged"); } - } else if (response.getType() == IqPacket.TYPE.TIMEOUT) { + } else if (response.getType() == Iq.Type.TIMEOUT) { mLastSyncAttempt = Attempt.NULL; } else { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to sync contact list with api server"); diff --git a/src/quicksy/res/values-ar/strings.xml b/src/quicksy/res/values-ar/strings.xml index ddf1d878886f507bc4a6ff2f689edd64c7f7122b..cf44a653b2b27dfbd971ce8a51de059800c5ffdb 100644 --- a/src/quicksy/res/values-ar/strings.xml +++ b/src/quicksy/res/values-ar/strings.xml @@ -1,10 +1,10 @@ - مدى الوقت الذي يظل فيه Quicksy هادئًا بعد رؤية نشاط على جهاز آخر - عبر إرسال الأخطاء انت تقوم بالمساعدة في تطوير برمجة Quicksy + طول الفترة الزمنية التي يظل فيها Quicksy هادئًا بعد رؤية النشاط على جهاز آخر + بإرسال تتبعات المكدس، فإنك تساعد في التطوير المستمر لـ Quicksy إجعل كلّ جهات إتصالك تعلم أنك تستعمل كويكسي للمواصلة في إستقبال التنبيهات، حتى والشاشة مغلقة، يجب عليك أن تضيف Quicksy إلى قائمة التطبيقات المحميّة. - صورة حساب Quicksy + صورة الملف الشخصي بسرعة Quicksy إن كويكسي Quicksy غير متوفر في بلدكم. لا يمكن التأكد من خادم الهويّة. خطأ أمني مجهول. diff --git a/src/quicksy/res/values-et/strings.xml b/src/quicksy/res/values-et/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/quicksy/res/values-et/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/quicksy/res/values-gl/strings.xml b/src/quicksy/res/values-gl/strings.xml index 0f1343d66ebce6390612b5a6aa5dfe003f8ae508..57bf791696730f36bb4b01d291538227164a96e1 100644 --- a/src/quicksy/res/values-gl/strings.xml +++ b/src/quicksy/res/values-gl/strings.xml @@ -4,7 +4,7 @@ Enviando trazas do rexistro estás axudando ao desenvolvemento de Quicksy Permitir a todos os teus contactos saber cando estás a utilizar Quicksy Para seguir recibindo notificacións, mesmo coa pantalla apagada, tes que engadir a Quicksy á lista de apps protexidas. - Imaxe de perfil Quicksy + Imaxe de perfil en Quicksy Quicksy non está dispoñible no teu país. Non se puido verificar a identidade do servidor. Fallo de seguridade descoñecido. diff --git a/src/quicksy/res/values-nl/strings.xml b/src/quicksy/res/values-nl/strings.xml index eec11a1f90b41c109cf89000a6b69c44811afe9e..80b7a48e46e27cb0dc2b62c411b6e87a7cc891f1 100644 --- a/src/quicksy/res/values-nl/strings.xml +++ b/src/quicksy/res/values-nl/strings.xml @@ -4,9 +4,9 @@ Door crashrapportages te versturen help je de ontwikkeling van Quicksy Laat al je contactpersonen weten wanneer je Quicksy gebruikt Om meldingen te blijven ontvangen, zelfs wanneer het scherm uit staat, moet je Quicksy toevoegen aan de lijst met beschermde apps. - Quicksy-profielafbeelding + Quicksy profielafbeelding Quicksy is niet beschikbaar in je land. Kan serveridentiteit niet verifiëren. Onbekende beveiligingsfout. Time-out bij verbinden met server. - + \ No newline at end of file diff --git a/src/quicksy/res/values-ro-rRO/strings.xml b/src/quicksy/res/values-ro-rRO/strings.xml index 7dd79ce4ab0fcf271c9a6200d9813b3804298c1c..a2f4fbe04153e9591b7ec98f04da1c23c5b6e00d 100644 --- a/src/quicksy/res/values-ro-rRO/strings.xml +++ b/src/quicksy/res/values-ro-rRO/strings.xml @@ -4,9 +4,9 @@ Trimițând datele despre erori ajutați la continuarea dezvoltării aplicației Quicksy Contactele vă sunt anunțate atunci când folosiți Quicksy Pentru a continua să primiți notificări, chiar și când ecranul este oprit, trebuie să adăugați Quicksy în lista de aplicații protejate. - Poză profil Quicksy + Poză de profil Quicksy Quicksy nu este disponibilă în țara dumneavoastră. Nu s-a putut verifica identitatea serverului. Eroare de securitate necunoscută. A expirat timpul de așteptare conexiune server. - + \ No newline at end of file diff --git a/src/quicksy/res/values-zh-rCN/strings.xml b/src/quicksy/res/values-zh-rCN/strings.xml index 839ab36ed180f5092e8398315a064f7537accd44..cf4f46cf6cec6627b21eaf4fc2e632441b40ca48 100644 --- a/src/quicksy/res/values-zh-rCN/strings.xml +++ b/src/quicksy/res/values-zh-rCN/strings.xml @@ -1,9 +1,9 @@ - 发现另一台设备上的活动后,Quicksy 在此期间保持安静 - 通过发送堆栈跟踪,您正在帮助 Quicksy 的持续开发 + 发现另一台设备上的活动后,Quicksy 在此期间保持静音 + 通过发送崩溃报告,您可以帮助 Quicksy 的持续开发 让您的所有联系人知道您最后使用 Quicksy 的时间 - 为了在屏幕关闭时也能收到消息提醒,您需要将 Quicksy 加入受保护的应用列表。 + 为了在屏幕关闭后继续接收通知,您需要将 Quicksy 加入受保护的应用列表。 Quicksy 个人资料图片 Quicksy 在您所在的国家/地区无法使用。 无法验证服务器身份。