diff --git a/.woodpecker.yml b/.woodpecker.yml index affaf71106c7209b45473f9a549d38958b1fa5ac..e4596179941d78fcf60a827a91e73e13906b88e9 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -1,7 +1,11 @@ steps: - build: - image: codeberg.org/freeyourgadget/android-fdroid-tools:latest - commands: - - ./gradlew clean - - ./gradlew assembleConversationsFreeDebug - - ./gradlew assembleQuicksyFreeDebug + build: + when: + - branch: master + event: push + - event: tag + image: codeberg.org/freeyourgadget/android-fdroid-tools:latest + commands: + - ./gradlew clean + - ./gradlew assembleConversationsFreeDebug + - ./gradlew assembleQuicksyFreeDebug diff --git a/CHANGELOG.md b/CHANGELOG.md index b5d72e162f7f1b7103e1eedf4cdfb0dd140484f8..9516c25bd2476b23b1f137e4a72d75e20ac5ea61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # Changelog +### Version 2.17.8 + +* Fix some minor UI bugs +* Fix connection issues with .onion domains on non-default ports + +### Version 2.17.7 + +* Easier access to custom notification sounds via Contact details -> Overflow menu -> Custom notifications) +* Fix direct share targets on new Android versions +* Ability to restrict avatar visibility to contacts + +### Version 2.17.6 + +* Add ability to show message bubbles left-aligned + +### Version 2.17.5 + +* Move message bubbles closer together instead of merging them +* Add ability to hide avatars in chat view when not strictly necessary (Settings -> Interface -> Chat Bubbles -> Show avatars) + +### Version 2.17.4 + +* improve handling of some emoji reactions + +### Version 2.17.3 + +* Always show call button +* Various bug fixes + +### Version 2.17.2 + +* Fix calls on Android 15 +* Fix rare crash / regression introduced with 2.17.0 + +### Version 2.17.1 + +* Fix UI glitch when showing multiple reactions + +### Version 2.17.0 + +* Support Message Reactions + ### Version 2.16.7 * Add timeout to call initiation diff --git a/build.gradle b/build.gradle index d5d834097d5c677737fe45cab4bdec2fc8700ef7..33ca8aa62621cac1797223e9105e1eb71d6cf812 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:8.5.2' + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0" } } @@ -16,6 +17,7 @@ plugins { } apply plugin: 'com.android.application' +apply plugin: "com.diffplug.spotless" repositories { google() @@ -39,6 +41,15 @@ configurations { quicksyImplementation } +spotless { + ratchetFrom '2.17.4' + java { + target '**/*.java' + googleJavaFormat().aosp().reflowLongStrings() + } +} + + dependencies { androidTestImplementation 'tools.fastlane:screengrab:2.1.1' androidTestImplementation 'junit:junit:4.13.2' @@ -48,14 +59,14 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation "androidx.core:core:1.10.1" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3' implementation project(':libs:annotation') annotationProcessor project(':libs:annotation-processor') - implementation 'androidx.viewpager:viewpager:1.0.0' + implementation 'androidx.viewpager:viewpager:1.1.0' - playstoreImplementation('com.google.firebase:firebase-messaging:24.0.1') { + playstoreImplementation('com.google.firebase:firebase-messaging:24.1.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' @@ -66,12 +77,14 @@ dependencies { 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.sharetarget:sharetarget:1.2.0" + 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.13.0-alpha06' + implementation 'com.google.android.material:material:1.13.0-alpha09' implementation 'androidx.work:work-runtime:2.9.1' implementation "androidx.emoji2:emoji2:1.5.0" @@ -87,10 +100,7 @@ dependencies { implementation "com.wefika:flowlayout:0.4.1" //noinspection GradleDependency - implementation('com.github.natario1:Transcoder:v0.9.1') { - exclude group: 'com.otaliastudios.opengl', module: 'egloo' - } - implementation 'com.github.natario1:Egloo:v0.4.0' + implementation 'io.deepmedia.community:transcoder-android:0.11.2' implementation 'org.jxmpp:jxmpp-jid:1.0.3' implementation 'org.jxmpp:jxmpp-stringprep-libidn:1.0.3' diff --git a/fastlane/metadata/android/de-DE/changelogs/4212104.txt b/fastlane/metadata/android/de-DE/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..68997eff0870bb1e7c78284b61b9b146792db7cf --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212104.txt @@ -0,0 +1 @@ +* Unterstützung von Reaktionen auf Nachrichten diff --git a/fastlane/metadata/android/de-DE/changelogs/4212204.txt b/fastlane/metadata/android/de-DE/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..f7827c6d60921b98cd0fe8141ed3e05fc7c8b36d --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212204.txt @@ -0,0 +1 @@ +* Darstellungsfehler beim Anzeigen mehrerer Reaktionen behoben diff --git a/fastlane/metadata/android/de-DE/changelogs/4212304.txt b/fastlane/metadata/android/de-DE/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc865be6b499d414854b49f8e8dfaf8c8704bc7c --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Anrufe auf Android 15 behoben +* Seltener Absturz behoben / Regression die mit 2.17.0 aufgetreten ist diff --git a/fastlane/metadata/android/de-DE/changelogs/4212404.txt b/fastlane/metadata/android/de-DE/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..0bae96c951f3fc9c2e87b715da23e24ef1deef43 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Anruf-Taste immer anzeigen +* Verschiedene Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/4212504.txt b/fastlane/metadata/android/de-DE/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..c5f074b61dd752ea51ae4bdd344844b3dd4bd7c2 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212504.txt @@ -0,0 +1 @@ +* Handhabung einiger Emoji-Reaktionen verbessert diff --git a/fastlane/metadata/android/de-DE/changelogs/4212604.txt b/fastlane/metadata/android/de-DE/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..d36b0cd264100a4da4ff95c0c182ff7602aba624 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Nachrichten näher zusammengerückt, anstatt sie zu verschmelzen +* Möglichkeit hinzugefügt, Profilbilder in Chat-Ansichten auszublenden, wenn sie nicht zwingend erforderlich sind (Einstellungen > Benutzeroberfläche > Sprechblasen > Profilbilder anzeigen) diff --git a/fastlane/metadata/android/de-DE/changelogs/4212704.txt b/fastlane/metadata/android/de-DE/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..2fdd237623319693005051fd1bc1eeb44ae72b71 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212704.txt @@ -0,0 +1 @@ +* Möglichkeit hinzugefügt, Nachrichten linksbündig anzuzeigen diff --git a/fastlane/metadata/android/de-DE/changelogs/4212804.txt b/fastlane/metadata/android/de-DE/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..2a94e67805ab5e0f86182035bc63a4755db9ee25 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Leichterer Zugang zu benutzerdefinierten Benachrichtigungstönen über Kontaktdetails > weiteres Menü > Benutzerdefinierte Benachrichtigungen +* Direkte Freigabeziele auf neuen Android-Versionen korrigiert +* Möglichkeit, die Sichtbarkeit von Profilbildern auf Kontakte zu beschränken diff --git a/fastlane/metadata/android/de-DE/changelogs/4212904.txt b/fastlane/metadata/android/de-DE/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..c46d7a1aa6729de631c64a07dff0f129cf9d2f90 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Kleine UI-Bugs behoben +* Verbindungsprobleme mit .onion-Domains auf nicht standardmäßigen Ports behoben diff --git a/fastlane/metadata/android/en-US/changelogs/4212104.txt b/fastlane/metadata/android/en-US/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..f4c551c06919a689142583fbf1cd60fa7c83d5ef --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212104.txt @@ -0,0 +1 @@ +* Support Message Reactions diff --git a/fastlane/metadata/android/en-US/changelogs/4212204.txt b/fastlane/metadata/android/en-US/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..976971e5a56d1ef863e19cd4789def4af4634dcb --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212204.txt @@ -0,0 +1 @@ +* Fix UI glitch when showing multiple reactions diff --git a/fastlane/metadata/android/en-US/changelogs/4212304.txt b/fastlane/metadata/android/en-US/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..800d1fb03c4fdb8f7256b2e534f1021f9d45b7cd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Fix calls on Android 15 +* Fix rare crash / regression introduced with 2.17.0 diff --git a/fastlane/metadata/android/en-US/changelogs/4212404.txt b/fastlane/metadata/android/en-US/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..2addf4df6e5be0a40ec5d8f21eae2d3c25f610e7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Always show call button +* Various bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/4212504.txt b/fastlane/metadata/android/en-US/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..4655c359363d685602711e92848eb8d87fbe0770 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212504.txt @@ -0,0 +1 @@ +* improve handling of some emoji reactions diff --git a/fastlane/metadata/android/en-US/changelogs/4212604.txt b/fastlane/metadata/android/en-US/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..84d0effac257f8e9b438a6cd4bb5f82a96e25fd2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Move message bubbles closer together instead of merging them +* Add ability to hide avatars in chat views when not stricly necessary (Settings -> Interface -> Chat Bubbles -> Show avatars) diff --git a/fastlane/metadata/android/en-US/changelogs/4212704.txt b/fastlane/metadata/android/en-US/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..1c21c350b62e1b0d25f2fedd1ca66ee6a92a82aa --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212704.txt @@ -0,0 +1 @@ +* Add ability to show message bubbles left-aligned diff --git a/fastlane/metadata/android/en-US/changelogs/4212804.txt b/fastlane/metadata/android/en-US/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..9055128c02d915a6025c5ac17bfb89f43332e74a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Easier access to custom notification sounds via Contact details -> Overflow menu -> Custom notifications) +* Fix direct share targets on new Android versions +* Ability to restrict avatar visibility to contacts diff --git a/fastlane/metadata/android/en-US/changelogs/4212904.txt b/fastlane/metadata/android/en-US/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..c700e2eff1327888e238dfd8b9e8176b786cce62 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Fix some minor UI bugs +* Fix connection issues with .onion domains on non-default ports diff --git a/fastlane/metadata/android/es-ES/changelogs/4211704.txt b/fastlane/metadata/android/es-ES/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..0d18da37311897c3d56251d1f8d2654cfa70af10 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* Ofrecer valores más grandes para la recepción automática de archivo +* Proporcionar más información en 'Información del servidor' +* Varias correcciones de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/4211804.txt b/fastlane/metadata/android/es-ES/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..821f50e6b2e9a18b2253e31a95c1dd0fa1dd9625 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4211804.txt @@ -0,0 +1 @@ +* Agregar tiempo de espera al inicio de la llamada diff --git a/fastlane/metadata/android/es-ES/changelogs/4212104.txt b/fastlane/metadata/android/es-ES/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc349204d5a1b7ae61250dcac86fa961eb26c2ec --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212104.txt @@ -0,0 +1 @@ +* Reacciones a los mensajes de soporte diff --git a/fastlane/metadata/android/es-ES/changelogs/4212204.txt b/fastlane/metadata/android/es-ES/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..1565c4ca903e87c22e217f3977b96f8b4e5f0fd7 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212204.txt @@ -0,0 +1 @@ +* Se solucionó un error de la interfaz de usuario al mostrar múltiples reacciones diff --git a/fastlane/metadata/android/es-ES/changelogs/4212304.txt b/fastlane/metadata/android/es-ES/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..d7ab6b5f8c2b49494f451700991576bfd72d27a0 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Arreglar llamadas en Android 15 +* Corrección de un error raro/ regresión introducida con 2.17.0 diff --git a/fastlane/metadata/android/es-ES/changelogs/4212404.txt b/fastlane/metadata/android/es-ES/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..1b555cccb22f84fe289a674e200c8529a05262f6 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Mostrar siempre el botón de llamada +* Correcciones de errores varios diff --git a/fastlane/metadata/android/es-ES/changelogs/4212504.txt b/fastlane/metadata/android/es-ES/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..310442197e313e061937ec589647a5009b5f39d7 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212504.txt @@ -0,0 +1 @@ +* mejorar el manejo de algunas reacciones con emojis diff --git a/fastlane/metadata/android/es-ES/changelogs/4212604.txt b/fastlane/metadata/android/es-ES/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..fe1ca6a77a686167f15867451b41e1c4a4f78bdd --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Acercar burbujas de chat en vez de combinarlas en una sola +* Posibilidad de ocultar imágenes de perfil cuando no es estrictamente necesario (Ajustes -> Interfaz -> Burbujas de chat -> Ver imágenes de perfil) diff --git a/fastlane/metadata/android/es-ES/changelogs/4212704.txt b/fastlane/metadata/android/es-ES/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..f72fc661ba183a400c5c62a46feaff59e0933ea9 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212704.txt @@ -0,0 +1 @@ +* Posibilidad de alinear burbujas de mensaje sobre el margen izquierdo diff --git a/fastlane/metadata/android/es-ES/changelogs/4212804.txt b/fastlane/metadata/android/es-ES/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..0733de85f4aca9a4526c0a13067055a15cf918c0 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Acceso más fácil a sonidos de notificación personalizados desde Detalles del contacto -> Menú emergente -> Notificaciones personalizadas +* Corrección de compartición directa en nuevas versiones de Android +* Posibilidad de limitar visibilidad de imagen de perfil sólo a contactos diff --git a/fastlane/metadata/android/es-ES/changelogs/4212904.txt b/fastlane/metadata/android/es-ES/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..9ceaa20a4b11c60e8157b967d8a2c082d7be5cd1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Se corrigieron algunos errores menores de la interfaz de usuario. +* Solucionar problemas de conexión con dominios .onion (.cebolla) en puertos no predeterminados diff --git a/fastlane/metadata/android/et/changelogs/42061.txt b/fastlane/metadata/android/et/changelogs/42061.txt new file mode 100644 index 0000000000000000000000000000000000000000..e4bc14e9c428d0541debac6ff6953b5183cf157d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42061.txt @@ -0,0 +1 @@ +* Eemaldasime Google Play versioonist kanalite tuvastuse diff --git a/fastlane/metadata/android/et/changelogs/42062.txt b/fastlane/metadata/android/et/changelogs/42062.txt new file mode 100644 index 0000000000000000000000000000000000000000..92312203453d8f62bf87609adf9abf8e039d66d7 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42062.txt @@ -0,0 +1 @@ +* Lülitasime välja võimaluse avada failihaldurist varukoopia faile (.ceb) diff --git a/fastlane/metadata/android/et/changelogs/42065.txt b/fastlane/metadata/android/et/changelogs/42065.txt new file mode 100644 index 0000000000000000000000000000000000000000..13f81531bf3783f5dc15f4dc89fe8bf087033ba3 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42065.txt @@ -0,0 +1 @@ +* Võtsime varukoopiate jaoks kasutusele uue failivormingu diff --git a/fastlane/metadata/android/et/changelogs/42068.txt b/fastlane/metadata/android/et/changelogs/42068.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae26bff5dcabdec5eeb736f52a670551e56287be --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42068.txt @@ -0,0 +1,2 @@ +* vestlusekohaste teavituste tugi +* Android 10s kasutame opus-koodekit häälsõnumite jaoks diff --git a/fastlane/metadata/android/et/changelogs/42072.txt b/fastlane/metadata/android/et/changelogs/42072.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd7f230e94a8324fd34e0923198391135131695c --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42072.txt @@ -0,0 +1,3 @@ +* libwebrtc teek sõltub nüüd versioonist M117 ja uuendasime libvpx teeki +* Häälsõnumid kasutavad jälle AAC koodekit +* Rakendustekohaste keeleeelistuste tugi diff --git a/fastlane/metadata/android/et/changelogs/4207704.txt b/fastlane/metadata/android/et/changelogs/4207704.txt new file mode 100644 index 0000000000000000000000000000000000000000..6e367918adf52c07501e8143c9a6f9a47efe8582 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Privaatsete nimeserverite tugi (DNS üle TLSi) +* Võimalus valida käivitusikooni kujundust +* Parandasime harvaesineva failide jagamise vea, mis esines Android 11+ puhul diff --git a/fastlane/metadata/android/et/changelogs/4208104.txt b/fastlane/metadata/android/et/changelogs/4208104.txt new file mode 100644 index 0000000000000000000000000000000000000000..eaf7f3f98712e3b889b5fbcb76be90bdaf02187a --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Lihtsam ligipääs valikule „Näita QR-koodi“ +* PEP-järjehoidjate tugi +* Lisasime SDP pakkumine/vastus mudeli toe (seda kasutavad SIPi lüüsid) +* Üldine API arvestab nüüd Android 14'ga diff --git a/fastlane/metadata/android/et/changelogs/4208804.txt b/fastlane/metadata/android/et/changelogs/4208804.txt new file mode 100644 index 0000000000000000000000000000000000000000..c8e553d6bcf6fd5081e35335eaea34b5c43f2fec --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4208804.txt @@ -0,0 +1,3 @@ +* P2P failide teisaldamise tugi kasutades WebRTC andmekanaleid +* Parandasime Bind 2.0 ühilduvusvead, kui kasutusel on ejabberd +* Lisasime rakendusele Let’s Encrypti juursertifikaadid, kui kasutusel on Android <= 7 diff --git a/fastlane/metadata/android/et/changelogs/4209004.txt b/fastlane/metadata/android/et/changelogs/4209004.txt new file mode 100644 index 0000000000000000000000000000000000000000..fe15f61a903cf0595c3892adb3166b45e700c0bf --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* väikesed veaparandused +* Quicksy liitumisloogika väikesed kohendused diff --git a/fastlane/metadata/android/et/changelogs/4209204.txt b/fastlane/metadata/android/et/changelogs/4209204.txt new file mode 100644 index 0000000000000000000000000000000000000000..cdb8e7cab41b7c30310ba9edd0a6b3558e89acaf --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Play Store'i versioonis on nüüd lihtsam ligipääs privaatsuspoliitikale (Quicksy ja Conversations) +* Conversationsi Play Store'i versioonist oleme eemaldanud lõimingu aadressiraamatuga diff --git a/fastlane/metadata/android/et/changelogs/4209404.txt b/fastlane/metadata/android/et/changelogs/4209404.txt new file mode 100644 index 0000000000000000000000000000000000000000..135b260268ccb4ddcf2e8d4839c75eb8324fab4d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4209404.txt @@ -0,0 +1 @@ +* Parandsime väikese regressiooni, mis tekkis versioonis 2.13.1 diff --git a/fastlane/metadata/android/et/changelogs/4210104.txt b/fastlane/metadata/android/et/changelogs/4210104.txt new file mode 100644 index 0000000000000000000000000000000000000000..1ce095bef7cd36ff29703c186e2c2e31c17c71c9 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4210104.txt @@ -0,0 +1 @@ +* Parandasime hääl- ja videokõnede lõimimise operatsioonisüsteemiga diff --git a/fastlane/metadata/android/et/changelogs/4210404.txt b/fastlane/metadata/android/et/changelogs/4210404.txt new file mode 100644 index 0000000000000000000000000000000000000000..5bafb88784cc79f7ae59737fb3094c2d70555937 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4210404.txt @@ -0,0 +1,3 @@ +* Parandasime hääl- ja videokõned Android 8's +* Parandasime vea protsesside trügimisega lõimitud kõnede puhul +* Parandasime vea kui videopakkimine jäi muutmatuks diff --git a/fastlane/metadata/android/et/changelogs/4210504.txt b/fastlane/metadata/android/et/changelogs/4210504.txt new file mode 100644 index 0000000000000000000000000000000000000000..cf5f9a364a3775a664c5ae0d9e72ab12dd315966 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4210504.txt @@ -0,0 +1,2 @@ +* Taastasime Android 6+7 puhul ligipääsu kanalite tuvastamisele +* Parandasime logimist, kui lõimitud kõned ei toimi diff --git a/fastlane/metadata/android/et/changelogs/4210704.txt b/fastlane/metadata/android/et/changelogs/4210704.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f26994c0269c1329bd7df8a7c54d471a0c097e7 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4210704.txt @@ -0,0 +1,3 @@ +* Kasutame Material 3 kujundust +* Korraldasime seadistused ümber +* Sünkroniseerime lugemise olekuid eri seadmete vahel diff --git a/fastlane/metadata/android/et/changelogs/4210804.txt b/fastlane/metadata/android/et/changelogs/4210804.txt new file mode 100644 index 0000000000000000000000000000000000000000..08fe87e75a673ed465c9d45ececc09c8108a8885 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4210804.txt @@ -0,0 +1,2 @@ +* Näitame sõnumite olekuid ikoonidena +* Lisasime sõnumimullide välimusele suurema kirjatüübi valimise võimaluse diff --git a/fastlane/metadata/android/et/changelogs/4210904.txt b/fastlane/metadata/android/et/changelogs/4210904.txt new file mode 100644 index 0000000000000000000000000000000000000000..cf7fc5a5235a9c52d610c07909ff80f901bb72a2 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4210904.txt @@ -0,0 +1,2 @@ +* Parandasime Quicksy registreerimise Androidi versioonides 6/7 +* Saabuva kõne märguanne teavituskanalis diff --git a/fastlane/metadata/android/et/changelogs/4211004.txt b/fastlane/metadata/android/et/changelogs/4211004.txt new file mode 100644 index 0000000000000000000000000000000000000000..ffbd895b3847674047dce96992afded22bc6f8e2 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211004.txt @@ -0,0 +1,2 @@ +* Parandasime kõnede lõimimise vead mõnedes Android 14-põhistes nutiseadmetes +* Lisasime seadistuse „Kutsed võõrastelt“ diff --git a/fastlane/metadata/android/et/changelogs/4211104.txt b/fastlane/metadata/android/et/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..be8b646b221233887e9d950d9e420a931bb1b696 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Nüüd saab teha regulaarseid varukoopiaid +* Välistame kõnede lõimimise kõikides Realme nutiseadmetes kuni Androidi versioonini 11 +* Väikesed kasutajaliidese parandused (kõnemullid) diff --git a/fastlane/metadata/android/et/changelogs/4211204.txt b/fastlane/metadata/android/et/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..5cb5fecd1b2627d31fb52fb52d2f805798562c95 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Parandasime vea, kus kõne summutamine lõppeb väljundseadmete vahetamisel +* Välistame kõnede lõimimise kõikides Umidigi nutiseadmetes diff --git a/fastlane/metadata/android/et/changelogs/4211304.txt b/fastlane/metadata/android/et/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..63d91ad689b3366bbc3bedf0c80d32e85fc33e37 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211304.txt @@ -0,0 +1 @@ +* vältimaks varunduse peatumist peale kümmet minutit, käivitame ta alati esiplaanil diff --git a/fastlane/metadata/android/et/changelogs/4211404.txt b/fastlane/metadata/android/et/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..c0fc59046a793cafc5dc9c5d4e70d8ba0dbfb6a5 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* välistasime vanemad Oppo telefonid kõnelõimingutest +* mitmed veaparandused diff --git a/fastlane/metadata/android/et/changelogs/4211604.txt b/fastlane/metadata/android/et/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..a0571dea82198717c9e2096ead3c152460df6c50 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211604.txt @@ -0,0 +1 @@ +* väikesed veaparandused diff --git a/fastlane/metadata/android/et/changelogs/4211704.txt b/fastlane/metadata/android/et/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..d9dadda4d8e53f570484e684ad7adddf5eac60c4 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* muutsime vaikimisi allalaaditavate failide mahu suuremaks +* näitame rohkem teavet valikus „Serveri info“ +* parandasime mitmeid vigu diff --git a/fastlane/metadata/android/et/changelogs/4211804.txt b/fastlane/metadata/android/et/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..a4c9c989a4b147674f6ba5bd2982234407498be6 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4211804.txt @@ -0,0 +1 @@ +* Lisasime kõne alustamisele aegumise diff --git a/fastlane/metadata/android/et/changelogs/4212104.txt b/fastlane/metadata/android/et/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..e9d3f444a7fd144d81bf4d5cb0d94edb4639924b --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212104.txt @@ -0,0 +1 @@ +* Sõnumitest reageerimise tugi diff --git a/fastlane/metadata/android/et/changelogs/4212204.txt b/fastlane/metadata/android/et/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..c48235eca4a75e88f4949e254675edcc139740c3 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212204.txt @@ -0,0 +1 @@ +* Parandasime kasutajaliidese vea, kui kuvamisel oli mitu reageerimist diff --git a/fastlane/metadata/android/et/changelogs/4212304.txt b/fastlane/metadata/android/et/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..31a3d46c219cf620c562fd0412cfef9d8e1a33ea --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Parandasime kõnete vead Android 15's +* Parandasime harvaesineva kokkujooksmise/regressiooni, mis tekkis alates versioonist 2.17.0 diff --git a/fastlane/metadata/android/et/changelogs/4212404.txt b/fastlane/metadata/android/et/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..a5bee3dc73d194b0665cdda34a171063621a1493 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* näitame alati kõne alustamise ikooni +* erinevad veaparandused diff --git a/fastlane/metadata/android/et/changelogs/4212504.txt b/fastlane/metadata/android/et/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..891ec1b4cf898c2c5e0b862b02c44882a5926f17 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212504.txt @@ -0,0 +1 @@ +* parandasime mõnede emoji-põhiste reaktsioonide töötlemist diff --git a/fastlane/metadata/android/et/changelogs/4212604.txt b/fastlane/metadata/android/et/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..7f2f67a79d22c56f886bd2fb6764890bb6a5d3bd --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Tõstsime jutumullid üksteisele lähemale vältides nende meldimist +* Lisasime võimaluse peita vestlustes tunnuspilte, kui seda tõesti vaja pole (Seadistused -> Kasutajaliides -> Jutumullid -> Näita tunnuspilte) diff --git a/fastlane/metadata/android/et/changelogs/4212704.txt b/fastlane/metadata/android/et/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..422e57094d4db000a57c3db6494af65aaf3634eb --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212704.txt @@ -0,0 +1 @@ +* Lisasime võimaluse kuvada kõiki jutumulle joondatuna vasakule diff --git a/fastlane/metadata/android/et/changelogs/4212804.txt b/fastlane/metadata/android/et/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..6296d9a617a142be5eb757832070e239903b2d4a --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Lihtsam lihipääs sinu seadistatud teavitushelinatele Kontakti üksikasjad -> Jätkumenüü -> Kohandatud teavitused) +* Parandasime otsejagamise linkide kasutamise uutes Androidi versioonides +* Võimalus piirata tunnuspildi näitamist kontaktidele diff --git a/fastlane/metadata/android/et/changelogs/4212904.txt b/fastlane/metadata/android/et/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..97e1fe7fabe3fb0a3511f9f2aa880bc622e31971 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Parandasime mõned väiksemad kasutajaliidese vead +* Parandasime .onion domeenide ühenduse vead mittestandardsete portide kasutamisel diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212104.txt b/fastlane/metadata/android/gl-ES/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b93142c71d699af7f832784a12bc78d45b2f7da --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212104.txt @@ -0,0 +1 @@ +* Compatibilidade con Reaccións ás Mensaxes diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212204.txt b/fastlane/metadata/android/gl-ES/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..117a4a757c7db337719c9a24d664c1ba630d60af --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212204.txt @@ -0,0 +1 @@ +* Arranxo do problema na interface cando hai varias reaccións diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212304.txt b/fastlane/metadata/android/gl-ES/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..9b1064d6981adb5beaf32a9959520857f3e2008c --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Arranxo das chamadas en Android 15 +* Arranxo do problema raro / regresión introducido en 2.17.0 diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212404.txt b/fastlane/metadata/android/gl-ES/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..59454bb1ce0ecb57cae038b40c130a0d81bd6daa --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Mostra sempre o botón de chamada +* Arranxo de varios problemas diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212504.txt b/fastlane/metadata/android/gl-ES/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..a6a5c9bd3ad667e179093e3ea04c8c9157c796a8 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212504.txt @@ -0,0 +1 @@ +* mellora na xestión da reacción con emojis diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212604.txt b/fastlane/metadata/android/gl-ES/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..8375c7fb1277537fae1b300e3d241fb60133a01a --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Xuntar máis as burbullas das mensaxes no lugar de xuntalas +* Poder agochar os avatares na vista de conversas cando non é estritamente necesario (Axustes -> Interface -> Burbullas -> Mostrar avatares) diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212704.txt b/fastlane/metadata/android/gl-ES/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..b3969bf8cab5cfa7061575d20501ea7818bad80c --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212704.txt @@ -0,0 +1 @@ +* Podes enfeitar no lado esquerdo todas as mensaxes diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212804.txt b/fastlane/metadata/android/gl-ES/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..ac87c05627e523fac72a859a16e70b9a67c5b2f7 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Acceso máis doado á personalización do son da notificación en Detalles do contacto -> Menú emerxente -> Notificacións personalizadas) +* Arranxo das opcións directas para compartir nas novas versións de Android +* Posibilidade de limitar a visibilidade do avatar só para contactos diff --git a/fastlane/metadata/android/gl-ES/changelogs/4212904.txt b/fastlane/metadata/android/gl-ES/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b89483ca3e58be58f70cbfb8a0fedea1c7375c6 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* arranxo de problemas menores na interface +* arranxo de problemas coa conexión a dominios .onion en portos non predeterminados diff --git a/fastlane/metadata/android/it-IT/changelogs/4211804.txt b/fastlane/metadata/android/it-IT/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..468fa7c69db54cf0356a200d15a0f25ef3a8cb62 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4211804.txt @@ -0,0 +1 @@ +* Aggiunta scadenza per iniziazione della chiamata diff --git a/fastlane/metadata/android/it-IT/changelogs/4212104.txt b/fastlane/metadata/android/it-IT/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..67535dc1a2d09e774cea78be94a9ebd8016804b2 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212104.txt @@ -0,0 +1 @@ +* Supporto per le reazioni ai messaggi diff --git a/fastlane/metadata/android/it-IT/changelogs/4212204.txt b/fastlane/metadata/android/it-IT/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..925213342b5be43b630c7634151b444ed9823590 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212204.txt @@ -0,0 +1 @@ +* Corretto errore di interfaccia che mostrava reazioni multiple diff --git a/fastlane/metadata/android/it-IT/changelogs/4212304.txt b/fastlane/metadata/android/it-IT/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..6dba0777353985516a5b04f7f8397b769fd359ef --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Correzione delle chiamate su Android 15 +* Corretto un raro crash / regressione introdotto nella 2.17.0 diff --git a/fastlane/metadata/android/pl-PL/changelogs/42060.txt b/fastlane/metadata/android/pl-PL/changelogs/42060.txt new file mode 100644 index 0000000000000000000000000000000000000000..f0b657c3581bf65d5971035eadbc81b86f77c5ca --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42060.txt @@ -0,0 +1 @@ +* Naprawienie litery ‚q’ nieprawidłowo rozpoznawanej jako cyrylica diff --git a/fastlane/metadata/android/pl-PL/changelogs/42061.txt b/fastlane/metadata/android/pl-PL/changelogs/42061.txt new file mode 100644 index 0000000000000000000000000000000000000000..1c163ae9f8a47950fc0bfd96d9a29715e16859bd --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42061.txt @@ -0,0 +1 @@ +* Usunięcie funkcjonalności odkrywania kanałów z wersji dla Google Play diff --git a/fastlane/metadata/android/pl-PL/changelogs/42062.txt b/fastlane/metadata/android/pl-PL/changelogs/42062.txt new file mode 100644 index 0000000000000000000000000000000000000000..228de1949bf0465ab3faf157c39ce4c9e7817a0c --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42062.txt @@ -0,0 +1 @@ +* Wyłączenie otwierania plików kopii zapasowej (.ceb) z przeglądarki plików diff --git a/fastlane/metadata/android/pl-PL/changelogs/42065.txt b/fastlane/metadata/android/pl-PL/changelogs/42065.txt new file mode 100644 index 0000000000000000000000000000000000000000..63ea3eabce6da9e9dbe0bce334cf90f6aaf6009d --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42065.txt @@ -0,0 +1 @@ +* Wprowadzenie nowego formatu pliku kopii zapasowej diff --git a/fastlane/metadata/android/pl-PL/changelogs/42068.txt b/fastlane/metadata/android/pl-PL/changelogs/42068.txt new file mode 100644 index 0000000000000000000000000000000000000000..fa41c6c7a28958bbc81999aae2e3f2a65ca1d5c2 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42068.txt @@ -0,0 +1,2 @@ +* Obsługa ustawień powiadomień dla danej rozmowy +* Używanie Opus dla wiadomości głosowych w Androidzie 10 diff --git a/fastlane/metadata/android/pl-PL/changelogs/42072.txt b/fastlane/metadata/android/pl-PL/changelogs/42072.txt new file mode 100644 index 0000000000000000000000000000000000000000..a1262e9d2309949a90de639b568224caa3eb40cb --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42072.txt @@ -0,0 +1,3 @@ +* Podwyższenie wymaganej wersji libwebrtc do M117 i podwyższenie wymaganej wersji libvpx +* Powrót do AAC dla wiadomości głosowych +* Obsługa ustawień języka dla aplikacji diff --git a/fastlane/metadata/android/pl-PL/changelogs/4207704.txt b/fastlane/metadata/android/pl-PL/changelogs/4207704.txt new file mode 100644 index 0000000000000000000000000000000000000000..b8aca91ba389474a870d2d85b40faf0b3a6abea9 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4207704.txt @@ -0,0 +1,3 @@ +* Obsługa prywatnego DNS (DNS over TLS) +* Obsługa motywów ikony uruchamiania aplikacji +* Naprawa rzadkiego problemu z uprawnieniami podczas udostępniania plików na Androidzie 11 i nowszych diff --git a/fastlane/metadata/android/pl-PL/changelogs/4208104.txt b/fastlane/metadata/android/pl-PL/changelogs/4208104.txt new file mode 100644 index 0000000000000000000000000000000000000000..05a85c99891be48432ae2c424ebb936a0a42c60e --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4208104.txt @@ -0,0 +1,4 @@ +* Łatwiejszy dostęp do „Pokaż kod QR” +* Obsługa natywnych zakładek PEP +* Obsługa modelu SDP Offer/Answer (używanego przez bramki SIP) +* Podwyższenie docelowego API do Androida 14 diff --git a/fastlane/metadata/android/pl-PL/changelogs/4208804.txt b/fastlane/metadata/android/pl-PL/changelogs/4208804.txt new file mode 100644 index 0000000000000000000000000000000000000000..653b7cbb26e88d0e55b18064d27803923aed5841 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4208804.txt @@ -0,0 +1,3 @@ +* Obsługa przesyłanie plików P2P przez kanały danych WebRTC +* Naprawiono problemy interoperacyjności z Bind 2.0 na ejabberd +* Wbudowanie certyfikatów głównych Let’s Encrypt dla Androida 7 i starszych diff --git a/fastlane/metadata/android/pl-PL/changelogs/4209004.txt b/fastlane/metadata/android/pl-PL/changelogs/4209004.txt new file mode 100644 index 0000000000000000000000000000000000000000..656ecf22295f5fa0af3b8d169d5f1091d82fec80 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4209004.txt @@ -0,0 +1,2 @@ +* Drobne poprawki błędów +* Małe zmiany we wprowadzeniu do Quicksy diff --git a/fastlane/metadata/android/pl-PL/changelogs/4209204.txt b/fastlane/metadata/android/pl-PL/changelogs/4209204.txt new file mode 100644 index 0000000000000000000000000000000000000000..5fa281e23c93599ab2e0b7b506cd998abd9ca788 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4209204.txt @@ -0,0 +1,2 @@ +* Łatwiejszy dostęp do polityki prywatności w wersji dla Sklepu Play (Quicksy i Conversations) +* Usunięcie integracji z książką adresową w wersji Conversations dla Sklepu Play diff --git a/fastlane/metadata/android/pl-PL/changelogs/4209404.txt b/fastlane/metadata/android/pl-PL/changelogs/4209404.txt new file mode 100644 index 0000000000000000000000000000000000000000..3cd789377b9a6ff0b9da09191cedc48356cee4a6 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4209404.txt @@ -0,0 +1 @@ +* Naprawiono małą regresję wprowadzoną w 2.13.1 diff --git a/fastlane/metadata/android/pl-PL/changelogs/4210104.txt b/fastlane/metadata/android/pl-PL/changelogs/4210104.txt new file mode 100644 index 0000000000000000000000000000000000000000..49e01c70d00637cb4751c19edf0a84bcdf79d547 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4210104.txt @@ -0,0 +1 @@ +* Ulepszono integrację rozmów głosowych i wideo z systemem operacyjnym diff --git a/fastlane/metadata/android/pl-PL/changelogs/4210404.txt b/fastlane/metadata/android/pl-PL/changelogs/4210404.txt new file mode 100644 index 0000000000000000000000000000000000000000..b70775e0c18d7f41b3bd2b5dc896a6f69dc797ce --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4210404.txt @@ -0,0 +1,3 @@ +* Naprawiono rozmowy głosowe i wideo w Androidzie 8 +* Naprawiono problemy typu race condition w nowej integracji rozmów +* Naprawiono nieznikającą kompresję wideo diff --git a/fastlane/metadata/android/pl-PL/changelogs/4210504.txt b/fastlane/metadata/android/pl-PL/changelogs/4210504.txt new file mode 100644 index 0000000000000000000000000000000000000000..b480b09bb04d3e7e984821a1f90d213cfabd5a76 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4210504.txt @@ -0,0 +1,2 @@ +* Przywrócenie dostępu do odkrywania kanałów w Androidzie 6 i 7 +* Ulepszenie logowania nieudanej integracji rozmów diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212104.txt b/fastlane/metadata/android/pl-PL/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..438f01fef891e04a23f150ef12129094731bc817 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212104.txt @@ -0,0 +1 @@ +* Obsługa reakcji na wiadomości diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212204.txt b/fastlane/metadata/android/pl-PL/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..54d774b3d6860e8fbc8833ed0706f0c195bccf1f --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212204.txt @@ -0,0 +1 @@ +* Naprawienie usterki interfejsu użytkownika podczas pokazywania wielu reakcji diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212304.txt b/fastlane/metadata/android/pl-PL/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..27a1f7d03aa9e6dd28a3a51eb1619d420847fdea --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Naprawienie rozmów w Androidzie 15 +* Naprawienie rzadkiej awarii (regresji) wprowadzonej w 2.17.0 diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212404.txt b/fastlane/metadata/android/pl-PL/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..403a9c18c8e62a2b5880227ccd6c3d759fcad277 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Pokazywanie przycisku rozmowy zawsze +* Różne poprawki błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212504.txt b/fastlane/metadata/android/pl-PL/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..dc07e11f044d9397b071bb304877acfdc4f422e0 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212504.txt @@ -0,0 +1 @@ +* Poprawienie obsługi niektórych emoji reakcji diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212604.txt b/fastlane/metadata/android/pl-PL/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..defd53fd1caf3188b4344b8198cca8d7dfbe2d63 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Przeniesienie dymków wiadomości bliżej siebie zamiast ich łączenia +* Dodanie możliwości ukrywania awatarów w widokach rozmów gdy nie są konieczne (Ustawienia → Interfejs → Dymki rozmów → Pokaż awatary) diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212704.txt b/fastlane/metadata/android/pl-PL/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..276739170ec81e5224235d761e63b97cfff4b03e --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212704.txt @@ -0,0 +1 @@ +* Dodanie możliwości pokazywania dymków wiadomości wyrównanych do lewej diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212804.txt b/fastlane/metadata/android/pl-PL/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..a5360a0533d0c4bd140e8863f6d28cf7d166ac10 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Łatwiejszy dostęp do własnych dźwięków powiadomień poprzez Szczegóły kontaktu → Więcej opcji → Własne powiadomienia +* Naprawienie bezpośrednich celów udostępniania w nowszych wersjach Androida +* Możliwość ograniczenia widoczności awatara do kontaktów diff --git a/fastlane/metadata/android/pl-PL/changelogs/4212904.txt b/fastlane/metadata/android/pl-PL/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..4fb3906ada86b03a4e7e9aec87d954519bb4f0cd --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Poprawienie drobnych błędów interfejsu użytkownika +* Poprawienie problemów z połączeniem z domenami .onion na niedomyślnych portach diff --git a/fastlane/metadata/android/ro/changelogs/394.txt b/fastlane/metadata/android/ro/changelogs/394.txt new file mode 100644 index 0000000000000000000000000000000000000000..a42ade02aa14c37174b89a000fd1be3b9677e488 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/394.txt @@ -0,0 +1,2 @@ +* Reparat notificările care nu apăreau în anumite condiții +* Reparat probleme de compatibilitate și crăpări legate de apeluri A/V diff --git a/fastlane/metadata/android/ro/changelogs/395.txt b/fastlane/metadata/android/ro/changelogs/395.txt new file mode 100644 index 0000000000000000000000000000000000000000..1c382e0ce94fe200a386878e771741b472f040f9 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/395.txt @@ -0,0 +1,3 @@ +* adăugat 'Întoarcere la discuție' pe ecranul de apel audio +* îmbunătățit scurtăturile de tastatură +* corecturi de erori diff --git a/fastlane/metadata/android/ro/changelogs/397.txt b/fastlane/metadata/android/ro/changelogs/397.txt new file mode 100644 index 0000000000000000000000000000000000000000..c6e8e3eafd1aa344000c8f82be9de58ccce757e8 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/397.txt @@ -0,0 +1,3 @@ +* Gestionare fișiere GPX +* Îmbunătățit performanța pentru restaurarea copiilor de siguranță +* Corecturi de erori diff --git a/fastlane/metadata/android/ro/changelogs/398.txt b/fastlane/metadata/android/ro/changelogs/398.txt new file mode 100644 index 0000000000000000000000000000000000000000..52fe55e96bc05bce0c3c1e6ba9463c59b15d45cd --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/398.txt @@ -0,0 +1,4 @@ +* Căutare în conversații individuale +* Notifică utilizatorul dacă livrarea mesajului eșuează +* Ține minte numele afișate (poreclele) de la utilizatorii Quicksy între reporniri +* Adăugat buton pentru a porni Orbot (Tor) din notificare dacă este necesar diff --git a/fastlane/metadata/android/ro/changelogs/401.txt b/fastlane/metadata/android/ro/changelogs/401.txt new file mode 100644 index 0000000000000000000000000000000000000000..c82c0d79975a9242cc6b456628e1e48fbecdeff2 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/401.txt @@ -0,0 +1,2 @@ +* corectat căutarea pe Android <= 5 +* optimizat consumul de memorie diff --git a/fastlane/metadata/android/ro/changelogs/402.txt b/fastlane/metadata/android/ro/changelogs/402.txt new file mode 100644 index 0000000000000000000000000000000000000000..7eec1b94417af4c1786f4d633f54146ba2c540c7 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/402.txt @@ -0,0 +1,3 @@ +* Oferă generarea de invitație facilă pe serverele care suportă +* Afișează GIFuri trimise din Movim +* stochează avatarii în cache diff --git a/fastlane/metadata/android/ro/changelogs/403.txt b/fastlane/metadata/android/ro/changelogs/403.txt new file mode 100644 index 0000000000000000000000000000000000000000..4945950151ebcd3becc6c9edea2b86c8d50cf245 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/403.txt @@ -0,0 +1,3 @@ +* Corectat probleme de conectivitate când conturi diferite foloseau mecanisme SCRAM diferite +* Adăugat suport pentru SCRAM-SHA-512 +* Permis transfer de fișiere P2P (Jingle) cu contactul sine diff --git a/fastlane/metadata/android/ro/changelogs/404.txt b/fastlane/metadata/android/ro/changelogs/404.txt new file mode 100644 index 0000000000000000000000000000000000000000..ca77ff37f1451c91d24b7434769d544dbf381c68 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/404.txt @@ -0,0 +1 @@ +* mici îmbunătățiri de stabilitate pentru apeluri A/V diff --git a/fastlane/metadata/android/ro/changelogs/405.txt b/fastlane/metadata/android/ro/changelogs/405.txt new file mode 100644 index 0000000000000000000000000000000000000000..fd7ce6de75a21538744276b1d225f0b25aa73826 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: primește automat verificare SMS diff --git a/fastlane/metadata/android/ro/changelogs/407.txt b/fastlane/metadata/android/ro/changelogs/407.txt new file mode 100644 index 0000000000000000000000000000000000000000..86149b4de84e21aa711610bca80e0fe004b1cfd5 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/407.txt @@ -0,0 +1,3 @@ +* Arată butonul de apel pentru contacte deconectate dacă au anuțat anterior suportul +* Butonul de revenire nu mai termină apelul când apelul este conectat +* corecturi de erori diff --git a/fastlane/metadata/android/ro/changelogs/42000.txt b/fastlane/metadata/android/ro/changelogs/42000.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8447d3cdc1ed77735010897b61ea3a0b32f31b1 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Posibilitatea de a selecta tonuri de apel +* Corectat descoperirea ID-ului cheii OpenPGP pentru OpenKeychain 5.6+ +* Verificarea corectă a certificatelor TLS punycode +* Îmbunătățit stabilitatea creerii sesiunii RTP (apelare) diff --git a/fastlane/metadata/android/ro/changelogs/42006.txt b/fastlane/metadata/android/ro/changelogs/42006.txt new file mode 100644 index 0000000000000000000000000000000000000000..9f7dabde46fc7b8b796fa85c920e486a5afdbeba --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Verifică apelurile A/V cu sesiuni OMEMO preexistente +* Îmbunătățit compatibilitatea cu implementări WebRTC non libwebrtc diff --git a/fastlane/metadata/android/ro/changelogs/42010.txt b/fastlane/metadata/android/ro/changelogs/42010.txt new file mode 100644 index 0000000000000000000000000000000000000000..46cd92ca690324d49ff13b7339498d2a3a5116ae --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Corectat felurite erori relative la suportul pentru Tor +* Îmbunătățit compatibilitatea apelurilor cu Dino diff --git a/fastlane/metadata/android/ro/changelogs/42012.txt b/fastlane/metadata/android/ro/changelogs/42012.txt new file mode 100644 index 0000000000000000000000000000000000000000..965afdc5bb80ed7ee0c362185373ba48e459e251 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/42012.txt @@ -0,0 +1 @@ +* Corectat up/download HTTP pentru utilizatorii care nu au încredere în CA din sistem diff --git a/fastlane/metadata/android/ro/changelogs/42013.txt b/fastlane/metadata/android/ro/changelogs/42013.txt new file mode 100644 index 0000000000000000000000000000000000000000..8924f3f943dd71b0292c127930a967a6a5b86889 --- /dev/null +++ b/fastlane/metadata/android/ro/changelogs/42013.txt @@ -0,0 +1 @@ +* Rezolvat problemele 'lipsă conectivitate' pe Android 7.1 diff --git a/fastlane/metadata/android/ru-RU/changelogs/4210104.txt b/fastlane/metadata/android/ru-RU/changelogs/4210104.txt index 3b2e3069bb4a03d99dc86f1087e2b43c15c8db76..c42e447f9266484143334abc99cce612a5853995 100644 --- a/fastlane/metadata/android/ru-RU/changelogs/4210104.txt +++ b/fastlane/metadata/android/ru-RU/changelogs/4210104.txt @@ -1 +1 @@ -* Улучшена интеграция вызовов A/V с операционной системой +* Улучшена интеграция аудио- и видеовызовов с операционной системой diff --git a/fastlane/metadata/android/ru-RU/changelogs/4210404.txt b/fastlane/metadata/android/ru-RU/changelogs/4210404.txt new file mode 100644 index 0000000000000000000000000000000000000000..280144fff51228da301e25b51905093c83eef5fa --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4210404.txt @@ -0,0 +1,3 @@ +* Исправлены аудио- и видеовызовы на Android 8 +* Исправлены приоритеты в новой интеграции вызовов +* Исправлена проблема со сжатием видео diff --git a/fastlane/metadata/android/ru-RU/changelogs/4210504.txt b/fastlane/metadata/android/ru-RU/changelogs/4210504.txt new file mode 100644 index 0000000000000000000000000000000000000000..73a70549ff33f7977578c5141638cf4215021e0a --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4210504.txt @@ -0,0 +1,2 @@ +* Восстановлен доступ к поиску каналов для Android 6+7 +* Улучшено ведение журнала для неудачной интеграции вызовов diff --git a/fastlane/metadata/android/ru-RU/changelogs/4210704.txt b/fastlane/metadata/android/ru-RU/changelogs/4210704.txt new file mode 100644 index 0000000000000000000000000000000000000000..497d807095325d11367c261d7e1fcc559c1aa334 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4210704.txt @@ -0,0 +1,3 @@ +* Используется тема Material 3 +* Реорганизация настроек +* Синхронизация состояния прочтения на разных устройствах diff --git a/fastlane/metadata/android/ru-RU/changelogs/4210804.txt b/fastlane/metadata/android/ru-RU/changelogs/4210804.txt new file mode 100644 index 0000000000000000000000000000000000000000..6a6207a57354e8cd5950269d041fe9e6bb44eb7d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4210804.txt @@ -0,0 +1,2 @@ +* Отображение статуса сообщения значком +* Добавлен параметр "Крупный шрифт" для пузырей сообщений diff --git a/fastlane/metadata/android/ru-RU/changelogs/4210904.txt b/fastlane/metadata/android/ru-RU/changelogs/4210904.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f74049e2a39518999816f1e1985cc5760a10c28 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4210904.txt @@ -0,0 +1,2 @@ +* исправлена регистрация Quicksy на Android 6-7 +* проигрывание рингтона вызова через канал уведомлений diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211004.txt b/fastlane/metadata/android/ru-RU/changelogs/4211004.txt new file mode 100644 index 0000000000000000000000000000000000000000..bb6411e72eedb048493846c19526c53366395ce9 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211004.txt @@ -0,0 +1,2 @@ +* исправлена интеграция звонков для устройств с Android 14 +* добавлена настройка «Приглашения от незнакомцев» diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211104.txt b/fastlane/metadata/android/ru-RU/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..5f65fadee00d6ca767f374ba8e5c6afa2cab8b75 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* Планировщик регулярного резервного копирования +* Все устройства Realme до Android 11 исключены из интеграции вызовов +* Незначительные улучшения интерфейса (пузыри сообщений) diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211204.txt b/fastlane/metadata/android/ru-RU/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..1054a8426c766af28e792f855d1c12ce1ab27e7c --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* Исправлена ошибка, из-за которой звук вызова включался при переключении устройств вывода +* Все устройства Umidigi исключены из интеграции вызовов diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211304.txt b/fastlane/metadata/android/ru-RU/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..9a70a488583400e959531d5c5e7930451d058a84 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211304.txt @@ -0,0 +1 @@ +* Резервное копирование запускается как приоритетная служба, чтобы предотвратить остановку процесса через 10 минут diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211404.txt b/fastlane/metadata/android/ru-RU/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..62bffa9de4a9feb3294fd43a1898db2fac225f45 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* старые устройства Oppo исключены из интеграции звонков +* различные исправления diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211604.txt b/fastlane/metadata/android/ru-RU/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..13b38ab36cdec6a2d4161711d61877a4806108ff --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211604.txt @@ -0,0 +1 @@ +* Незначительные исправления diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211704.txt b/fastlane/metadata/android/ru-RU/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..d072da7efb7d9c4904761d44692585aab60ce331 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* Предлагаются более высокие значения автоматического приёма файлов +* Отображается дополнительная информация в разделе "Информация о сервере" +* Различные исправления ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/4211804.txt b/fastlane/metadata/android/ru-RU/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..44ec8d8ad339823e90cffb01e77aee431a5e57ae --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4211804.txt @@ -0,0 +1 @@ +* Добавлено время истечения для попытки начала вызова diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212104.txt b/fastlane/metadata/android/ru-RU/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..c46070924cd3530c657a8b756c23ece76dd80e44 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212104.txt @@ -0,0 +1 @@ +* Поддержка реакций сообщений diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212204.txt b/fastlane/metadata/android/ru-RU/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..14c9d8b0106da63f183b512a1a131e275204d72d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212204.txt @@ -0,0 +1 @@ +* Исправлен сбой интерфейса при отображении нескольких реакций diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212304.txt b/fastlane/metadata/android/ru-RU/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..a2394f670fb70f3b60a31c149a5d2f69dd489678 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Исправлены звонки на Android 15 +* Исправлен редкий сбой, появившийся в версии 2.17.0 diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212404.txt b/fastlane/metadata/android/ru-RU/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..56b5f9f6bb6b4015e59e71d384f10653d253b333 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Постоянное отображение кнопки звонка +* Различные исправления ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212504.txt b/fastlane/metadata/android/ru-RU/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..a5d2269b6ead8f35c91e4c64c87511010b9f4112 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212504.txt @@ -0,0 +1 @@ +* Улучшена обработка некоторых реакций на эмодзи diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212604.txt b/fastlane/metadata/android/ru-RU/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..c58561c659a578bcd671eab21a810d9b7813ebc4 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Пузыри сообщений сдвигаются ближе друг к другу без объединения +* Добавлена возможность скрывать аватары в беседах, если в них нет особой необходимости (Настройки → Интерфейс → Пузыри сообщений → Показывать аватары) diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212704.txt b/fastlane/metadata/android/ru-RU/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..39910ddef4aa262d11bc7b3d69341fea828b39df --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212704.txt @@ -0,0 +1 @@ +* Добавлена возможность показывать пузыри сообщений с выравниванием по левому краю diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212804.txt b/fastlane/metadata/android/ru-RU/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..bc9bb30574136a862f75f6bcf0314a9d7165ad8d --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Упрощён доступ к настройке звука уведомлений (через "Сведения о контакте" → Дополнительное меню → "Настраиваемые уведомления") +* Исправлены получатели прямого обмена в новых версиях Android +* Добавлена возможность ограничить отображение аватара для контактов diff --git a/fastlane/metadata/android/ru-RU/changelogs/4212904.txt b/fastlane/metadata/android/ru-RU/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..58cf6a44ba27d3a97f3907c153bec0c747415538 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Исправлены незначительные ошибки интерфейса +* Исправлены проблемы с подключением к доменам .onion на нестандартных портах diff --git a/fastlane/metadata/android/sq/changelogs/4212104.txt b/fastlane/metadata/android/sq/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b421f81930129ad1ccf9ca5880dac53c46181b9 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212104.txt @@ -0,0 +1 @@ +* Mbulim për Reagime Ndaj Mesazhesh diff --git a/fastlane/metadata/android/sq/changelogs/4212204.txt b/fastlane/metadata/android/sq/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..f1c36671ffd4f62bfbd2a489ce73a87ca292d686 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212204.txt @@ -0,0 +1 @@ +* Ndreqje e një problemi të vockël në UI, kur shfaqen reagime të shumtë diff --git a/fastlane/metadata/android/sq/changelogs/4212304.txt b/fastlane/metadata/android/sq/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..1281f9ed9dda4b7a01ba3ac681321c486e5b4819 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Ndreqje thirrjesh në Android 15 +* Ndreqje vithisjeje / prapakthimi të rrallë, sjellë me 2.17.0 diff --git a/fastlane/metadata/android/sq/changelogs/4212404.txt b/fastlane/metadata/android/sq/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..34203d3db3f83b82fcb201cc4bf4531b41199010 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Shfaq përherë buton thirrjesh +* Ndreqje të ndryshme të metash diff --git a/fastlane/metadata/android/sq/changelogs/4212504.txt b/fastlane/metadata/android/sq/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..ef2b5ee2414dca1046538f12d2b5284074c1b7fc --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212504.txt @@ -0,0 +1 @@ +* përmirësim i trajtimit të disa reagimeve me emoxhi diff --git a/fastlane/metadata/android/sq/changelogs/4212604.txt b/fastlane/metadata/android/sq/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..4c80d1818c4d0aaff19760ea176f6fda645a5f6b --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Kalim i flluskave të mesazheve më afër njëra-tjetrës, në vend se përzierje e tyre +* Shtim aftësie për fshehje avatarësh në pamje fjalosjesh, kur s’është domosdo e nevojshme (Rregullime -> Ndërfaqe -> Flluska Fjalosjesh -> Shfaq avatarë) diff --git a/fastlane/metadata/android/sq/changelogs/4212704.txt b/fastlane/metadata/android/sq/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..7b61ac22db17afbaaf33a613ed91a99e17ece928 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212704.txt @@ -0,0 +1 @@ +* Shtim aftësie për t’i shfaqur flluskat e mesazheve në të majtë diff --git a/fastlane/metadata/android/sq/changelogs/4212804.txt b/fastlane/metadata/android/sq/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..240b9af4f1db0a2b7ed1f542e5c4c2e6a6dbad28 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Përdorim më i kollajtë i tingujsh vetjakë njoftimesh, përmes hollësish Kontakti -> Menu shtresë përsipër -> Njoftime vetjake) +* Ndreqje objektivash ndarjeje të drejtpërdrejtë në versione të rinj Android +* Aftësi për t’u kufizuar kontakteve dukshmëri avatarësh diff --git a/fastlane/metadata/android/sq/changelogs/4212904.txt b/fastlane/metadata/android/sq/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..16144cd645fa489406c81cceb8dc466d97148132 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Ndreqje e disa të metave të vockla në UI +* Ndreqje problemesh lidhjeje me përkatësi .onion në porta jo-parazgjedhje diff --git a/fastlane/metadata/android/sv-SE/changelogs/390.txt b/fastlane/metadata/android/sv-SE/changelogs/390.txt new file mode 100644 index 0000000000000000000000000000000000000000..8f0882eaa7e8b0c1f12e6e12bd35bde7d5429fe5 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/390.txt @@ -0,0 +1 @@ +* Erbjud att spela in ljudmeddelande när mottagaren av samtalet är upptagen diff --git a/fastlane/metadata/android/sv-SE/changelogs/397.txt b/fastlane/metadata/android/sv-SE/changelogs/397.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc9da9de91e30aa3d2b86fa4f29612c8d6d945dd --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/397.txt @@ -0,0 +1,3 @@ +* Hantera GPX-filer +* Förbättra prestanda för återställning av säkerhetskopia +* bugg-fixar diff --git a/fastlane/metadata/android/sv-SE/changelogs/404.txt b/fastlane/metadata/android/sv-SE/changelogs/404.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd3de07b5aec01b9baca0dc21287ff779b457324 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/404.txt @@ -0,0 +1 @@ +* mindre stabilitetsförbättringar för A/V-samtal diff --git a/fastlane/metadata/android/sv-SE/changelogs/42010.txt b/fastlane/metadata/android/sv-SE/changelogs/42010.txt new file mode 100644 index 0000000000000000000000000000000000000000..1a7fa4133e0dbf64ef982b0f6713cfafd252c4a9 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Diverse bugg-fixar runt stöd för Tor +* Förbättra samtalskompabilitet med Dino diff --git a/fastlane/metadata/android/sv-SE/changelogs/42023.txt b/fastlane/metadata/android/sv-SE/changelogs/42023.txt new file mode 100644 index 0000000000000000000000000000000000000000..49e8451cfcc2ab9417c50451b60c38d6375f4454 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Fixade krasch vid rendering av några citat +* Fixade krasch i välkomstskärmen diff --git a/fastlane/metadata/android/sv-SE/changelogs/42043.txt b/fastlane/metadata/android/sv-SE/changelogs/42043.txt new file mode 100644 index 0000000000000000000000000000000000000000..147c7262e0c57ba838f9c408127d3b4f61bdabd4 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42043.txt @@ -0,0 +1 @@ +* Fixade återgång med filöverföring via P2P diff --git a/fastlane/metadata/android/sv-SE/changelogs/42061.txt b/fastlane/metadata/android/sv-SE/changelogs/42061.txt new file mode 100644 index 0000000000000000000000000000000000000000..9fbbf81e77b2fa17f87473aa2e91831f2ed01b26 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42061.txt @@ -0,0 +1 @@ +* Ta bort funktionen för att upptäcka kanaler från Google Play-versionen diff --git a/fastlane/metadata/android/sv-SE/changelogs/42062.txt b/fastlane/metadata/android/sv-SE/changelogs/42062.txt new file mode 100644 index 0000000000000000000000000000000000000000..7745cedaf4d065436309b50dff69b75bf7bca35c --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42062.txt @@ -0,0 +1 @@ +* Inaktivera öppnandet av säkerhetskopieringsfiler (.ceb) från filhanteraren diff --git a/fastlane/metadata/android/sv-SE/changelogs/42068.txt b/fastlane/metadata/android/sv-SE/changelogs/42068.txt new file mode 100644 index 0000000000000000000000000000000000000000..003deb4458450c042fddd42cac3c9110a3c6d9b3 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42068.txt @@ -0,0 +1,2 @@ +* stöd för inställningar för meddelanden per konversation +* använd opus för ljudmeddelanden i Android 10 diff --git a/fastlane/metadata/android/sv-SE/changelogs/4209404.txt b/fastlane/metadata/android/sv-SE/changelogs/4209404.txt new file mode 100644 index 0000000000000000000000000000000000000000..a1a5dbbbb2731754f81b63ef873ecd32f4740bec --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4209404.txt @@ -0,0 +1 @@ +* Fixade mindre återgångar som introducerades med 2.13.1 diff --git a/fastlane/metadata/android/sv-SE/changelogs/4210104.txt b/fastlane/metadata/android/sv-SE/changelogs/4210104.txt new file mode 100644 index 0000000000000000000000000000000000000000..70cb22a2ee63de867ea30352dd1b9602296ea868 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4210104.txt @@ -0,0 +1 @@ +* Förbättra integrationen av A/V-samtal i operativsystemet diff --git a/fastlane/metadata/android/sv-SE/changelogs/4210704.txt b/fastlane/metadata/android/sv-SE/changelogs/4210704.txt new file mode 100644 index 0000000000000000000000000000000000000000..5c540aadbda65059adfc2aa98cddb13783ea8522 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4210704.txt @@ -0,0 +1,3 @@ +* Använd temat Material 3 +* Omorganisera inställningar +* Synkronisera läs-status mellan enheter diff --git a/fastlane/metadata/android/sv-SE/changelogs/4210804.txt b/fastlane/metadata/android/sv-SE/changelogs/4210804.txt new file mode 100644 index 0000000000000000000000000000000000000000..70eb6e2b2676521327f9798e04a64d0b38b63a83 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4210804.txt @@ -0,0 +1,2 @@ +* Visa meddelandestatus som ikoner +* Introducera inställningen 'Stort typsnitt' för meddelandebubblor diff --git a/fastlane/metadata/android/sv-SE/changelogs/4211304.txt b/fastlane/metadata/android/sv-SE/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..b6af679a13ee18beb1d67e5b90094762a7b3ef01 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4211304.txt @@ -0,0 +1 @@ +* Kör säkerhetskopiering som en förgrundstjänst för att förhindra processer stoppas efter 10 minuter diff --git a/fastlane/metadata/android/sv-SE/changelogs/4211404.txt b/fastlane/metadata/android/sv-SE/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..37e23821a097fc515cbee769b5b324b7e1f1d2a2 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* exkludera äldre Oppo-enheter från samtalsintegrering +* diverse bugg-fixar diff --git a/fastlane/metadata/android/sv-SE/changelogs/4211804.txt b/fastlane/metadata/android/sv-SE/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..3d130ecbc35a16a0c2c2426cebb34f7b74dd2b67 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4211804.txt @@ -0,0 +1 @@ +* Lägg till en tidsgräns för att påbörja samtalet diff --git a/fastlane/metadata/android/sv-SE/changelogs/4212204.txt b/fastlane/metadata/android/sv-SE/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..39b1bfb0fc305e4504fa9d5349f2b0b39515447d --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4212204.txt @@ -0,0 +1 @@ +* Fixade en störning i det grafiska gränssnittet när flertalet reaktioner visas diff --git a/fastlane/metadata/android/sv-SE/changelogs/4212404.txt b/fastlane/metadata/android/sv-SE/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..925c7c03345a131968a17c970435619fd66da02e --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Visa alltid samtalsknapp +* Diverse bugg-fixar diff --git a/fastlane/metadata/android/sv-SE/changelogs/4212504.txt b/fastlane/metadata/android/sv-SE/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..403a4f07f8a37e747e7d2392d08e1bcbb513b5af --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4212504.txt @@ -0,0 +1 @@ +* förbättra hantering av en del emoji-reaktioner diff --git a/fastlane/metadata/android/sv-SE/changelogs/4212704.txt b/fastlane/metadata/android/sv-SE/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..1269ecd58716e8f2c98ac1e6129f0812d6100a2b --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/4212704.txt @@ -0,0 +1 @@ +* Lägg till förmåga att visa vänster-justerade meddelande-bubblor diff --git a/fastlane/metadata/android/uk/changelogs/4212104.txt b/fastlane/metadata/android/uk/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..b842175e7c34dd845a10affeec7a19f4d141b421 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212104.txt @@ -0,0 +1 @@ +* Підтримка реакцій на повідомлення diff --git a/fastlane/metadata/android/uk/changelogs/4212204.txt b/fastlane/metadata/android/uk/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..edaa8adc22e7236865a91e00bb5e90824cbaeef3 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212204.txt @@ -0,0 +1 @@ +* Виправлено помилку інтерфейсу під час показу декількох реакцій diff --git a/fastlane/metadata/android/uk/changelogs/4212304.txt b/fastlane/metadata/android/uk/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..28b128d2aeebf8fb8781f719b4c96975b13b7a04 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* Виправлено виклики на Android 15 +* Виправлено рідкісний збій / регресію, що з'явилися у 2.17.0 diff --git a/fastlane/metadata/android/uk/changelogs/4212404.txt b/fastlane/metadata/android/uk/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..4639a5a8836b543fbadea130eb2a363ba70c6fc0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Завжди показувати кнопку виклику +* Виправлення різноманітних помилок diff --git a/fastlane/metadata/android/uk/changelogs/4212504.txt b/fastlane/metadata/android/uk/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..344c67d8be0cf335fc5ecd9240399e92fda0a213 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212504.txt @@ -0,0 +1 @@ +* Покращено обробку деяких реакцій емоджі diff --git a/fastlane/metadata/android/uk/changelogs/4212604.txt b/fastlane/metadata/android/uk/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..19c44c1cf6d9febf2006a57930ffd7a66a306511 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Бульбашки повідомлень розташовуються ближче одна до одної, а не об'єднуються +* Додано можливість приховати непотрібні аватари у розмовах (Налаштування -> Інтерфейс -> Вигляд повідомлень -> Показувати аватари) diff --git a/fastlane/metadata/android/uk/changelogs/4212704.txt b/fastlane/metadata/android/uk/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..decf2874bcd5d459aecc35e70df1852c17d1b5b1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212704.txt @@ -0,0 +1 @@ +* Додано можливість вирівнювати бульбашки повідомлень ліворуч diff --git a/fastlane/metadata/android/uk/changelogs/4212804.txt b/fastlane/metadata/android/uk/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..a838d64ede07b670786ef41a6f4bf3f5661d4a86 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Полегшено доступ до користувацьких звуків сповіщень (Деталі контакту -> Додатково -> Індивідуальні сповіщення) +* Виправлення цілей прямого поширення на нових версіях Android +* Можливість обмежити видимість аватара для контактів diff --git a/fastlane/metadata/android/uk/changelogs/4212904.txt b/fastlane/metadata/android/uk/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..d4a7541096891aa11f77ce1d5cf5753cc440fe46 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Виправлено деякі незначні помилки в інтерфейсі +* Виправлено проблеми з'єднання з доменами .onion на портах не за замовчуванням diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212104.txt b/fastlane/metadata/android/zh-CN/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b298885a0360713de8b2be5a46e008c9b89e661 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212104.txt @@ -0,0 +1 @@ +* 支持消息回应 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212204.txt b/fastlane/metadata/android/zh-CN/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..eb7805db93f5495f1a2e3ffc6afec8f55b32be9b --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212204.txt @@ -0,0 +1 @@ +* 修复显示多个回应时的 UI 故障 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212304.txt b/fastlane/metadata/android/zh-CN/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..65f60dfebf581beaec1d868428e13688adc82118 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* 修复 Android 15 上的通话 +* 修复 2.17.0 引入的罕见崩溃问题 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212404.txt b/fastlane/metadata/android/zh-CN/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..7e7fa1b3b79cdc53b9ca037f6970eb13cac73181 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* 始终显示呼叫按钮 +* 各种错误修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212504.txt b/fastlane/metadata/android/zh-CN/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..84de6ffbc5de6cf5d849ef07e0b1b60199bf000c --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212504.txt @@ -0,0 +1 @@ +* 改进某些表情符号回应的处理 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212604.txt b/fastlane/metadata/android/zh-CN/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..609cdbe23a7f8e2338819119beb8985b2fddeef3 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* 将消息气泡靠得更近而不是合并它们 +* 添加在聊天视图中隐藏头像的功能(设置 -> 界面 -> 消息气泡 -> 显示头像) diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212704.txt b/fastlane/metadata/android/zh-CN/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..160aa46d04425c642a02c4607e7da5938ec90d64 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212704.txt @@ -0,0 +1 @@ +* 添加左对齐显示消息气泡的功能 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212804.txt b/fastlane/metadata/android/zh-CN/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..217baa5879723ebe76fe0acd61f41219403ca8bb --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* 通过(联系详情 -> 溢出菜单 -> 自定义通知)更轻松地访问自定义通知提示音 +* 修复新 Android 版本上的直接共享目标 +* 能够限制头像对联系人的可见性 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4212904.txt b/fastlane/metadata/android/zh-CN/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..7b3c86886f697687c5be67e56f6f023fb9ed5893 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* 修复一些小的 UI 错误 +* 修复非默认端口上 .onion 域的连接问题 diff --git a/fastlane/metadata/android/zh-TW/changelogs/387.txt b/fastlane/metadata/android/zh-TW/changelogs/387.txt index 38f22df14f2caeff87781e02d682189d777e66d5..473d718435dcf9c87476e7fe3e916bdd59e7ff16 100644 --- a/fastlane/metadata/android/zh-TW/changelogs/387.txt +++ b/fastlane/metadata/android/zh-TW/changelogs/387.txt @@ -1,2 +1,2 @@ * 重做憑證登錄的使用者介面 -* 新增置頂聊天的功能(加入最愛) +* 新增釘選聊天的功能(加入最愛) diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211104.txt b/fastlane/metadata/android/zh-TW/changelogs/4211104.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8754953eae3e18773850ecf4615954dafed3a88 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211104.txt @@ -0,0 +1,3 @@ +* 安排定期備份 +* 從通話集成中排除所有 Android 11 及以下的 realme 裝置 +* Minor UI(消息氣泡)改進 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211204.txt b/fastlane/metadata/android/zh-TW/changelogs/4211204.txt new file mode 100644 index 0000000000000000000000000000000000000000..73019691b48d84adbd1b937add3b8130c8d6dcb7 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211204.txt @@ -0,0 +1,2 @@ +* 修復了切換輸出設備時通話被取消靜音的問題 +* 從呼叫集成中排除所有 Umidigi 裝置 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211304.txt b/fastlane/metadata/android/zh-TW/changelogs/4211304.txt new file mode 100644 index 0000000000000000000000000000000000000000..44240545bcf8d0975fd6c8d6cbe18ce69731e4c3 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211304.txt @@ -0,0 +1 @@ +* 將備份作為前台服務運行,以防止進程在 10 分鐘後停止 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211404.txt b/fastlane/metadata/android/zh-TW/changelogs/4211404.txt new file mode 100644 index 0000000000000000000000000000000000000000..87429860066bfb2dd12b83b51698e04da24f3423 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211404.txt @@ -0,0 +1,2 @@ +* 從通話集成中排除較舊的 Oppo裝置 +* 各種錯誤修復 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211604.txt b/fastlane/metadata/android/zh-TW/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..c70766964558a001bb760845c1b878908ea0a349 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211604.txt @@ -0,0 +1 @@ +* 小錯誤修復 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211704.txt b/fastlane/metadata/android/zh-TW/changelogs/4211704.txt new file mode 100644 index 0000000000000000000000000000000000000000..e3b54eb1d3838740dd8d3b6d72083478891205af --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211704.txt @@ -0,0 +1,3 @@ +* 提供更高的自動檔案接受值 +* 在「伺服器資訊」中提供更多資訊 +* 各種錯誤修復 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4211804.txt b/fastlane/metadata/android/zh-TW/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..632aefa7d3e3f2a652ccaa58deb3610b75e6bde8 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4211804.txt @@ -0,0 +1 @@ +* 增加調用發起超時 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4212104.txt b/fastlane/metadata/android/zh-TW/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..af35e09facef37f1c25b6e433f93b8a126a49b9e --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4212104.txt @@ -0,0 +1 @@ +* 支援消息回應 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4212204.txt b/fastlane/metadata/android/zh-TW/changelogs/4212204.txt new file mode 100644 index 0000000000000000000000000000000000000000..a446703858e474c13ded14924f12d3b9d2e1e01f --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4212204.txt @@ -0,0 +1 @@ +* 修復顯示多個反應時的 UI 故障 diff --git a/fastlane/metadata/android/zh-TW/changelogs/4212304.txt b/fastlane/metadata/android/zh-TW/changelogs/4212304.txt new file mode 100644 index 0000000000000000000000000000000000000000..3d1993fd14f6a43ffd106eded9ae308d2d1d3df5 --- /dev/null +++ b/fastlane/metadata/android/zh-TW/changelogs/4212304.txt @@ -0,0 +1,2 @@ +* 修復 Android 15 上的通話 +* 修復 2.17.0 中引入的罕見崩潰/回歸問題 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 index c42cc5340537f328541761290dcb04eebae5d819..b93849d57d3ca8a0800f6affa153f727e6ae5cd4 100644 --- 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 @@ -4,17 +4,17 @@ 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.ComparisonChain; import com.google.common.collect.ImmutableMap; - +import com.google.common.collect.ImmutableSortedMap; 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.Nonnull; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; @@ -38,7 +38,7 @@ public class XmlElementProcessor extends AbstractProcessor { public boolean process(Set set, RoundEnvironment roundEnvironment) { final Set elements = roundEnvironment.getElementsAnnotatedWith(XmlElement.class); - final ImmutableMap.Builder builder = ImmutableMap.builder(); + final ImmutableSortedMap.Builder builder = ImmutableSortedMap.naturalOrder(); for (final Element element : elements) { if (element instanceof final TypeElement typeElement) { final Id id = of(typeElement); @@ -160,7 +160,7 @@ public class XmlElementProcessor extends AbstractProcessor { return false; } - public static class Id { + public static class Id implements Comparable { public final String name; public final String namespace; @@ -181,5 +181,13 @@ public class XmlElementProcessor extends AbstractProcessor { public int hashCode() { return Objects.hashCode(name, namespace); } + + @Override + public int compareTo(@Nonnull Id id) { + return ComparisonChain.start() + .compare(namespace, id.namespace) + .compare(name, id.name) + .result(); + } } } diff --git a/proguard-rules.pro b/proguard-rules.pro index 7a32b40861d608a1b1f6f5cc6c8d64a7cd0908c0..8741bbc19acbca1a958d32e8f809bbc59129d1af 100644 --- a/proguard-rules.pro +++ b/proguard-rules.pro @@ -45,6 +45,11 @@ !transient ; } +# Needed for proper GSON deserialization +-keep class com.google.gson.reflect.TypeToken +-keep class * extends com.google.gson.reflect.TypeToken +-keep public class * implements java.lang.reflect.Type + # Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and # EnclosingMethod is required to use InnerClasses. -keepattributes Signature, InnerClasses, EnclosingMethod diff --git a/src/cheogram/res/xml/shortcuts.xml b/src/cheogram/res/xml/shortcuts.xml deleted file mode 100644 index 6431baeb3ce1599f228ba8064e57ac96918c1bee..0000000000000000000000000000000000000000 --- a/src/cheogram/res/xml/shortcuts.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/src/conversations/fastlane/metadata/android/es-ES/full_description.txt b/src/conversations/fastlane/metadata/android/es-ES/full_description.txt index 8535495474703dfcc474ead02580c91a8ead8b85..8c314f3f0cef64263bab72b783c17c0052faea48 100644 --- a/src/conversations/fastlane/metadata/android/es-ES/full_description.txt +++ b/src/conversations/fastlane/metadata/android/es-ES/full_description.txt @@ -1,39 +1,37 @@ -Fácil de usar, fiable y con poca batería. Con soporte integrado para imágenes, chats de grupo y cifrado e2e. +Fácil de usar, confiable y amigable con la batería. Con soporte integrado para imágenes, chats grupales y cifrado de extremo a extremo. Principios de diseño: -* Ser lo más bonito y fácil de usar posible sin sacrificar la seguridad ni la privacidad. -* Basarse en protocolos existentes y bien establecidos. -* No requerir una cuenta de Google o, específicamente, Google Cloud Messaging (GCM). -* Requerir el menor número de permisos posible - +Ser tan bello y fácil de usar como sea posible, sin sacrificar la seguridad o la privacidad. +Confiar en protocolos existentes y bien establecidos. +No requerir una cuenta de Google ni Google Cloud Messaging (GCM). +Requerir la menor cantidad de permisos posible. Características: -* Cifrado de extremo a extremo con OMEMO o OpenPGP. -* Envío y recepción de imágenes -* Llamadas de audio y vídeo cifradas (DTLS-SRTP) -* Interfaz de usuario intuitiva que sigue las directrices de diseño de Android -* Imágenes / Avatares para tus contactos -* Sincronización con el cliente de escritorio -* Conferencias (con soporte para marcadores) -* Integración de la libreta de direcciones -* Múltiples cuentas / bandeja de entrada unificada -* Muy bajo impacto en la duración de la batería - -Conversations hace que sea muy fácil crear una cuenta en el servidor gratuito conversations.im. Sin embargo, Conversations también funciona con cualquier otro servidor XMPP. Muchos servidores XMPP están gestionados por voluntarios y son gratuitos. +Cifrado de extremo a extremo con OMEMO o OpenPGP. +Envío y recepción de imágenes. +Llamadas de audio y video cifradas (DTLS-SRTP). +Interfaz intuitiva que sigue las pautas de diseño de Android. +Imágenes / Avatares para tus contactos. +Sincronización con el cliente de escritorio. +Conferencias (con soporte para marcadores). +Integración con la agenda de contactos. +Múltiples cuentas / bandeja de entrada unificada. +Muy bajo impacto en la duración de la batería. +Conversations facilita mucho la creación de una cuenta en el servidor gratuito conversations.im. Sin embargo, Conversations también funcionará con cualquier otro servidor XMPP. Muchos servidores XMPP son gestionados por voluntarios y son gratuitos. Características de XMPP: -Conversations funciona con todos los servidores XMPP existentes. Sin embargo, XMPP es un protocolo extensible. Estas extensiones también están estandarizadas en los llamados XEP. Conversations soporta un par de ellas para mejorar la experiencia general del usuario. Existe la posibilidad de que su actual servidor XMPP no soporte estas extensiones. Por lo tanto, para sacar el máximo provecho de Conversaciones deberías considerar o bien cambiar a un servidor XMPP que lo haga o - mejor aún - ejecutar tu propio servidor XMPP para ti y tus amigos. +Conversations funciona con todos los servidores XMPP disponibles. Sin embargo, XMPP es un protocolo extensible. Estas extensiones también están estandarizadas en lo que se llaman XEP. Conversations admite algunas de estas para mejorar la experiencia general del usuario. Existe la posibilidad de que tu servidor XMPP actual no soporte estas extensiones. Por lo tanto, para aprovechar al máximo Conversations, deberías considerar cambiar a un servidor XMPP que sí lo haga o, aún mejor, configurar tu propio servidor XMPP para ti y tus amigos. -Estos XEPs son (por el momento): +Estos XEP son, hasta ahora: -* XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Se utilizará para transferir archivos si ambas partes están detrás de un cortafuegos (NAT). -* XEP-0163: Protocolo de Evento Personal para avatares -* XEP-0191: El comando de bloqueo te permite hacer una lista negra de spammers o bloquear contactos sin eliminarlos de tu lista. -* XEP-0198: Stream Management permite a XMPP sobrevivir a pequeños cortes de red y cambios de la conexión TCP subyacente. -* XEP-0280: Message Carbons que sincroniza automáticamente los mensajes que envías a tu cliente de escritorio y por lo tanto te permite cambiar sin problemas de tu cliente móvil a tu cliente de escritorio y viceversa en una sola conversación. -* XEP-0237: Versionado de listas, principalmente para ahorrar ancho de banda en conexiones móviles deficientes. -* XEP-0313: Gestión de Archivo de Mensajes sincroniza el historial de mensajes con el servidor. Ponerse al día con los mensajes que fueron enviados mientras Conversaciones estaba fuera de línea. -* XEP-0352: Indicación del Estado del Cliente permite al servidor saber si Conversaciones está o no en segundo plano. Permite al servidor ahorrar ancho de banda reteniendo paquetes sin importancia. -* XEP-0363: Carga de Archivos HTTP permite compartir archivos en conferencias y con contactos sin conexión. Requiere un componente adicional en su servidor. +XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Se utilizará para transferir archivos si ambas partes están detrás de un firewall o NAT. +XEP-0163: Protocolo de Eventos Personales para avatares. +XEP-0191: El comando de bloqueo te permite poner en la lista negra a spammers o bloquear contactos sin eliminarlos de tu lista de contactos. +XEP-0198: La gestión de flujos permite que XMPP sobreviva a pequeñas interrupciones de red y cambios en la conexión TCP subyacente. +XEP-0280: Message Carbons que sincroniza automáticamente los mensajes que envías a tu cliente de escritorio, lo que te permite cambiar sin problemas entre tu cliente móvil y el de escritorio dentro de una conversación. +XEP-0237: Versionado de Roster, principalmente para ahorrar ancho de banda en conexiones móviles deficientes. +XEP-0313: Gestión del Archivo de Mensajes que sincroniza el historial de mensajes con el servidor. Pone al día los mensajes que se enviaron mientras Conversations estaba fuera de línea. +XEP-0352: Indicación de Estado del Cliente que informa al servidor si Conversations está en segundo plano o no. Permite al servidor ahorrar ancho de banda al retener paquetes no importantes. +XEP-0363: Carga de Archivos HTTP que permite compartir archivos en conferencias y con contactos fuera de línea. Requiere un componente adicional en tu servidor. 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 11f7274bdebebf58375141e69e3b5a2fa1c70449..7ed04767246cd7fab459ea0ebc43836a01ddd32f 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 teléfono inteligente +Mensajería instantánea XMPP cifrada y fácil de usar para tu dispositivo móvil diff --git a/src/conversations/fastlane/metadata/android/et/full_description.txt b/src/conversations/fastlane/metadata/android/et/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..c58150c0d86a08c6515b48dcccc2afea0e67e91e --- /dev/null +++ b/src/conversations/fastlane/metadata/android/et/full_description.txt @@ -0,0 +1,39 @@ +Lihtsaltkasutatav, töökindel ja akusõbralik. Piltide, jututubade ja läbiva krüptimise tugi. + +Eesmärgid: + +* Ilus ja lihtne rakendus ilma kahjustamata turvalisust ja privaatsust +* Olemasolevate ja toimivate protokollide kasutamine +* Peab toimima ilma Google kasutajakontota ning mitte vajama Google Cloud Messaging (GCM) teenuseid +* Peab eeldama nutiseadmes võimalikult vähe õigusi + +Funktsionaalsus: + +* Läbiv krüptimine kas OMEMO või OpenPGP abil +* Võimalus saata ja vastu võtta pilte +* Krüptitud hääl- ja videokõned (DTLS-SRTP) +* Intuitiivne kasutajaliides, mis lähtub Androidi kujunduspõhimõtetest +* Kontaktide pildid ja tunnuspildid +* Sünkroniseerimine töölauakliendiga +* Konverentsid/jututoad (järjehoidjate toega) +* Lõimimine aadressiraamatuga +* Mitme kasutajakonto kasutamise võimalus ühise sisendvooga +* Üliminimaalne mõju akukasutusele + +Conversationsi abil on lihtne lisada kasutajakontot tasuta conversations.im serveris. Aga Conversations toimib hästi kõikide XMPP serveritega. Paljusid XMPP-servereid haldavad vabatahtlikud ning nende kasutamine on tasuta. + +XMPP funktsionaalsus: + +Conversations toimib igasuguste XMPP-serveritega. Aga XMPP on laiendatav protokoll ning need laiendused on standardiseeritud XEP'idena. Et üldine kasutajakogemus oleks sujuv, toetab Conversations neist paljusid. On võimalus, et sinu XMPP serveris pole mõni konkreetne laiendus kasutusel. Seega parima tulemuse saad siis, kui leiad serveri, mis toetab neist kõiki või paned enda ja oma sõprade jaoks püsti oma XMPP serveri. + +Hetkel on toetatud XEP'id: + +* XEP-0065: SOCKS5 Bytestreams (või mod_proxy65). On kasutusel failide teisaldamisel, kui kõik vestluse osapooled on tulemüüri taga või asuvad NATitud võrgus. +* XEP-0163: Personal Eventing Protocol tunnuspiltide kasutamise jaoks. +* XEP-0191: Blocking Command võimaldab blokeerida spämmereid ja muid mittesoovitud osapooli ilma kontaktiloendi muutmiseta. +* XEP-0198: Stream Management võimaldab XMPP-ühendusel toimida väikeste võrgukatkestuste ja TCP-ühenduste muutuste puhul. +* XEP-0280: Message Carbons automaatselt sünkroniseerib sõnumid nutiseadme ja töölauakliendi vahel ning võimaldab sul tõhusalt kasutada neist seda, mida parasjagu vaja. +* XEP-0237: Roster Versioning võimaldab säästa ribalaiust kehvade sideühenduste puhul. +* XEP-0313: Message Archive Management sünkroniseerib sõnumite ajalugu serveri ja klientide vahel. See tahab, et saad sõnumid ka sõnumid ajast, mil Conversations polnud võrgus. +* XEP-0352: Client State Indication võimaldab serveril teada, kas Conversations töötab taustal. Võimaldab serveril jätta saatmata mittevajalikud paketid ja sellega säästa ribalaiust. +* XEP-0363: HTTP File Upload võimaldab jagada faile jututubades ja vallasrežiimis klientidega. Eeldab täiendava mooduli kasutamist serveris. diff --git a/src/conversations/fastlane/metadata/android/et/short_description.txt b/src/conversations/fastlane/metadata/android/et/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..1bf55355b80b6a8d33545d23ebbe73d1ef4047c7 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/et/short_description.txt @@ -0,0 +1 @@ +Krüptitud ja lihtsaltkasutatav XMPP klient sinu nutiseadme jaoks diff --git a/src/conversations/fastlane/metadata/android/it-IT/full_description.txt b/src/conversations/fastlane/metadata/android/it-IT/full_description.txt index d390af661340371a1eec874fdce2f4c5e56127b3..d9347e3bf310a3b9caaab5ecc75fd9d8ffbe8ce1 100644 --- a/src/conversations/fastlane/metadata/android/it-IT/full_description.txt +++ b/src/conversations/fastlane/metadata/android/it-IT/full_description.txt @@ -28,7 +28,7 @@ Conversations funziona con tutti i server XMPP. Tuttavia XMPP è un protocollo e Queste XEP sono, ad oggi: -* XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Usata per trasferire file se entrambe le parti sono dietro un firewall (NAT). +* XEP-0065: SOCKS5 Bytestreams (o mod_proxy65). Usata per trasferire file se entrambe le parti sono dietro un firewall o NAT. * XEP-0163: Personal Eventing Protocol. Per gli avatar. * XEP-0191: Blocking command. Ti consente di bloccare lo spam o i contatti senza rimuoverli dal tuo elenco. * XEP-0198: Stream Management. Consente a XMPP di resistere a brevi disconnessioni e cambi della connessione TCP sottostante. diff --git a/src/conversations/fastlane/metadata/android/ru-RU/short_description.txt b/src/conversations/fastlane/metadata/android/ru-RU/short_description.txt index 059ec9eb73419eb1821d52281dfecbdc1b403352..da8b6c54bd9b8282ed9218040371d299886b1daf 100644 --- a/src/conversations/fastlane/metadata/android/ru-RU/short_description.txt +++ b/src/conversations/fastlane/metadata/android/ru-RU/short_description.txt @@ -1 +1 @@ -Зашифрованный и простой в использовании XMPP мессенджер для вашего мобильного +Шифрующий и простой в использовании XMPP-мессенджер для мобильного устройства diff --git a/src/conversations/fastlane/metadata/android/sq/full_description.txt b/src/conversations/fastlane/metadata/android/sq/full_description.txt index f6f03b1513984e54293cbc4cd04fcc3ee0ab9cef..00260d588233ec04f50e5a29a50db662437e361c 100644 --- a/src/conversations/fastlane/metadata/android/sq/full_description.txt +++ b/src/conversations/fastlane/metadata/android/sq/full_description.txt @@ -36,4 +36,4 @@ Këto XEP-e janë - deri sot: * XEP-0237: Roster Versioning kryesisht për të kursyer sasi trafiku në lidhje celulare të dobëta * XEP-0313: Message Archive Management njëkohëson historik mesazhesh me shërbyesin. Ndiqni mesazhet që qenë dërguar ndërkohë që Conversations s’qe në linjë. * XEP-0352: Client State Indication i lejon shërbyesit të dijë nëse është apo jo në prapaskenë Conversations. I lejon shërbyesit të kursejë sasi trafiku, duke mbajtur paketa pa rëndësi. -* XEP-0363: HTTP File Upload ju lejon të ndani me të tjerë kartela në konferenca dhe me kontakte jo në linjë. Lyp një përbërë shtesë në shërbyesin tuaj. +* XEP-0363: HTTP File Upload ju lejon të ndani me të tjerë kartela në konferenca dhe me kontakte jo në linjë. Lyp një përbërës shtesë në shërbyesin tuaj. diff --git a/src/conversations/fastlane/metadata/android/sr/full_description.txt b/src/conversations/fastlane/metadata/android/sr/full_description.txt index 10f602f71998e6e17c4b4cbfa9d271cc2d285c98..da2c4a60ee49438fd5c26aab58593aebd170c36f 100644 --- a/src/conversations/fastlane/metadata/android/sr/full_description.txt +++ b/src/conversations/fastlane/metadata/android/sr/full_description.txt @@ -28,7 +28,7 @@ Conversations ради уз сваки XMPP сервер. Међутим XMPP ј Ови XEP-ови су - за сада: -* XEP-0065: SOCKS5 Bytestreams (или mod_proxy65). Користи се за пребацивање фајлова ако су обе стране иза firewall-а (NAT). +* XEP-0065: SOCKS5 Bytestreams (или mod_proxy65). Користи се за пребацивање фајлова ако су обе стране иза firewall-а или NAT-а). * XEP-0163: Personal Eventing Protocol за аватаре * XEP-0191: Blocking command омогућава blacklist-овање спамера или блокирање контаката без њиховог уклањања из твог списка. * XEP-0198: Stream Management омогућава да XMPP преживи мање прекиде на мрежи и промене у TCP веза. diff --git a/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt b/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt index 0e9114c98bda9640c41f1dd6b04636afcd70f52c..59318b4cd9ed3a0e36d248e552114ceebd09f358 100644 --- a/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt +++ b/src/conversations/fastlane/metadata/android/zh-TW/full_description.txt @@ -28,7 +28,7 @@ Conversations 可以在所有 XMPP 伺服器上運作。然而,XMPP 是一個 如下 XEP - 截止目前: -* XEP-0065:SOCKS5 位元資料流 (或 mod_proxy65),將被用於傳輸檔案,如果雙方都在防火牆之後 (NAT)。 +* XEP-0065:SOCKS5 位元資料流 (或 mod_proxy65),將被用於傳輸檔案,如果雙方都在防火牆或NAT之後。 * XEP-0163:用於虛擬化身的私人活動通訊協定 * XEP-0191:封鎖命令可讓您將濫發垃圾郵件者列入黑名單,或封鎖聯絡人而不把他們從名冊中移除。 * XEP-0198:串流管理允許 XMPP 在小型網路中斷和基礎 TCP 連線的變更中生存。 diff --git a/src/conversations/res/values-fi/strings.xml b/src/conversations/res/values-fi/strings.xml index c416b255544f83e01f5bc9d99e693b3dcab29283..fdf1fc58d05b77962b3b81f0ec909b4b540fe3ad 100644 --- a/src/conversations/res/values-fi/strings.xml +++ b/src/conversations/res/values-fi/strings.xml @@ -4,12 +4,13 @@ Käytä conversations.im:ää Luo uusi tili Onko sinulla jo XMPP-tunnus? Jos käytät jo toista XMPP-sovellusta tai olet käyttänyt Conversationsia aiemmin, niin voi olla. Jos ei, voit tehdä uuden XMPP-tilin saman tien.\nVinkki: Jotkin sähköpostipalvelut tarjoavat myös XMPP-tilin. - XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä asiakasohjelmaa minkä tahansa haluamasi XMPP-palvelimen kanssa. -\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin. + XMPP on tietystä palveluntarjoasta riippumaton pikaviestiverkosto. Voit käyttää tätä sovellusta minkä tahansa haluamasi XMPP-palvelimen kanssa.\nHelppouden nimissä olemme kuitenkin helpottaneet tilin luomista conversations.im:iin. Sinut on kutsuttu %1$s:iin. Opastamme sinua tilin luomisen kanssa.\nValitessasi palvelimen %1$s palveluntarjoajaksesi voit jutella muiden palveluntajoajien käyttäjien kanssa kertomalla heille koko XMPP-osoitteesi. Sinut on kutsuttu palvelimelle %1$s. Käyttäjänimesi on valittu valmiiksi puolestasi. Opastamme sinua tilin luomisen kanssa.\nVoit jutella muiden palveluntarjoajien käyttäjien kanssa kertomalle heille koko XMPP-osoitteesi. Kutsusi palvelimelle Virheellisesti muotoiltu koodi Jos henkilö on lähellä, hän voi myös hyväksyä kutsun lukemalla allaolevan koodin. - Jaa kutsu sovelluksella... + Jaa kutsu sovelluksella… + Napauta jakamispainiketta lähettääksesi kontaktillesi kutsun %1$s:een. + Liity %1$s:hen ja pikakeskustele kanssani: %2$s \ No newline at end of file diff --git a/src/conversations/res/values-hu/strings.xml b/src/conversations/res/values-hu/strings.xml index 27280900778c3a8930b030f46b7c7bcb0cdd298d..069f6511e9231941921c4b24429b7332cf6f53c8 100644 --- a/src/conversations/res/values-hu/strings.xml +++ b/src/conversations/res/values-hu/strings.xml @@ -4,8 +4,7 @@ A conversations.im használata Új fiók létrehozása Már rendelkezik XMPP-fiókkal? Ez az eset állhat fenn, ha már egy másik XMPP-klienst használ, vagy ha már korábban használta a Conversations alkalmazást. Ha nem, akkor most létrehozhat egy új XMPP-fiókot.\nTipp: egyes e-mail szolgáltatók is biztosítanak XMPP-fiókokat. - Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt a kliensprogramot bármely XMPP-kiszolgálóhoz használhatja. -\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. + Az XMPP egy szolgáltatófüggetlen, azonnali üzenetküldő hálózat. Ezt az alkalmazást bármely XMPP-kiszolgálóhoz használhatja.\nAzonban a kényelem érdekében megkönnyítettük a conversations.im szolgáltatón való fióklétrehozást, ami kifejezetten a Conversations alkalmazással történő használatra lett tervezve. Meghívást kapott a(z) %1$s kiszolgálóra. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nHa a(z) %1$s kiszolgálót választja szolgáltatóként, akkor képes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. Meghívást kapott a(z) %1$s kiszolgálóra. Már kiválasztottak Önnek egy felhasználónevet. Végig fogjuk vezetni egy fiók létrehozásának folyamatán.\nKépes lesz más szolgáltatók felhasználóival is kommunikálni, ha megadja nekik a teljes XMPP-címét. Az Ön kiszolgálómeghívása diff --git a/src/conversations/res/values-ru/strings.xml b/src/conversations/res/values-ru/strings.xml index 27f98bae9d8133fde9b9ca8268be0d5cef6bbe5b..6b9f61657750e8d7ea8e7032ad86179687e5ff55 100644 --- a/src/conversations/res/values-ru/strings.xml +++ b/src/conversations/res/values-ru/strings.xml @@ -4,7 +4,7 @@ Использовать conversations.im Создать новый аккаунт У вас есть аккаунт XMPP? Если вы использовали Conversations или другой XMPP-клиент в прошлом, то скорее всего, он у вас есть. Если у вас нет аккаунта, вы можете создать его прямо сейчас. -\nПодсказка: некоторые провайдеры электронной почты также регистрируют аккаунты XMPP. +\nПодсказка: некоторые провайдеры эл. почты также предоставляют аккаунты XMPP. XMPP - это независимая сеть обмена сообщениями. Это приложение позволяет подключиться к любому XMPP-серверу на ваш выбор. \nЕсли у вас нет сервера, предлагаем вам зарегистрировать аккаунт на conversations.im, сервере, специально предназначенном для работы с Conversations. Вас пригласили на %1$s. Мы проведём вас через процесс создания аккаунта. @@ -13,8 +13,8 @@ \nЭтот аккаунт позволит вам общаться с пользователями и на этом, и на других серверах, используя ваш полный XMPP-адрес. Ваше приглашение Неправильный формат кода - Нажмите кнопку «Поделиться», чтобы отправить вашему контакту приглашение в %1$s. + Нажмите кнопку \"Поделиться\", чтобы отправить вашему контакту приглашение в %1$s. Если ваш контакт находится поблизости, он также может отсканировать приведённый ниже код, чтобы принять ваше приглашение. Присоединяйтесь к %1$s и пообщайтесь со мной: %2$s - Поделиться приглашением с… + Поделиться приглашением… \ 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 e1507867a4fffc811492721f88e39a81b136be03..73198d777707c7291bef21e051400cdc42151ab6 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -1,20 +1,16 @@ - 选择您的 XMPP 提供者 + 选择 XMPP 提供者 使用 conversations.im 创建新账号 - 您已经有 XMPP 账号了吗?如果您之前使用过 Conversations 或其他 XMPP 客户端,那么您已经有账号了。如果没有,您可以立即创建一个。 -\n提示:一些电子邮件服务也提供 XMPP 账号。 - XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此应用。 -\n不过,您可以轻松地在 conversations.im 上创建账号;特别适合与 Conversations 使用的提供者。 - 您已受邀加入 %1$s。我们将指导您创建账号。 -\n当选择 %1$s 作为提供者时,向其他 XMPP 用户提供您的完整地址,就能和对方交流。 - 您已受邀加入 %1$s。已为您选择了用户名。我们将指导您创建账号。 -\n向其他 XMPP 用户提供您的完整地址,就能和对方交流。 + 您已经有 XMPP 账号了吗?如果之前使用过 Conversations 或其他 XMPP 客户端,那么已经有账号了。如果没有,现在可以创建账号。\n提示:一些邮件服务提供者也提供 XMPP 账号。 + XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此应用。\n不过,您可以轻松地在 conversations.im 上创建账号,这是专门适用于 Conversations 的提供者。 + 您已受邀加入 %1$s。我们将指导您创建账号。\n选择 %1$s 作为提供者时,向别人提供您的完整 XMPP 地址,就能和对方交流。 + 您已受邀加入 %1$s。已为您选择了用户名。我们将指导您创建账号。\n向别人提供您的完整 XMPP 地址,就能和对方交流。 您的服务器邀请 配置代码格式不正确 - 点击分享按钮,向您的联系人发送加入 %1$s 的邀请。 - 如果您的联系人在附近,对方也可以扫描下方二维码接受邀请。 + 点按分享按钮,向您的联系人发送 %1$s 的邀请。 + 如果联系人在附近,也可以扫描下方二维码接受邀请。 加入 %1$s 和我聊天:%2$s - 分享邀请至… + 分享邀请… \ No newline at end of file diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 414f18f7d3575d993bc8ad820a9f22b706f60ee0..11fc651b993d0a6504e9a29f683e21a5949a4fa8 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -124,14 +124,6 @@ android:name=".services.ImportBackupService" android:exported="false" android:foregroundServiceType="dataSync" /> - - - - - - + - + android:value="androidx.sharetarget.ChooserTargetServiceCompat" /> + android:label="@string/media_browser" /> + of(final Element channelBinding) { - Preconditions.checkArgument( - channelBinding == null - || ("sasl-channel-binding".equals(channelBinding.getName()) - && Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())), - "pass null or a valid channel binding stream feature"); + public static Collection of(final SaslChannelBinding channelBinding) { + if (channelBinding == null) { + return Collections.emptyList(); + } return Collections2.filter( Collections2.transform( - Collections2.filter( - channelBinding == null - ? Collections.emptyList() - : channelBinding.getChildren(), - c -> c != null && "channel-binding".equals(c.getName())), - c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))), + channelBinding.getChannelBindings(), cb -> ChannelBinding.of(cb.getType())), Predicates.notNull()); } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DowngradeProtection.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DowngradeProtection.java new file mode 100644 index 0000000000000000000000000000000000000000..6daaa8809398e6f4aa2c7d311ed894fc0ae05299 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DowngradeProtection.java @@ -0,0 +1,98 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import java.util.Collection; + +public class DowngradeProtection { + + private static final char SEPARATOR = ','; + private static final char SEPARATOR_MECHANISM_AND_BINDING = '|'; + + public final ImmutableList mechanisms; + public final ImmutableList channelBindings; + + public DowngradeProtection( + final Collection mechanisms, final Collection channelBindings) { + this.mechanisms = Ordering.natural().immutableSortedCopy(mechanisms); + this.channelBindings = Ordering.natural().immutableSortedCopy(channelBindings); + } + + public DowngradeProtection(final Collection mechanisms) { + this.mechanisms = Ordering.natural().immutableSortedCopy(mechanisms); + this.channelBindings = null; + } + + public String asDString() { + ensureSaslMechanismFormat(this.mechanisms); + ensureNoSeparators(this.mechanisms); + if (this.channelBindings != null) { + ensureNoSeparators(this.channelBindings); + ensureBindingFormat(this.channelBindings); + final var builder = new StringBuilder(); + Joiner.on(SEPARATOR).appendTo(builder, mechanisms); + builder.append(SEPARATOR_MECHANISM_AND_BINDING); + Joiner.on(SEPARATOR).appendTo(builder, channelBindings); + return builder.toString(); + } else { + return Joiner.on(SEPARATOR).join(mechanisms); + } + } + + private static void ensureNoSeparators(final Iterable list) { + for (final String item : list) { + if (item.indexOf(SEPARATOR) >= 0 + || item.indexOf(SEPARATOR_MECHANISM_AND_BINDING) >= 0) { + throw new SecurityException("illegal chars found in list"); + } + } + } + + private static void ensureSaslMechanismFormat(final Iterable names) { + for (final String name : names) { + ensureSaslMechanismFormat(name); + } + } + + private static void ensureSaslMechanismFormat(final String name) { + if (Strings.isNullOrEmpty(name)) { + throw new SecurityException("Empty sasl mechanism names are not permitted"); + } + // https://www.rfc-editor.org/rfc/rfc4422.html#section-3.1 + if (name.length() <= 20 + && CharMatcher.inRange('A', 'Z') + .or(CharMatcher.inRange('0', '9')) + .or(CharMatcher.is('-')) + .or(CharMatcher.is('_')) + .matchesAllOf(name) + && !Character.isDigit(name.charAt(0))) { + return; + } + throw new SecurityException("Encountered illegal sasl name"); + } + + private static void ensureBindingFormat(final Iterable names) { + for (final String name : names) { + ensureBindingFormat(name); + } + } + + private static void ensureBindingFormat(final String name) { + if (Strings.isNullOrEmpty(name)) { + throw new SecurityException("Empty binding names are not permitted"); + } + // https://www.rfc-editor.org/rfc/rfc5056.html#section-7d + if (CharMatcher.inRange('A', 'Z') + .or(CharMatcher.inRange('a', 'z')) + .or(CharMatcher.inRange('0', '9')) + .or(CharMatcher.is('.')) + .or(CharMatcher.is('-')) + .matchesAllOf(name)) { + return; + } + throw new SecurityException("Encountered illegal binding name"); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 93c722e372e1204f5759e8e0b30ac6fb13c9b166..40f48a2392d6523ab4a6d573c1b116033b652f7a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -1,20 +1,15 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Log; - import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.Collections2; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.SSLSockets; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; - import java.util.Collection; import java.util.Collections; - import javax.net.ssl.SSLSocket; public abstract class SaslMechanism { @@ -53,18 +48,7 @@ public abstract class SaslMechanism { return ""; } - public static Collection mechanisms(final Element authElement) { - if (authElement == null) { - return Collections.emptyList(); - } - return Collections2.transform( - Collections2.filter( - authElement.getChildren(), - c -> c != null && "mechanism".equals(c.getName())), - c -> c == null ? null : c.getContent()); - } - - protected enum State { + public enum State { INITIAL, AUTH_TEXT_SENT, RESPONSE_SENT, @@ -76,14 +60,11 @@ public abstract class SaslMechanism { SASL_2; public static Version of(final Element element) { - switch (Strings.nullToEmpty(element.getNamespace())) { - case Namespace.SASL: - return SASL; - case Namespace.SASL_2: - return SASL_2; - default: - throw new IllegalArgumentException("Unrecognized SASL namespace"); - } + return switch (Strings.nullToEmpty(element.getNamespace())) { + case Namespace.SASL -> SASL; + case Namespace.SASL_2 -> SASL_2; + default -> throw new IllegalArgumentException("Unrecognized SASL namespace"); + }; } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index b3c36cda6f8890e7bcf276d728952d4ffcd271eb..97ae1600ecfe8a95d4d34b7fc8253174110f3b84 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -1,24 +1,27 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Base64; - import com.google.common.base.CaseFormat; +import com.google.common.base.Joiner; import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableMap; import com.google.common.hash.HashFunction; - -import java.nio.charset.Charset; +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.Ints; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.Map; import java.util.concurrent.ExecutionException; - import javax.crypto.SecretKey; import javax.net.ssl.SSLSocket; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; - -abstract class ScramMechanism extends SaslMechanism { +public abstract class ScramMechanism extends SaslMechanism { public static final SecretKey EMPTY_KEY = new SecretKey() { @@ -46,8 +49,9 @@ abstract class ScramMechanism extends SaslMechanism { private final String gs2Header; private final String clientNonce; protected State state = State.INITIAL; - private String clientFirstMessageBare; + private final String clientFirstMessageBare; private byte[] serverSignature = null; + private DowngradeProtection downgradeProtection = null; ScramMechanism(final Account account, final ChannelBinding channelBinding) { super(account); @@ -67,28 +71,41 @@ abstract class ScramMechanism extends SaslMechanism { } // This nonce should be different for each authentication attempt. this.clientNonce = CryptoHelper.random(100); - clientFirstMessageBare = ""; + this.clientFirstMessageBare = + String.format( + "n=%s,r=%s", + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())), + this.clientNonce); + } + + public void setDowngradeProtection(final DowngradeProtection downgradeProtection) { + Preconditions.checkState( + this.state == State.INITIAL, "setting downgrade protection in invalid state"); + this.downgradeProtection = downgradeProtection; } protected abstract HashFunction getHMac(final byte[] key); protected abstract HashFunction getDigest(); - private KeyPair getKeyPair(final String password, final String salt, final int iterations) + private KeyPair getKeyPair(final String password, final byte[] salt, final int iterations) throws ExecutionException { - return CACHE.get( - new CacheKey(getMechanism(), password, salt, iterations), - () -> { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = - hi( - password.getBytes(), - Base64.decode(salt, Base64.DEFAULT), - iterations); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - return new KeyPair(clientKey, serverKey); - }); + final var key = new CacheKey(getMechanism(), password, salt, iterations); + return CACHE.get(key, () -> calculateKeyPair(password, salt, iterations)); + } + + private KeyPair calculateKeyPair(final String password, final byte[] salt, final int iterations) + throws InvalidKeyException { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = hi(password.getBytes(), salt, iterations); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + return new KeyPair(clientKey, serverKey); + } + + @Override + public String getMechanism() { + return ""; } private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { @@ -119,152 +136,167 @@ abstract class ScramMechanism extends SaslMechanism { @Override public String getClientFirstMessage(final SSLSocket sslSocket) { - if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = - "n=" - + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) - + ",r=" - + this.clientNonce; - state = State.AUTH_TEXT_SENT; - } - return Base64.encodeToString( - (gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); + Preconditions.checkState( + this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state"); + this.state = State.AUTH_TEXT_SENT; + final byte[] message = (gs2Header + clientFirstMessageBare).getBytes(); + return BaseEncoding.base64().encode(message); } @Override public String getResponse(final String challenge, final SSLSocket socket) throws AuthenticationException { - switch (state) { - case AUTH_TEXT_SENT: - if (challenge == null) { - throw new AuthenticationException("challenge can not be null"); - } - byte[] serverFirstMessage; - try { - serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); - } catch (IllegalArgumentException e) { - throw new AuthenticationException("Unable to decode server challenge", e); - } - final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); - String nonce = ""; - int iterationCount = -1; - String salt = ""; - for (final String token : tokenizer) { - if (token.length() > 1 && token.charAt(1) == '=') { - switch (token.charAt(0)) { - case 'i': - try { - iterationCount = Integer.parseInt(token.substring(2)); - } catch (final NumberFormatException e) { - throw new AuthenticationException(e); - } - break; - case 's': - salt = token.substring(2); - break; - case 'r': - nonce = token.substring(2); - break; - case 'm': - /* - * RFC 5802: - * m: This attribute is reserved for future extensibility. In this - * version of SCRAM, its presence in a client or a server message - * MUST cause authentication failure when the attribute is parsed by - * the other end. - */ - throw new AuthenticationException( - "Server sent reserved token: `m'"); - } - } - } + return switch (state) { + case AUTH_TEXT_SENT -> processServerFirstMessage(challenge, socket); + case RESPONSE_SENT -> processServerFinalMessage(challenge); + default -> throw new InvalidStateException(state); + }; + } - if (iterationCount < 0) { - throw new AuthenticationException("Server did not send iteration count"); - } - if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException( - "Server nonce does not contain client nonce: " + nonce); - } - if (salt.isEmpty()) { - throw new AuthenticationException("Server sent empty salt"); - } + private String processServerFirstMessage(final String challenge, final SSLSocket socket) + throws AuthenticationException { + if (Strings.isNullOrEmpty(challenge)) { + throw new AuthenticationException("challenge can not be null"); + } + byte[] serverFirstMessage; + try { + serverFirstMessage = BaseEncoding.base64().decode(challenge); + } catch (final IllegalArgumentException e) { + throw new AuthenticationException("Unable to decode server challenge", e); + } + final Map attributes; + try { + attributes = splitToAttributes(new String(serverFirstMessage)); + } catch (final IllegalArgumentException e) { + throw new AuthenticationException("Duplicate attributes"); + } + if (attributes.containsKey("m")) { + /* + * RFC 5802: + * m: This attribute is reserved for future extensibility. In this + * version of SCRAM, its presence in a client or a server message + * MUST cause authentication failure when the attribute is parsed by + * the other end. + */ + throw new AuthenticationException("Server sent reserved token: 'm'"); + } + final String i = attributes.get("i"); + final String s = attributes.get("s"); + final String nonce = attributes.get("r"); + final String d = attributes.get("d"); + if (Strings.isNullOrEmpty(s) || Strings.isNullOrEmpty(nonce) || Strings.isNullOrEmpty(i)) { + throw new AuthenticationException("Missing attributes from server first message"); + } + final Integer iterationCount = Ints.tryParse(i); - final byte[] channelBindingData = getChannelBindingData(socket); - - final int gs2Len = this.gs2Header.getBytes().length; - final byte[] cMessage = new byte[gs2Len + channelBindingData.length]; - System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len); - System.arraycopy( - channelBindingData, 0, cMessage, gs2Len, channelBindingData.length); - - final String clientFinalMessageWithoutProof = - "c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce; - - final byte[] authMessage = - (clientFirstMessageBare - + ',' - + new String(serverFirstMessage) - + ',' - + clientFinalMessageWithoutProof) - .getBytes(); - - final KeyPair keys; - try { - keys = - getKeyPair( - CryptoHelper.saslPrep(account.getPassword()), - salt, - iterationCount); - } catch (ExecutionException e) { - throw new AuthenticationException("Invalid keys generated"); - } - final byte[] clientSignature; - try { - serverSignature = hmac(keys.serverKey, authMessage); - final byte[] storedKey = digest(keys.clientKey); + if (iterationCount == null || iterationCount < 0) { + throw new AuthenticationException("Server did not send iteration count"); + } + if (!nonce.startsWith(clientNonce)) { + throw new AuthenticationException( + "Server nonce does not contain client nonce: " + nonce); + } - clientSignature = hmac(storedKey, authMessage); + final byte[] salt; - } catch (final InvalidKeyException e) { - throw new AuthenticationException(e); - } + try { + salt = BaseEncoding.base64().decode(s); + } catch (final IllegalArgumentException e) { + throw new AuthenticationException("Invalid salt in server first message"); + } - final byte[] clientProof = new byte[keys.clientKey.length]; + if (d != null && this.downgradeProtection != null) { + final String asSeenInFeatures; + try { + asSeenInFeatures = downgradeProtection.asDString(); + } catch (final SecurityException e) { + throw new AuthenticationException(e); + } + final var hashed = BaseEncoding.base64().encode(digest(asSeenInFeatures.getBytes())); + if (!hashed.equals(d)) { + throw new AuthenticationException("Mismatch in SSDP"); + } + } - if (clientSignature.length < keys.clientKey.length) { - throw new AuthenticationException( - "client signature was shorter than clientKey"); - } + final byte[] channelBindingData = getChannelBindingData(socket); - for (int i = 0; i < clientProof.length; i++) { - clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); - } + final int gs2Len = this.gs2Header.getBytes().length; + final byte[] cMessage = new byte[gs2Len + channelBindingData.length]; + System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len); + System.arraycopy(channelBindingData, 0, cMessage, gs2Len, channelBindingData.length); - final String clientFinalMessage = - clientFinalMessageWithoutProof - + ",p=" - + Base64.encodeToString(clientProof, Base64.NO_WRAP); - state = State.RESPONSE_SENT; - return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); - case RESPONSE_SENT: - try { - final String clientCalculatedServerFinalMessage = - "v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals( - new String(Base64.decode(challenge, Base64.DEFAULT)))) { - throw new Exception(); - } - state = State.VALID_SERVER_RESPONSE; - return ""; - } catch (Exception e) { - throw new AuthenticationException( - "Server final message does not match calculated final message"); - } - default: - throw new InvalidStateException(state); + final String clientFinalMessageWithoutProof = + String.format("c=%s,r=%s", BaseEncoding.base64().encode(cMessage), nonce); + + final var authMessage = + Joiner.on(',') + .join( + clientFirstMessageBare, + new String(serverFirstMessage), + clientFinalMessageWithoutProof); + + final KeyPair keys; + try { + keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount); + } catch (final ExecutionException e) { + throw new AuthenticationException("Invalid keys generated"); } + final byte[] clientSignature; + try { + serverSignature = hmac(keys.serverKey, authMessage.getBytes()); + final byte[] storedKey = digest(keys.clientKey); + + clientSignature = hmac(storedKey, authMessage.getBytes()); + + } catch (final InvalidKeyException e) { + throw new AuthenticationException(e); + } + + final byte[] clientProof = new byte[keys.clientKey.length]; + + if (clientSignature.length < keys.clientKey.length) { + throw new AuthenticationException("client signature was shorter than clientKey"); + } + + for (int j = 0; j < clientProof.length; j++) { + clientProof[j] = (byte) (keys.clientKey[j] ^ clientSignature[j]); + } + + final var clientFinalMessage = + String.format( + "%s,p=%s", + clientFinalMessageWithoutProof, BaseEncoding.base64().encode(clientProof)); + this.state = State.RESPONSE_SENT; + return BaseEncoding.base64().encode(clientFinalMessage.getBytes()); + } + + private Map splitToAttributes(final String message) { + final ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (final String token : Splitter.on(',').split(message)) { + final var tuple = Splitter.on('=').limit(2).splitToList(token); + if (tuple.size() == 2) { + builder.put(tuple.get(0), tuple.get(1)); + } + } + return builder.buildOrThrow(); + } + + private String processServerFinalMessage(final String challenge) + throws AuthenticationException { + final String serverFinalMessage; + try { + serverFinalMessage = new String(BaseEncoding.base64().decode(challenge)); + } catch (final IllegalArgumentException e) { + throw new AuthenticationException("Invalid base64 in server final message", e); + } + final var clientCalculatedServerFinalMessage = + String.format("v=%s", BaseEncoding.base64().encode(serverSignature)); + if (clientCalculatedServerFinalMessage.equals(serverFinalMessage)) { + this.state = State.VALID_SERVER_RESPONSE; + return ""; + } + throw new AuthenticationException( + "Server final message does not match calculated final message"); } protected byte[] getChannelBindingData(final SSLSocket sslSocket) @@ -276,12 +308,16 @@ abstract class ScramMechanism extends SaslMechanism { } private static class CacheKey { - final String algorithm; - final String password; - final String salt; - final int iterations; - - private CacheKey(String algorithm, String password, String salt, int iterations) { + private final String algorithm; + private final String password; + private final byte[] salt; + private final int iterations; + + private CacheKey( + final String algorithm, + final String password, + final byte[] salt, + final int iterations) { this.algorithm = algorithm; this.password = password; this.salt = salt; @@ -289,19 +325,20 @@ abstract class ScramMechanism extends SaslMechanism { } @Override - public boolean equals(Object o) { + public boolean equals(final Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CacheKey cacheKey = (CacheKey) o; return iterations == cacheKey.iterations && Objects.equal(algorithm, cacheKey.algorithm) && Objects.equal(password, cacheKey.password) - && Objects.equal(salt, cacheKey.salt); + && Arrays.equals(salt, cacheKey.salt); } @Override public int hashCode() { - return Objects.hashCode(algorithm, password, salt, iterations); + final int result = Objects.hashCode(algorithm, password, iterations); + return 31 * result + Arrays.hashCode(salt); } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index a5831a520b52404dab31f432a26b608b64f28966..0cd880c85c7c82e4211a49a400216aa9988dc165 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -7,6 +7,7 @@ import android.os.SystemClock; import android.util.Log; import androidx.core.graphics.ColorUtils; +import androidx.annotation.NonNull; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -340,11 +341,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.password = password; } + @NonNull public String getHostname() { return Strings.nullToEmpty(this.hostname); } - public void setHostname(String hostname) { + public void setHostname(final String hostname) { this.hostname = hostname; } @@ -432,7 +434,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public HashedToken getFastMechanism() { - final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism); + final HashedToken.Mechanism fastMechanism = + HashedToken.Mechanism.ofOrNull(this.fastMechanism); final String token = this.fastToken; if (fastMechanism == null || Strings.isNullOrEmpty(token)) { return null; @@ -841,11 +844,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable public enum State { DISABLED(false, false), - LOGGED_OUT(false,false), + LOGGED_OUT(false, false), OFFLINE(false), CONNECTING(false), ONLINE(false), NO_INTERNET(false), + CONNECTION_TIMEOUT, UNAUTHORIZED, TEMPORARY_AUTH_FAILURE, SERVER_NOT_FOUND, @@ -915,6 +919,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return R.string.account_status_not_found; case NO_INTERNET: return R.string.account_status_no_internet; + case CONNECTION_TIMEOUT: + return R.string.account_status_connection_timeout; case REGISTRATION_FAILED: return R.string.account_status_regis_fail; case REGISTRATION_WEB: diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 592016b97f560661fc8d69a3e8341d6d1e3d9b04..37467abcd04abeddb0eae00dc16652cc9c707c29 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -237,7 +237,7 @@ public class Bookmark extends Element implements ListItem { } public String getNick() { - return this.findChildContent("nick"); + return Strings.emptyToNull(this.findChildContent("nick")); } public void setNick(String nick) { @@ -315,7 +315,6 @@ public class Bookmark extends Element implements ListItem { this.conversation = null; } else { this.conversation = new WeakReference<>(conversation); - conversation.getMucOptions().notifyOfBookmarkNick(getNick()); } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index ceb15b3a31877761dedf77ef89a534ca2c13e639..7c6f9ec9d5609c65022e1d94d3f7d8ce2b31fa8c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -32,7 +32,6 @@ import java.util.Objects; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; import eu.siacs.conversations.android.AbstractPhoneContact; import eu.siacs.conversations.android.JabberIdContact; import eu.siacs.conversations.persistance.FileBackend; @@ -639,7 +638,7 @@ public class Contact implements ListItem, Blockable { public synchronized boolean unsetPhoneContact(Class clazz) { resetOption(getOption(clazz)); boolean changed = false; - if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) { + if (!getOption(Options.SYNCED_VIA_ADDRESS_BOOK) && !getOption(Options.SYNCED_VIA_OTHER)) { setSystemAccount(null); changed |= setPhotoUri(null); changed |= setSystemName(null); @@ -713,7 +712,7 @@ public class Contact implements ListItem, Blockable { public static int getOption(Class clazz) { if (clazz == JabberIdContact.class) { - return Options.SYNCED_VIA_ADDRESSBOOK; + return Options.SYNCED_VIA_ADDRESS_BOOK; } else { return Options.SYNCED_VIA_OTHER; } @@ -756,7 +755,7 @@ public class Contact implements ListItem, Blockable { public static final int PENDING_SUBSCRIPTION_REQUEST = 5; public static final int DIRTY_PUSH = 6; public static final int DIRTY_DELETE = 7; - private static final int SYNCED_VIA_ADDRESSBOOK = 8; + private static final int SYNCED_VIA_ADDRESS_BOOK = 8; public static final int SYNCED_VIA_OTHER = 9; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 52b5f0c2941d75ef984825ad5d7487ba7a4b4000..1e790acad03f1b9ee38dffa1b18d465fbf205f6d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.entities; +import static eu.siacs.conversations.entities.Bookmark.printableValue; + import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; @@ -75,11 +77,12 @@ import com.google.android.material.color.MaterialColors; import com.google.android.material.tabs.TabLayout; import com.google.android.material.textfield.TextInputLayout; import com.google.common.base.Optional; +import com.google.common.base.Strings; import com.google.common.collect.ComparisonChain; +import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; -import com.google.common.collect.HashMultimap; import io.ipfs.cid.Cid; @@ -156,12 +159,21 @@ 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 java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.atomic.AtomicBoolean; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; 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 class Conversation extends AbstractEntity + implements Blockable, Comparable, Conversational, AvatarService.Avatarable { public static final String TABLENAME = "conversations"; public static final int STATUS_AVAILABLE = 0; @@ -180,7 +192,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; public static final String ATTRIBUTE_NOTIFY_REPLIES = "notify_replies"; public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history"; - public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = "formerly_private_non_anonymous"; + public static final String ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS = + "formerly_private_non_anonymous"; public static final String ATTRIBUTE_PINNED_ON_TOP = "pinned_on_top"; static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; static final String ATTRIBUTE_MEMBERS_ONLY = "members_only"; @@ -202,7 +215,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private int status; private final long created; private int mode; - private JSONObject attributes; + private final JSONObject attributes; private Jid nextCounterpart; private transient MucOptions mucOptions = null; private boolean messagesLeftOnServer = true; @@ -220,17 +233,31 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl private String displayState = null; protected boolean anyMatchSpam = false; - public Conversation(final String name, final Account account, final Jid contactJid, - final int mode) { - this(java.util.UUID.randomUUID().toString(), name, null, account - .getUuid(), contactJid, System.currentTimeMillis(), - STATUS_AVAILABLE, mode, ""); + public Conversation( + final String name, final Account account, final Jid contactJid, final int mode) { + this( + java.util.UUID.randomUUID().toString(), + name, + null, + account.getUuid(), + contactJid, + System.currentTimeMillis(), + STATUS_AVAILABLE, + mode, + ""); this.account = account; } - public Conversation(final String uuid, final String name, final String contactUuid, - final String accountUuid, final Jid contactJid, final long created, final int status, - final int mode, final String attributes) { + public Conversation( + final String uuid, + final String name, + final String contactUuid, + final String accountUuid, + final Jid contactJid, + final long created, + final int status, + final int mode, + final String attributes) { this.uuid = uuid; this.name = name; this.contactUuid = contactUuid; @@ -239,26 +266,37 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl this.created = created; this.status = status; this.mode = mode; - try { - this.attributes = new JSONObject(attributes == null ? "" : attributes); - } catch (JSONException e) { - this.attributes = new JSONObject(); + this.attributes = parseAttributes(attributes); + } + + private static JSONObject parseAttributes(final String attributes) { + if (Strings.isNullOrEmpty(attributes)) { + return new JSONObject(); + } else { + try { + return new JSONObject(attributes); + } catch (final JSONException e) { + return new JSONObject(); + } } } - public static Conversation fromCursor(Cursor cursor) { - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), - cursor.getString(cursor.getColumnIndex(NAME)), - cursor.getString(cursor.getColumnIndex(CONTACT)), - cursor.getString(cursor.getColumnIndex(ACCOUNT)), - JidHelper.parseOrFallbackToInvalid(cursor.getString(cursor.getColumnIndex(CONTACTJID))), - cursor.getLong(cursor.getColumnIndex(CREATED)), - cursor.getInt(cursor.getColumnIndex(STATUS)), - cursor.getInt(cursor.getColumnIndex(MODE)), - cursor.getString(cursor.getColumnIndex(ATTRIBUTES))); + public static Conversation fromCursor(final Cursor cursor) { + return new Conversation( + cursor.getString(cursor.getColumnIndexOrThrow(UUID)), + cursor.getString(cursor.getColumnIndexOrThrow(NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(CONTACT)), + cursor.getString(cursor.getColumnIndexOrThrow(ACCOUNT)), + JidHelper.parseOrFallbackToInvalid( + cursor.getString(cursor.getColumnIndexOrThrow(CONTACTJID))), + cursor.getLong(cursor.getColumnIndexOrThrow(CREATED)), + cursor.getInt(cursor.getColumnIndexOrThrow(STATUS)), + cursor.getInt(cursor.getColumnIndexOrThrow(MODE)), + cursor.getString(cursor.getColumnIndexOrThrow(ATTRIBUTES))); } - public static Message getLatestMarkableMessage(final List messages, boolean isPrivateAndNonAnonymousMuc) { + public static Message getLatestMarkableMessage( + final List messages, boolean isPrivateAndNonAnonymousMuc) { for (int i = messages.size() - 1; i >= 0; --i) { final Message message = messages.get(i); if (message.getStatus() <= Message.STATUS_RECEIVED @@ -279,10 +317,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } final String contact = conversation.getJid().getDomain().toEscapedString(); final String account = conversation.getAccount().getServer(); - if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { + if (Config.OMEMO_EXCEPTIONS.matchesContactDomain(contact) + || Config.OMEMO_EXCEPTIONS.ACCOUNT_DOMAINS.contains(account)) { return false; } - return conversation.isSingleOrPrivateAndNonAnonymous() || conversation.getBooleanAttribute(ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); + return conversation.isSingleOrPrivateAndNonAnonymous() + || conversation.getBooleanAttribute( + ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, false); } public boolean hasMessagesLeftOnServer() { @@ -334,7 +375,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public int countFailedDeliveries() { int count = 0; synchronized (this.messages) { - for(final Message message : this.messages) { + for (final Message message : this.messages) { if (message.getStatus() == Message.STATUS_SEND_FAILED) { ++count; } @@ -358,12 +399,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return null; } - public Message findUnsentMessageWithUuid(String uuid) { synchronized (this.messages) { for (final Message message : this.messages) { final int s = message.getStatus(); - if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { + if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) + && message.getUuid().equals(uuid)) { return message; } } @@ -404,10 +445,16 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (final Message message : this.messages) { final Transferable transferable = message.getTransferable(); - final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); + final boolean unInitiatedButKnownSize = + MessageUtils.unInitiatedButKnownSize(message); if (message.getUuid().equals(uuid) && message.getEncryption() != Message.ENCRYPTION_PGP - && (message.isFileOrImage() || message.treatAsDownloadable() || unInitiatedButKnownSize || (transferable != null && transferable.getStatus() != Transferable.STATUS_UPLOADING))) { + && (message.isFileOrImage() + || message.treatAsDownloadable() + || unInitiatedButKnownSize + || (transferable != null + && transferable.getStatus() + != Transferable.STATUS_UPLOADING))) { return message; } } @@ -434,7 +481,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (uuids.contains(message.getUuid())) { message.setDeleted(true); deleted = true; - if (message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && pgpDecryptionService != null) { pgpDecryptionService.discard(message); } } @@ -452,7 +500,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl if (file.uuid.toString().equals(message.getUuid())) { message.setDeleted(file.deleted); changed = true; - if (file.deleted && message.getEncryption() == Message.ENCRYPTION_PGP && pgpDecryptionService != null) { + if (file.deleted + && message.getEncryption() == Message.ENCRYPTION_PGP + && pgpDecryptionService != null) { pgpDecryptionService.discard(message); } } @@ -480,7 +530,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public boolean setOutgoingChatState(ChatState state) { - if (mode == MODE_SINGLE && !getContact().isSelf() || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { + if (mode == MODE_SINGLE && !getContact().isSelf() + || (isPrivateAndNonAnonymous() && getNextCounterpart() == null)) { if (this.mOutgoingChatState != state) { this.mOutgoingChatState = state; return true; @@ -513,7 +564,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl final ArrayList results = new ArrayList<>(); synchronized (this.messages) { for (Message message : this.messages) { - if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) { + if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) + && message.getStatus() == Message.STATUS_UNSEND) { results.add(message); } } @@ -528,7 +580,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl for (Message message : this.messages) { if (id.equals(message.getUuid()) || (message.getStatus() >= Message.STATUS_SEND - && id.equals(message.getRemoteMsgId()))) { + && id.equals(message.getRemoteMsgId()))) { return message; } } @@ -556,7 +608,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl continue; } if (counterpart == null || mcp.equals(counterpart) || mcp.asBareJid().equals(counterpart)) { - final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId())); + final boolean idMatch = id.equals(message.getUuid()) || id.equals(message.getRemoteMsgId()) || (getMode() == MODE_MULTI && id.equals(message.getServerMsgId())); if (idMatch) return message; } } @@ -590,7 +642,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public Message findReceivedWithRemoteId(final String id) { synchronized (this.messages) { for (final Message message : this.messages) { - if (message.getStatus() == Message.STATUS_RECEIVED && id.equals(message.getRemoteMsgId())) { + if (message.getStatus() == Message.STATUS_RECEIVED + && id.equals(message.getRemoteMsgId())) { return message; } } @@ -861,7 +914,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public boolean setCorrectingMessage(Message correctingMessage) { - setAttribute(ATTRIBUTE_CORRECTING_MESSAGE, correctingMessage == null ? null : correctingMessage.getUuid()); + setAttribute( + ATTRIBUTE_CORRECTING_MESSAGE, + correctingMessage == null ? null : correctingMessage.getUuid()); return correctingMessage == null && draftMessage != null; } @@ -877,7 +932,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl @Override public int compareTo(@NonNull Conversation another) { return ComparisonChain.start() - .compareFalseFirst(another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false)) + .compareFalseFirst( + another.getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false), + getBooleanAttribute(ATTRIBUTE_PINNED_ON_TOP, false)) .compare(another.getSortableTime(), getSortableTime()) .result(); } @@ -979,8 +1036,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return message; } - public @NonNull - CharSequence getName() { + public @NonNull CharSequence getName() { if (getMode() == MODE_MULTI) { final String roomName = getMucOptions().getName(); final String subject = getMucOptions().getSubject(); @@ -1000,6 +1056,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return contactJid.getLocal() != null ? contactJid.getLocal() : contactJid; } } + } else if ((QuickConversationsService.isConversations() + || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) + && isWithStranger()) { + return contactJid; } else { return this.getContact().getDisplayName(); } @@ -1071,9 +1131,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl this.mode = mode; } - /** - * short for is Private and Non-anonymous - */ + /** short for is Private and Non-anonymous */ public boolean isSingleOrPrivateAndNonAnonymous() { return mode == MODE_SINGLE || isPrivateAndNonAnonymous(); } @@ -1110,7 +1168,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return Message.ENCRYPTION_NONE; } if (OmemoSetting.isAlways()) { - return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE; + return suitableForOmemoByDefault(this) + ? Message.ENCRYPTION_AXOLOTL + : Message.ENCRYPTION_NONE; } final int defaultEncryption; if (suitableForOmemoByDefault(this)) { @@ -1135,8 +1195,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return nextMessage == null ? "" : nextMessage; } - public @Nullable - Draft getDraft() { + public @Nullable Draft getDraft() { long timestamp = getLongAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, 0); final long messageTime; synchronized (this.messages) { @@ -1160,7 +1219,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl boolean changed = !getNextMessage().equals(message); this.setAttribute(ATTRIBUTE_NEXT_MESSAGE, message); if (changed) { - this.setAttribute(ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, message == null ? 0 : System.currentTimeMillis()); + this.setAttribute( + ATTRIBUTE_NEXT_MESSAGE_TIMESTAMP, + message == null ? 0 : System.currentTimeMillis()); } return changed; } @@ -1188,7 +1249,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (int i = this.messages.size() - 1; i >= 0; --i) { Message message = this.messages.get(i); - if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { + if (message.getStatus() == Message.STATUS_UNSEND + || message.getStatus() == Message.STATUS_SEND) { String otherBody; if (message.hasFileOnRemoteHost()) { otherBody = message.getFileParams().url; @@ -1208,7 +1270,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl synchronized (this.messages) { for (int i = this.messages.size() - 1; i >= 0; --i) { final Message message = this.messages.get(i); - if ((message.getStatus() == s) && (message.getType() == Message.TYPE_RTP_SESSION) && sessionId.equals(message.getRemoteMsgId())) { + if ((message.getStatus() == s) + && (message.getType() == Message.TYPE_RTP_SESSION) + && sessionId.equals(message.getRemoteMsgId())) { return message; } } @@ -1222,7 +1286,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } synchronized (this.messages) { for (Message message : this.messages) { - if (serverMsgId.equals(message.getServerMsgId()) || remoteMsgId.equals(message.getRemoteMsgId())) { + if (serverMsgId.equals(message.getServerMsgId()) + || remoteMsgId.equals(message.getRemoteMsgId())) { return true; } } @@ -1237,10 +1302,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl for (int i = this.messages.size() - 1; i >= 0; --i) { final Message message = this.messages.get(i); if (message.isPrivateMessage()) { - continue; //it's unsafe to use private messages as anchor. They could be coming from user archive - } - if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon() || message.getServerMsgId() != null) { - lastReceived = new MamReference(message.getTimeSent(), message.getServerMsgId()); + continue; // it's unsafe to use private messages as anchor. They could be coming + // from user archive + } + if (message.getStatus() == Message.STATUS_RECEIVED + || message.isCarbon() + || message.getServerMsgId() != null) { + lastReceived = + new MamReference(message.getTimeSent(), message.getServerMsgId()); break; } } @@ -1257,7 +1326,10 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl } public boolean alwaysNotify() { - return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); + return mode == MODE_SINGLE + || getBooleanAttribute( + ATTRIBUTE_ALWAYS_NOTIFY, + Config.ALWAYS_NOTIFY_BY_DEFAULT || isPrivateAndNonAnonymous()); } public boolean notifyReplies() { @@ -1338,11 +1410,11 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl try { list.add(Jid.of(array.getString(i))); } catch (IllegalArgumentException e) { - //ignored + // ignored } } } catch (JSONException e) { - //ignored + // ignored } } return list; @@ -1440,7 +1512,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void expireOldMessages(long timestamp) { synchronized (this.messages) { - for (ListIterator iterator = this.messages.listIterator(); iterator.hasNext(); ) { + for (ListIterator iterator = this.messages.listIterator(); + iterator.hasNext(); ) { if (iterator.next().getTimeSent() < timestamp) { iterator.remove(); } @@ -1451,15 +1524,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public void sort() { synchronized (this.messages) { - Collections.sort(this.messages, (left, right) -> { - if (left.getTimeSent() < right.getTimeSent()) { - return -1; - } else if (left.getTimeSent() > right.getTimeSent()) { - return 1; - } else { - return 0; - } - }); + Collections.sort( + this.messages, + (left, right) -> { + if (left.getTimeSent() < right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() > right.getTimeSent()) { + return 1; + } else { + return 0; + } + }); untieMessages(); } } @@ -1473,8 +1548,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public int unreadCount(XmppConnectionService xmppConnectionService) { synchronized (this.messages) { int count = 0; - for (int i = messages.size() - 1; i >= 0; --i) { - final Message message = messages.get(i); + for (final Message message : Lists.reverse(this.messages)) { if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue; if (asReaction(message) != null) continue; if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue; diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 8eab5831ba29479ccad30acc66934292a9ca8b69..31db0cb06d55643d755cf31283d72680fc19a9bb 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -183,7 +183,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Message(Conversational conversation, String body, int encryption, int status) { - this(conversation, java.util.UUID.randomUUID().toString(), + this( + conversation, + java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().asBareJid(), null, @@ -214,7 +216,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Message(Conversation conversation, int status, int type, final String remoteMsgId) { - this(conversation, java.util.UUID.randomUUID().toString(), + this( + conversation, + java.util.UUID.randomUUID().toString(), conversation.getUuid(), conversation.getJid() == null ? null : conversation.getJid().asBareJid(), null, @@ -244,13 +248,33 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable null); } - protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final boolean carbon, - final String remoteMsgId, final String relativeFilePath, - final String serverMsgId, final String fingerprint, final boolean read, - final String edited, final boolean oob, final String errorMessage, final Set readByMarkers, - final boolean markable, final boolean deleted, final String bodyLanguage, final String occupantId, final Collection reactions, final long timeReceived, final String subject, final String fileParams, final List payloads) { + protected Message( + final Conversational conversation, + final String uuid, + final String conversationUUid, + final Jid counterpart, + final Jid trueCounterpart, + final String body, + final long timeSent, + final int encryption, + final int status, + final int type, + final boolean carbon, + final String remoteMsgId, + final String relativeFilePath, + final String serverMsgId, + final String fingerprint, + final boolean read, + final String edited, + final boolean oob, + final String errorMessage, + final Set readByMarkers, + final boolean markable, + final boolean deleted, + final String bodyLanguage, + final String occupantId, + final Collection reactions, + final long timeReceived, final String subject, final String fileParams, final List payloads) { this.conversation = conversation; this.uuid = uuid; this.conversationUuid = conversationUUid; @@ -396,7 +420,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } else { values.put(TRUE_COUNTERPART, trueCounterpart.toString()); } - values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body); + values.put( + BODY, + body.length() > Config.MAX_STORAGE_MESSAGE_CHARS + ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) + : body); values.put(TIME_SENT, timeSent); values.put(ENCRYPTION, encryption); values.put(STATUS, status); @@ -538,7 +566,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable if (this.trueCounterpart == null) { return null; } else { - return this.conversation.getAccount().getRoster() + return this.conversation + .getAccount() + .getRoster() .getContactFromContactList(this.trueCounterpart); } } @@ -716,8 +746,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean setErrorMessage(String message) { - boolean changed = (message != null && !message.equals(errorMessage)) - || (message == null && errorMessage != null); + boolean changed = + (message != null && !message.equals(errorMessage)) + || (message == null && errorMessage != null); this.errorMessage = message; return changed; } @@ -849,15 +880,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } } - boolean remoteMsgIdMatchInEdit(String id) { - for (Edit edit : this.edits) { - if (id.equals(edit.getEditedId())) { - return true; - } - } - return false; - } - public String getBodyLanguage() { return this.bodyLanguage; } @@ -867,7 +889,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public boolean edited() { - return this.edits.size() > 0; + return !this.edits.isEmpty(); } public void setTrueCounterpart(Jid trueCounterpart) { @@ -901,7 +923,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable Iterator iterator = this.readByMarkers.iterator(); while (iterator.hasNext()) { ReadByMarker marker = iterator.next(); - if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) { + if (marker.getRealJid() == null + && readByMarker.getFullJid().equals(marker.getFullJid())) { iterator.remove(); } } @@ -933,7 +956,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable boolean similar(Message message) { if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) { - return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); + return this.serverMsgId.equals(message.getServerMsgId()) + || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId()); } else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) { return true; } else if (this.body == null || this.counterpart == null) { @@ -949,32 +973,36 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart()); if (message.getRemoteMsgId() != null) { - final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); - if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { + final boolean hasUuid = + CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches(); + if (hasUuid + && matchingCounterpart + && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) { return true; } - return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) + return (message.getRemoteMsgId().equals(this.remoteMsgId) + || message.getRemoteMsgId().equals(this.uuid)) && matchingCounterpart - && (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); + && (body.equals(otherBody) + || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid)); } else { return this.remoteMsgId == null && matchingCounterpart && body.equals(otherBody) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; + && Math.abs(this.getTimeSent() - message.getTimeSent()) < 20_000; } } } public Message next() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { + if (this.conversation instanceof Conversation c) { + synchronized (c.messages) { if (this.mNextMessage == null) { - int index = conversation.messages.indexOf(this); - if (index < 0 || index >= conversation.messages.size() - 1) { + int index = c.messages.indexOf(this); + if (index < 0 || index >= c.messages.size() - 1) { this.mNextMessage = null; } else { - this.mNextMessage = conversation.messages.get(index + 1); + this.mNextMessage = c.messages.get(index + 1); } } return this.mNextMessage; @@ -985,15 +1013,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public Message prev() { - if (this.conversation instanceof Conversation) { - final Conversation conversation = (Conversation) this.conversation; - synchronized (conversation.messages) { + if (this.conversation instanceof Conversation c) { + synchronized (c.messages) { if (this.mPreviousMessage == null) { - int index = conversation.messages.indexOf(this); - if (index <= 0 || index > conversation.messages.size()) { + int index = c.messages.indexOf(this); + if (index <= 0 || index > c.messages.size()) { this.mPreviousMessage = null; } else { - this.mPreviousMessage = conversation.messages.get(index - 1); + this.mPreviousMessage = c.messages.get(index - 1); } } } @@ -1018,26 +1045,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return status != STATUS_RECEIVED && !isCarbon() && type != Message.TYPE_RTP_SESSION; } - public boolean mergeable(final Message message) { - return false; // Merging messages messes up reply, so disable for now - } - - private static boolean isStatusMergeable(int a, int b) { - return a == b || ( - (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) - || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING) - || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) - || (a == Message.STATUS_SEND && b == Message.STATUS_WAITING) - ); - } - - private static boolean isEncryptionMergeable(final int a, final int b) { - return a == b - && Arrays.asList(ENCRYPTION_NONE, ENCRYPTION_DECRYPTED, ENCRYPTION_AXOLOTL) - .contains(a); - } - public void setCounterparts(List counterparts) { this.counterparts = counterparts; } @@ -1048,7 +1055,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable @Override public int getAvatarBackgroundColor() { - if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) { + if (type == Message.TYPE_STATUS + && getCounterparts() != null + && getCounterparts().size() > 1) { return Color.TRANSPARENT; } else { return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this)); @@ -1099,9 +1108,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable this.reactions = reactions; } - public static class MergeSeparator { - } - public SpannableStringBuilder getSpannableBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { return getSpannableBody(thumbnailer, fallbackImg, true); } @@ -1182,56 +1188,14 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return spannableBody; } - public SpannableStringBuilder getMergedBody() { - return getMergedBody(null, null); - } - - public SpannableStringBuilder getMergedBody(GetThumbnailForCid thumbnailer, Drawable fallbackImg) { - SpannableStringBuilder body = getSpannableBody(thumbnailer, fallbackImg); - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null || current.getModerated() != null) { - break; - } - body.append("\n\n"); - body.setSpan(new MergeSeparator(), body.length() - 2, body.length(), - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - body.append(current.getSpannableBody(thumbnailer, fallbackImg)); - } - return body; + public SpannableStringBuilder getSpannableBody() { + return getSpannableBody(null, null); } public boolean hasMeCommand() { return this.body.trim().startsWith(ME_COMMAND); } - public int getMergedStatus() { - int status = this.status; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - status = current.status; - } - return status; - } - - public long getMergedTimeSent() { - long time = this.timeSent; - Message current = this; - while (current.mergeable(current.next())) { - current = current.next(); - if (current == null) { - break; - } - time = current.timeSent; - } - return time; - } - public boolean wasMergedIntoPrevious(XmppConnectionService xmppConnectionService) { Message prev = this.prev(); if (prev != null && getModerated() != null && prev.getModerated() != null) return true; @@ -1239,24 +1203,27 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable final boolean muted = getStatus() == Message.STATUS_RECEIVED && conversation.getMode() == Conversation.MODE_MULTI && xmppConnectionService.isMucUserMuted(new MucOptions.User(null, conversation.getJid(), getOccupantId(), null, null)); if (prev != null && muted && getOccupantId().equals(prev.getOccupantId())) return true; } - return prev != null && prev.mergeable(this); + return false; } public boolean trusted() { - Contact contact = this.getContact(); - return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf())); + final var contact = this.getContact(); + return status > STATUS_RECEIVED + || (contact != null && (contact.showInContactList() || contact.isSelf())); } public boolean fixCounterpart() { final Presences presences = conversation.getContact().getPresences(); if (counterpart != null && presences.has(Strings.nullToEmpty(counterpart.getResource()))) { return true; - } else if (presences.size() >= 1) { - counterpart = PresenceSelector.getNextCounterpart(getContact(), presences.toResourceArray()[0]); - return true; - } else { + } else if (presences.isEmpty()) { counterpart = null; return false; + } else { + counterpart = + PresenceSelector.getNextCounterpart( + getContact(), presences.toResourceArray()[0]); + return true; } } @@ -1265,19 +1232,17 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } public String getEditedId() { - if (edits.size() > 0) { - return edits.get(edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); + if (this.edits.isEmpty()) { + throw new IllegalStateException("Attempting to access unedited message"); } + return edits.get(edits.size() - 1).getEditedId(); } public String getEditedIdWireFormat() { - if (edits.size() > 0) { - return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId(); - } else { - throw new IllegalStateException("Attempting to store unedited message"); + if (this.edits.isEmpty()) { + throw new IllegalStateException("Attempting to access unedited message"); } + return edits.get(0).getEditedId(); } public List getLinks() { @@ -1509,7 +1474,6 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE; } - public boolean isTypeText() { return type == TYPE_TEXT || type == TYPE_PRIVATE; } @@ -1793,7 +1757,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public boolean isTrusted() { final AxolotlService axolotlService = conversation.getAccount().getAxolotlService(); - final FingerprintStatus s = axolotlService != null ? axolotlService.getFingerprintTrust(axolotlFingerprint) : null; + final FingerprintStatus s = + axolotlService != null + ? axolotlService.getFingerprintTrust(axolotlFingerprint) + : null; return s != null && s.isTrusted(); } @@ -1808,17 +1775,18 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable } private int getNextEncryption() { - if (this.conversation instanceof Conversation) { - Conversation conversation = (Conversation) this.conversation; + if (this.conversation instanceof Conversation c) { for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) { if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) { continue; } return iterator.getEncryption(); } - return conversation.getNextEncryption(); + return c.getNextEncryption(); } else { - throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs"); + throw new AssertionError( + "This should never be called since isInValidSession should be disabled for" + + " stubs"); } } @@ -1826,9 +1794,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); int futureEncryption = getCleanedEncryption(this.getNextEncryption()); - boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE - || futureEncryption == ENCRYPTION_NONE - || pastEncryption != futureEncryption; + boolean inUnencryptedSession = + pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; } @@ -1837,7 +1806,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { return ENCRYPTION_PGP; } - if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) { + if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + || encryption == ENCRYPTION_AXOLOTL_FAILED) { return ENCRYPTION_AXOLOTL; } return encryption; @@ -1875,7 +1845,11 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable return configurePrivateMessage(conversation, message, counterpart, false); } - private static boolean configurePrivateMessage(final Conversation conversation, final Message message, final Jid counterpart, final boolean isFile) { + private static boolean configurePrivateMessage( + final Conversation conversation, + final Message message, + final Jid counterpart, + final boolean isFile) { if (counterpart == null) { return false; } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 19d0658320d13e061629d32365a34f37a1114139..24b0d0e7642ab42adb356204cefc60dc4ba7ee2e 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -7,15 +7,12 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - import io.ipfs.cid.Cid; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.services.AvatarService; @@ -32,6 +29,7 @@ import eu.siacs.conversations.xml.Element; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -60,9 +58,7 @@ public class MucOptions { private User self; private String password = null; - private boolean tookProposedNickFromBookmark = false; - - public MucOptions(Conversation conversation) { + public MucOptions(final Conversation conversation) { this.account = conversation.getAccount(); this.conversation = conversation; final String nick = getProposedNick(conversation.getAttribute("mucNick")); @@ -104,10 +100,14 @@ public class MucOptions { return mAutoPushConfiguration; } - public boolean isSelf(Jid counterpart) { + public boolean isSelf(final Jid counterpart) { return counterpart.equals(self.getFullJid()); } + public boolean isSelf(final String occupantId) { + return occupantId.equals(self.getOccupantId()); + } + public void resetChatState() { synchronized (users) { for (User user : users) { @@ -116,17 +116,6 @@ public class MucOptions { } } - public boolean isTookProposedNickFromBookmark() { - return tookProposedNickFromBookmark; - } - - void notifyOfBookmarkNick(final String nick) { - final String normalized = normalize(account.getJid(),nick); - if (normalized != null && normalized.equals(getSelf().getNick())) { - this.tookProposedNickFromBookmark = true; - } - } - public boolean mamSupport() { return MessageArchiveService.Version.has(getFeatures()); } @@ -138,8 +127,8 @@ public class MucOptions { if (roomConfigName != null) { name = roomConfigName.getValue(); } else { - List identities = serviceDiscoveryResult.getIdentities(); - String identityName = identities.size() > 0 ? identities.get(0).getName() : null; + final var identities = serviceDiscoveryResult.getIdentities(); + final String identityName = !identities.isEmpty() ? identities.get(0).getName() : null; final Jid jid = conversation.getJid(); if (identityName != null && !identityName.equals(jid == null ? null : jid.getEscapedLocal())) { name = identityName; @@ -156,7 +145,7 @@ public class MucOptions { private Data getRoomInfoForm() { final List forms = serviceDiscoveryResult == null ? Collections.emptyList() : serviceDiscoveryResult.forms; - return forms.size() == 0 ? new Data() : forms.get(0); + return forms.isEmpty() ? new Data() : forms.get(0); } public String getAvatar() { @@ -360,29 +349,24 @@ public class MucOptions { return null; } - public User findUserByOccupantId(final String id, final Jid counterpart) { - if (id == null) { - return null; - } - synchronized (users) { - for (User user : users) { - if (id.equals(user.getOccupantId())) { - return user; - } - } + public User findUserByOccupantId(final String occupantId, final Jid counterpart) { + synchronized (this.users) { + final var found = Strings.isNullOrEmpty(occupantId) ? null : Iterables.find(this.users, u -> occupantId.equals(u.occupantId),null); + if (Strings.isNullOrEmpty(occupantId) || found != null) return found; + final var user = new User(this, counterpart, occupantId, null, new HashSet<>()); + user.setOnline(false); + return user; } - final var user = new User(this, counterpart, id, null, new HashSet<>()); - user.setOnline(false); - return user; } public User findOrCreateUserByRealJid(Jid jid, Jid fullJid, final String occupantId) { - User user = findUserByRealJid(jid); - if (user == null) { - user = new User(this, fullJid, occupantId, null, new HashSet<>()); - user.setRealJid(jid); - user.setOnline(false); + final User existing = findUserByRealJid(jid); + if (existing != null) { + return existing; } + final var user = new User(this, fullJid, occupantId, null, new HashSet<>()); + user.setRealJid(jid); + user.setOnline(false); return user; } @@ -396,6 +380,31 @@ public class MucOptions { } } + private User findUser(final Reaction reaction) { + if (reaction.trueJid != null) { + return findOrCreateUserByRealJid(reaction.trueJid.asBareJid(), reaction.from, reaction.occupantId); + } + final var existing = findUserByOccupantId(reaction.occupantId, reaction.from); + if (existing != null) { + return existing; + } else if (reaction.from != null) { + return new User(this,reaction.from,reaction.occupantId,null,new HashSet<>()); + } else { + return null; + } + } + + public List findUsers(final Collection reactions) { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(final Reaction reaction : reactions) { + final var user = findUser(reaction); + if (user != null) { + builder.add(user); + } + } + return builder.build(); + } + public boolean isContactInRoom(Contact contact) { return contact != null && isUserInRoom(findUserByRealJid(contact.getJid().asBareJid())); } @@ -499,20 +508,31 @@ public class MucOptions { } } - public String getProposedNick() { + private String getProposedNick() { return getProposedNick(null); } - public String getProposedNick(final String mucNick) { + private String getProposedNick(final String mucNick) { + final Bookmark bookmark = this.conversation.getBookmark(); + if (bookmark != null) { + // if we already have a bookmark we consider this the source of truth + return getProposedNickPure(); + } + final var storedJid = conversation.getJid(); + if (mucNick != null) { + return mucNick; + } else if (storedJid.isBareJid()) { + return defaultNick(account); + } else { + return storedJid.getResource(); + } + } + + public String getProposedNickPure() { final Bookmark bookmark = this.conversation.getBookmark(); final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick()); if (bookmarkedNick != null) { - this.tookProposedNickFromBookmark = true; return bookmarkedNick; - } else if (mucNick != null) { - return mucNick; - } else if (!conversation.getJid().isBareJid()) { - return conversation.getJid().getResource(); } else { return defaultNick(account); } @@ -527,15 +547,15 @@ public class MucOptions { } } - private static String normalize(Jid account, String nick) { - if (account == null || TextUtils.isEmpty(nick)) { + private static String normalize(final Jid account, final String nick) { + if (account == null || Strings.isNullOrEmpty(nick)) { return null; } try { return account.withResource(nick).getResource(); - } catch (IllegalArgumentException e) { - return nick; + } catch (final IllegalArgumentException e) { + return null; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Reaction.java b/src/main/java/eu/siacs/conversations/entities/Reaction.java index 734fe46803ed7b16317fab19a5135a22295f4f82..ce8ac5044163cdf8c842b723b95d8e7b31fa651c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Reaction.java +++ b/src/main/java/eu/siacs/conversations/entities/Reaction.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.entities; +import android.util.Log; + import androidx.annotation.NonNull; import com.cheogram.android.EmojiSearch; @@ -13,21 +15,22 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import com.google.common.collect.Ordering; -import com.google.gson.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; import io.ipfs.cid.Cid; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.xmpp.Jid; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -79,6 +82,10 @@ public class Reaction { this.envelopeId = envelopeId; } + public String normalizedReaction() { + return Emoticons.normalizeToVS16(this.reaction); + } + public static String toString(final Collection reactions) { return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions); } @@ -88,8 +95,9 @@ public class Reaction { return Collections.emptyList(); } try { - return GSON.fromJson(asString, new TypeToken>() {}.getType()); - } catch (final JsonSyntaxException e) { + return GSON.fromJson(asString, new TypeToken>() {}.getType()); + } catch (final IllegalArgumentException | JsonSyntaxException e) { + Log.e(Config.LOGTAG, "could not restore reactions", e); return Collections.emptyList(); } } @@ -228,7 +236,7 @@ public class Reaction { ImmutableSet.copyOf( Collections2.transform( Collections2.filter(reactions, r -> r.cid == null && !r.received), - r -> r.reaction))); + Reaction::normalizedReaction))); } public static final class Aggregated { diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index e591fa4ed23bd7a9bb930d7c0ca32104f9ff9c37..053361db62c0f5f2fabe102b3319beae746a4496 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -1,12 +1,5 @@ package eu.siacs.conversations.generator; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; @@ -24,21 +17,34 @@ 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 im.conversations.android.xmpp.model.correction.Replace; import im.conversations.android.xmpp.model.reactions.Reaction; import im.conversations.android.xmpp.model.reactions.Reactions; +import im.conversations.android.xmpp.model.unique.OriginId; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; 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"; - private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that."; + 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"; + private static final String PGP_FALLBACK_MESSAGE = + "I sent you a PGP encrypted message but your client doesn’t seem to support that."; public MessageGenerator(XmppConnectionService service) { super(service); } - private im.conversations.android.xmpp.model.stanza.Message preparePacket(Message message, boolean legacyEncryption) { + private im.conversations.android.xmpp.model.stanza.Message preparePacket( + final Message message, final boolean legacyEncryption) { Conversation conversation = (Conversation) message.getConversation(); Account account = conversation.getAccount(); - im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + 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()); @@ -60,11 +66,13 @@ public class MessageGenerator extends AbstractGenerator { } packet.setFrom(account.getJid()); packet.setId(message.getUuid()); - if (conversation.getMode() == Conversational.MODE_SINGLE || message.isPrivateMessage() || !conversation.getMucOptions().stableId()) { - packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid()); + if (conversation.getMode() == Conversational.MODE_MULTI + && !message.isPrivateMessage() + && !conversation.getMucOptions().stableId()) { + packet.addExtension(new OriginId(message.getUuid())); } if (message.edited()) { - packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat()); + packet.addExtension(new Replace(message.getEditedIdWireFormat())); } if (!legacyEncryption) { if (message.getSubject() != null && message.getSubject().length() > 0) packet.addChild("subject").setContent(message.getSubject()); @@ -80,16 +88,18 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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); + 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")); Element delay = packet.addChild("delay", "urn:xmpp:delay"); Date date = new Date(timestamp); delay.setAttribute("stamp", mDateFormat.format(date)); } - public im.conversations.android.xmpp.model.stanza.Message generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) { + 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; @@ -103,8 +113,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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(); + 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()); @@ -166,23 +178,32 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public im.conversations.android.xmpp.model.stanza.Message generateChatState(Conversation conversation) { + public im.conversations.android.xmpp.model.stanza.Message generateChatState( + Conversation conversation) { final Account account = conversation.getAccount(); - 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); + 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())); packet.addChild("no-store", "urn:xmpp:hints"); - packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store* + packet.addChild("no-storage", "urn:xmpp:hints"); // wrong! don't copy this. Its *store* return packet; } 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 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); + 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) { @@ -200,15 +221,22 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public im.conversations.android.xmpp.model.stanza.Message reaction(final Conversational conversation, final Message inReplyTo, final String reactingTo, final Collection ourReactions) { - final boolean groupChat = conversation.getMode() == Conversational.MODE_MULTI; - final Jid to = conversation.getJid().asBareJid(); - 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); + public im.conversations.android.xmpp.model.stanza.Message reaction( + final Jid to, + final boolean groupChat, + final Message inReplyTo, + final String reactingTo, + final Collection ourReactions) { + 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(to); final var reactions = packet.addExtension(new Reactions()); reactions.setId(reactingTo); - for(final String ourReaction : ourReactions) { + for (final String ourReaction : ourReactions) { reactions.addExtension(new Reaction(ourReaction)); } @@ -219,8 +247,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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(); + 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); @@ -240,8 +270,10 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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(); + 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()); @@ -258,7 +290,8 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public im.conversations.android.xmpp.model.stanza.Message invite(final Conversation conversation, final Jid contact) { + 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()); @@ -271,9 +304,13 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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(); + 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()); @@ -284,8 +321,10 @@ public class MessageGenerator extends AbstractGenerator { return receivedPacket; } - 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(); + 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); @@ -295,7 +334,8 @@ public class MessageGenerator extends AbstractGenerator { public im.conversations.android.xmpp.model.stanza.Message sessionFinish( final Jid with, final String sessionId, final Reason reason) { - final im.conversations.android.xmpp.model.stanza.Message packet = new im.conversations.android.xmpp.model.stanza.Message(); + 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); @@ -306,24 +346,33 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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 + 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); propose.setAttribute("id", proposal.sessionId); for (final Media media : proposal.media) { - propose.addChild("description", Namespace.JINGLE_APPS_RTP).setAttribute("media", media.toString()); + propose.addChild("description", Namespace.JINGLE_APPS_RTP) + .setAttribute("media", media.toString()); } packet.addChild("request", "urn:xmpp:receipts"); packet.addChild("store", "urn:xmpp:hints"); return packet; } - 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 + 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); @@ -332,9 +381,13 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - 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 + 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/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 0fae0f00c9d1e1cb5a6399a367227eedf03f28be..c0dca4c3d59c1bb08f738ce53487bc43be8694c5 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -7,6 +7,7 @@ import android.util.Pair; import com.cheogram.android.BobTransfer; import com.cheogram.android.WebxdcUpdate; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import java.io.File; @@ -68,6 +69,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar; 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.correction.Replace; import im.conversations.android.xmpp.model.forward.Forwarded; import im.conversations.android.xmpp.model.occupant.OccupantId; import im.conversations.android.xmpp.model.reactions.Reactions; @@ -351,12 +353,13 @@ public class MessageParser extends AbstractParser implements Consumer attachments = new LinkedHashSet<>(); for (Element child : packet.getChildren()) { @@ -505,6 +510,7 @@ public class MessageParser extends AbstractParser implements Consumer= 2) { - db.execSQL("update " + Account.TABLENAME + " set " - + Account.OPTIONS + " = " + Account.OPTIONS + " | 8"); + db.execSQL( + "update " + + Account.TABLENAME + + " set " + + Account.OPTIONS + + " = " + + Account.OPTIONS + + " | 8"); } if (oldVersion < 3 && newVersion >= 3) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.TYPE + " NUMBER"); + db.execSQL( + "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.TYPE + " NUMBER"); } if (oldVersion < 5 && newVersion >= 5) { db.execSQL("DROP TABLE " + Contact.TABLENAME); db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL("UPDATE " + Account.TABLENAME + " SET " - + Account.ROSTERVERSION + " = NULL"); + db.execSQL("UPDATE " + Account.TABLENAME + " SET " + Account.ROSTERVERSION + " = NULL"); } if (oldVersion < 6 && newVersion >= 6) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.TRUE_COUNTERPART + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.TRUE_COUNTERPART + + " TEXT"); } if (oldVersion < 7 && newVersion >= 7) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.REMOTE_MSG_ID + " TEXT"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.AVATAR + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " - + Account.AVATAR + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.REMOTE_MSG_ID + + " TEXT"); + db.execSQL( + "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.AVATAR + " TEXT"); + db.execSQL( + "ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.AVATAR + " TEXT"); } if (oldVersion < 8 && newVersion >= 8) { - db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " - + Conversation.ATTRIBUTES + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Conversation.TABLENAME + + " ADD COLUMN " + + Conversation.ATTRIBUTES + + " TEXT"); } if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Contact.TABLENAME + + " ADD COLUMN " + + Contact.LAST_TIME + + " NUMBER"); + db.execSQL( + "ALTER TABLE " + + Contact.TABLENAME + + " ADD COLUMN " + + Contact.LAST_PRESENCE + + " TEXT"); } if (oldVersion < 10 && newVersion >= 10) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.RELATIVE_FILE_PATH + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.RELATIVE_FILE_PATH + + " TEXT"); } if (oldVersion < 11 && newVersion >= 11) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.GROUPS + " TEXT"); + db.execSQL( + "ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.GROUPS + " TEXT"); db.execSQL("delete from " + Contact.TABLENAME); db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL"); } if (oldVersion < 12 && newVersion >= 12) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.SERVER_MSG_ID + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.SERVER_MSG_ID + + " TEXT"); } if (oldVersion < 13 && newVersion >= 13) { db.execSQL("delete from " + Contact.TABLENAME); @@ -518,26 +800,60 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 15 && newVersion >= 15) { recreateAxolotlDb(db); - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.FINGERPRINT + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.FINGERPRINT + + " TEXT"); } if (oldVersion < 16 && newVersion >= 16) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " - + Message.CARBON + " INTEGER"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.CARBON + + " INTEGER"); } if (oldVersion < 19 && newVersion >= 19) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.DISPLAY_NAME + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.DISPLAY_NAME + + " TEXT"); } if (oldVersion < 20 && newVersion >= 20) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.HOSTNAME + + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.PORT + + " NUMBER DEFAULT 5222"); } if (oldVersion < 26 && newVersion >= 26) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT"); + db.execSQL( + "ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.STATUS_MESSAGE + + " TEXT"); } if (oldVersion < 40 && newVersion >= 40) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.RESOURCE + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.RESOURCE + + " TEXT"); } /* Any migrations that alter the Account table need to happen BEFORE this migration, as it * depends on account de-serialization. @@ -545,45 +861,67 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 17 && newVersion >= 17 && newVersion < 31) { List accounts = getAccounts(db); for (Account account : accounts) { - String ownDeviceIdString = account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID); + String ownDeviceIdString = + account.getKey(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID); if (ownDeviceIdString == null) { continue; } int ownDeviceId = Integer.valueOf(ownDeviceIdString); - SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), ownDeviceId); + SignalProtocolAddress ownAddress = + new SignalProtocolAddress( + account.getJid().asBareJid().toString(), ownDeviceId); deleteSession(db, account, ownAddress); IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account); if (identityKeyPair != null) { String[] selectionArgs = { - account.getUuid(), - CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()) + account.getUuid(), + CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()) }; ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.TRUSTED, 2); - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + db.update( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + values, + SQLiteAxolotlStore.ACCOUNT + + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + + " = ? ", selectionArgs); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not load own identity key pair"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not load own identity key pair"); } } } if (oldVersion < 18 && newVersion >= 18) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ + " NUMBER DEFAULT 1"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.READ + + " NUMBER DEFAULT 1"); } if (oldVersion < 21 && newVersion >= 21) { List accounts = getAccounts(db); for (Account account : accounts) { account.unsetPgpSignature(); - db.update(Account.TABLENAME, account.getContentValues(), Account.UUID - + "=?", new String[]{account.getUuid()}); + db.update( + Account.TABLENAME, + account.getContentValues(), + Account.UUID + "=?", + new String[] {account.getUuid()}); } } if (oldVersion >= 15 && oldVersion < 22 && newVersion >= 22) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); + db.execSQL( + "ALTER TABLE " + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + + " ADD COLUMN " + + SQLiteAxolotlStore.CERTIFICATE); } if (oldVersion < 23 && newVersion >= 23) { @@ -591,11 +929,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 24 && newVersion >= 24) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); + db.execSQL( + "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); } if (oldVersion < 25 && newVersion >= 25) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER"); + db.execSQL( + "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER"); } if (oldVersion < 26 && newVersion >= 26) { @@ -611,51 +951,116 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 29 && newVersion >= 29) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.ERROR_MESSAGE + + " TEXT"); } if (oldVersion >= 15 && oldVersion < 31 && newVersion >= 31) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.TRUST + " TEXT"); - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.ACTIVE + " NUMBER"); + db.execSQL( + "ALTER TABLE " + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + + " ADD COLUMN " + + SQLiteAxolotlStore.TRUST + + " TEXT"); + db.execSQL( + "ALTER TABLE " + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + + " ADD COLUMN " + + SQLiteAxolotlStore.ACTIVE + + " NUMBER"); HashMap migration = new HashMap<>(); - migration.put(0, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); - migration.put(1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); - migration.put(2, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true)); - migration.put(3, createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false)); - migration.put(4, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); - migration.put(5, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); - migration.put(6, createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false)); - migration.put(7, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true)); - migration.put(8, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false)); + migration.put( + 0, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); + migration.put( + 1, createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); + migration.put( + 2, + createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true)); + migration.put( + 3, + createFingerprintStatusContentValues( + FingerprintStatus.Trust.COMPROMISED, false)); + migration.put( + 4, + createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); + migration.put( + 5, + createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); + migration.put( + 6, + createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false)); + migration.put( + 7, + createFingerprintStatusContentValues( + FingerprintStatus.Trust.VERIFIED_X509, true)); + migration.put( + 8, + createFingerprintStatusContentValues( + FingerprintStatus.Trust.VERIFIED_X509, false)); for (Map.Entry entry : migration.entrySet()) { String whereClause = SQLiteAxolotlStore.TRUSTED + "=?"; String[] where = {String.valueOf(entry.getKey())}; - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, entry.getValue(), whereClause, where); + db.update( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + entry.getValue(), + whereClause, + where); } - } if (oldVersion >= 15 && oldVersion < 32 && newVersion >= 32) { - db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER"); + db.execSQL( + "ALTER TABLE " + + SQLiteAxolotlStore.IDENTITIES_TABLENAME + + " ADD COLUMN " + + SQLiteAxolotlStore.LAST_ACTIVATION + + " NUMBER"); ContentValues defaults = new ContentValues(); defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION, System.currentTimeMillis()); db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, defaults, null, null); } if (oldVersion >= 15 && oldVersion < 33 && newVersion >= 33) { String whereClause = SQLiteAxolotlStore.OWN + "=1"; - db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED, true), whereClause, null); + db.update( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED, true), + whereClause, + null); } if (oldVersion < 34 && newVersion >= 34) { db.execSQL(CREATE_MESSAGE_TIME_INDEX); - final File oldPicturesDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/Conversations/"); - final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/"); - final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Files/"); - final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Videos/"); + final File oldPicturesDirectory = + new File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES) + + "/Conversations/"); + final File oldFilesDirectory = + new File(Environment.getExternalStorageDirectory() + "/Conversations/"); + final File newFilesDirectory = + new File( + Environment.getExternalStorageDirectory() + + "/Conversations/Media/Conversations Files/"); + final File newVideosDirectory = + new File( + Environment.getExternalStorageDirectory() + + "/Conversations/Media/Conversations Videos/"); if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) { - final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Images/"); + final File newPicturesDirectory = + new File( + Environment.getExternalStorageDirectory() + + "/Conversations/Media/Conversations Images/"); newPicturesDirectory.getParentFile().mkdirs(); if (oldPicturesDirectory.renameTo(newPicturesDirectory)) { - Log.d(Config.LOGTAG, "moved " + oldPicturesDirectory.getAbsolutePath() + " to " + newPicturesDirectory.getAbsolutePath()); + Log.d( + Config.LOGTAG, + "moved " + + oldPicturesDirectory.getAbsolutePath() + + " to " + + newPicturesDirectory.getAbsolutePath()); } } if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) { @@ -668,17 +1073,26 @@ public class DatabaseBackend extends SQLiteOpenHelper { for (File file : files) { if (file.getName().equals(".nomedia")) { if (file.delete()) { - Log.d(Config.LOGTAG, "deleted nomedia file in " + oldFilesDirectory.getAbsolutePath()); + Log.d( + Config.LOGTAG, + "deleted nomedia file in " + + oldFilesDirectory.getAbsolutePath()); } } else if (file.isFile()) { final String name = file.getName(); boolean isVideo = false; int start = name.lastIndexOf('.') + 1; if (start < name.length()) { - String mime = MimeUtils.guessMimeTypeFromExtension(name.substring(start)); + String mime = + MimeUtils.guessMimeTypeFromExtension(name.substring(start)); isVideo = mime != null && mime.startsWith("video/"); } - File dst = new File((isVideo ? newVideosDirectory : newFilesDirectory).getAbsolutePath() + "/" + file.getName()); + File dst = + new File( + (isVideo ? newVideosDirectory : newFilesDirectory) + .getAbsolutePath() + + "/" + + file.getName()); if (file.renameTo(dst)) { Log.d(Config.LOGTAG, "moved " + file + " to " + dst); } @@ -694,17 +1108,30 @@ public class DatabaseBackend extends SQLiteOpenHelper { for (Account account : accounts) { account.setOption(Account.OPTION_REQUIRES_ACCESS_MODE_CHANGE, true); account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, false); - db.update(Account.TABLENAME, account.getContentValues(), Account.UUID - + "=?", new String[]{account.getUuid()}); + db.update( + Account.TABLENAME, + account.getContentValues(), + Account.UUID + "=?", + new String[] {account.getUuid()}); } } if (oldVersion < 37 && newVersion >= 37) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.READ_BY_MARKERS + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.READ_BY_MARKERS + + " TEXT"); } if (oldVersion < 38 && newVersion >= 38) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.MARKABLE + + " NUMBER DEFAULT 0"); } if (oldVersion < 39 && newVersion >= 39) { @@ -715,13 +1142,21 @@ public class DatabaseBackend extends SQLiteOpenHelper { List accounts = getAccounts(db); for (Account account : accounts) { account.setOption(Account.OPTION_MAGIC_CREATE, true); - db.update(Account.TABLENAME, account.getContentValues(), Account.UUID - + "=?", new String[]{account.getUuid()}); + db.update( + Account.TABLENAME, + account.getContentValues(), + Account.UUID + "=?", + new String[] {account.getUuid()}); } } if (oldVersion < 44 && newVersion >= 44) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.DELETED + " NUMBER DEFAULT 0"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.DELETED + + " NUMBER DEFAULT 0"); db.execSQL(CREATE_MESSAGE_DELETED_INDEX); db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX); db.execSQL(CREATE_MESSAGE_TYPE_INDEX); @@ -740,10 +1175,20 @@ public class DatabaseBackend extends SQLiteOpenHelper { Log.d(Config.LOGTAG, "deleted old edit information in " + diff + "ms"); } if (oldVersion < 47 && newVersion >= 47) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.PRESENCE_NAME + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Contact.TABLENAME + + " ADD COLUMN " + + Contact.PRESENCE_NAME + + " TEXT"); } if (oldVersion < 48 && newVersion >= 48) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + Contact.RTP_CAPABILITY + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Contact.TABLENAME + + " ADD COLUMN " + + Contact.RTP_CAPABILITY + + " TEXT"); } if (oldVersion < 49 && newVersion >= 49) { db.beginTransaction(); @@ -766,16 +1211,46 @@ public class DatabaseBackend extends SQLiteOpenHelper { requiresMessageIndexRebuild = true; } if (oldVersion < 50 && newVersion >= 50) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.PINNED_MECHANISM + + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.PINNED_CHANNEL_BINDING + + " TEXT"); } if (oldVersion < 51 && newVersion >= 51) { - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT"); - db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.FAST_MECHANISM + + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Account.TABLENAME + + " ADD COLUMN " + + Account.FAST_TOKEN + + " TEXT"); } if (oldVersion < 52 && newVersion >= 52) { - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OCCUPANT_ID + " TEXT"); - db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.REACTIONS + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.OCCUPANT_ID + + " TEXT"); + db.execSQL( + "ALTER TABLE " + + Message.TABLENAME + + " ADD COLUMN " + + Message.REACTIONS + + " TEXT"); } } @@ -787,21 +1262,33 @@ public class DatabaseBackend extends SQLiteOpenHelper { while (cursor.moveToNext()) { String newJid; try { - newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))).toString(); + newJid = + Jid.of(cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))) + .toString(); } catch (IllegalArgumentException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " - + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) - + ": " + ignored + ". Skipping..."); + Log.e( + Config.LOGTAG, + "Failed to migrate Conversation CONTACTJID " + + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) + + ": " + + ignored + + ". Skipping..."); continue; } final String[] updateArgs = { - newJid, - cursor.getString(cursor.getColumnIndex(Conversation.UUID)), + newJid, cursor.getString(cursor.getColumnIndex(Conversation.UUID)), }; - db.execSQL("update " + Conversation.TABLENAME - + " set " + Conversation.CONTACTJID + " = ? " - + " where " + Conversation.UUID + " = ?", updateArgs); + db.execSQL( + "update " + + Conversation.TABLENAME + + " set " + + Conversation.CONTACTJID + + " = ? " + + " where " + + Conversation.UUID + + " = ?", + updateArgs); } cursor.close(); @@ -812,21 +1299,33 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Contact.JID))).toString(); } catch (final IllegalArgumentException e) { - Log.e(Config.LOGTAG, "Failed to migrate Contact JID " - + cursor.getString(cursor.getColumnIndex(Contact.JID)) - + ": Skipping...", e); + Log.e( + Config.LOGTAG, + "Failed to migrate Contact JID " + + cursor.getString(cursor.getColumnIndex(Contact.JID)) + + ": Skipping...", + e); continue; } final String[] updateArgs = { - newJid, - cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), - cursor.getString(cursor.getColumnIndex(Contact.JID)), + newJid, + cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), + cursor.getString(cursor.getColumnIndex(Contact.JID)), }; - db.execSQL("update " + Contact.TABLENAME - + " set " + Contact.JID + " = ? " - + " where " + Contact.ACCOUNT + " = ? " - + " AND " + Contact.JID + " = ?", updateArgs); + db.execSQL( + "update " + + Contact.TABLENAME + + " set " + + Contact.JID + + " = ? " + + " where " + + Contact.ACCOUNT + + " = ? " + + " AND " + + Contact.JID + + " = ?", + updateArgs); } cursor.close(); @@ -835,25 +1334,37 @@ public class DatabaseBackend extends SQLiteOpenHelper { while (cursor.moveToNext()) { String newServer; try { - newServer = Jid.of( - cursor.getString(cursor.getColumnIndex(Account.USERNAME)), - cursor.getString(cursor.getColumnIndex(Account.SERVER)), - null - ).getDomain().toEscapedString(); + newServer = + Jid.of( + cursor.getString(cursor.getColumnIndex(Account.USERNAME)), + cursor.getString(cursor.getColumnIndex(Account.SERVER)), + null) + .getDomain() + .toEscapedString(); } catch (IllegalArgumentException ignored) { - Log.e(Config.LOGTAG, "Failed to migrate Account SERVER " - + cursor.getString(cursor.getColumnIndex(Account.SERVER)) - + ": " + ignored + ". Skipping..."); + Log.e( + Config.LOGTAG, + "Failed to migrate Account SERVER " + + cursor.getString(cursor.getColumnIndex(Account.SERVER)) + + ": " + + ignored + + ". Skipping..."); continue; } String[] updateArgs = { - newServer, - cursor.getString(cursor.getColumnIndex(Account.UUID)), + newServer, cursor.getString(cursor.getColumnIndex(Account.UUID)), }; - db.execSQL("update " + Account.TABLENAME - + " set " + Account.SERVER + " = ? " - + " where " + Account.UUID + " = ?", updateArgs); + db.execSQL( + "update " + + Account.TABLENAME + + " set " + + Account.SERVER + + " = ? " + + " where " + + Account.UUID + + " = ?", + updateArgs); } cursor.close(); } @@ -1023,9 +1534,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = {hash, ver}; - Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null, - ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", - selectionArgs, null, null, null); + Cursor cursor = + db.query( + ServiceDiscoveryResult.TABLENAME, + null, + ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", + selectionArgs, + null, + null, + null); if (cursor.getCount() == 0) { cursor.close(); return null; @@ -1035,7 +1552,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { ServiceDiscoveryResult result = null; try { result = new ServiceDiscoveryResult(cursor); - } catch (JSONException e) { /* result is still null */ } + } catch (JSONException e) { + /* result is still null */ + } cursor.close(); return result; @@ -1052,7 +1571,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getReadableDatabase(); String where = Resolver.Result.DOMAIN + "=?"; String[] whereArgs = {domain}; - final Cursor cursor = db.query(RESOLVER_RESULTS_TABLENAME, null, where, whereArgs, null, null, null); + final Cursor cursor = + db.query(RESOLVER_RESULTS_TABLENAME, null, where, whereArgs, null, null, null); Resolver.Result result = null; if (cursor != null) { try { @@ -1060,7 +1580,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { result = Resolver.Result.fromCursor(cursor); } } catch (Exception e) { - Log.d(Config.LOGTAG, "unable to find cached resolver result in database " + e.getMessage()); + Log.d( + Config.LOGTAG, + "unable to find cached resolver result in database " + e.getMessage()); return null; } finally { cursor.close(); @@ -1074,14 +1596,32 @@ public class DatabaseBackend extends SQLiteOpenHelper { String whereToDelete = PresenceTemplate.MESSAGE + "=?"; String[] whereToDeleteArgs = {template.getStatusMessage()}; db.delete(PresenceTemplate.TABELNAME, whereToDelete, whereToDeleteArgs); - db.delete(PresenceTemplate.TABELNAME, PresenceTemplate.UUID + " not in (select " + PresenceTemplate.UUID + " from " + PresenceTemplate.TABELNAME + " order by " + PresenceTemplate.LAST_USED + " desc limit 9)", null); + db.delete( + PresenceTemplate.TABELNAME, + PresenceTemplate.UUID + + " not in (select " + + PresenceTemplate.UUID + + " from " + + PresenceTemplate.TABELNAME + + " order by " + + PresenceTemplate.LAST_USED + + " desc limit 9)", + null); db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues()); } public List getPresenceTemplates() { ArrayList templates = new ArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(PresenceTemplate.TABELNAME, null, null, null, null, null, PresenceTemplate.LAST_USED + " desc"); + Cursor cursor = + db.query( + PresenceTemplate.TABELNAME, + null, + null, + null, + null, + null, + PresenceTemplate.LAST_USED + " desc"); while (cursor.moveToNext()) { templates.add(PresenceTemplate.fromCursor(cursor)); } @@ -1093,9 +1633,18 @@ public class DatabaseBackend extends SQLiteOpenHelper { CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = {Integer.toString(status)}; - Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME - + " where " + Conversation.STATUS + " = ? and " + Conversation.CONTACTJID + " is not null order by " - + Conversation.CREATED + " desc", selectionArgs); + Cursor cursor = + db.rawQuery( + "select * from " + + Conversation.TABLENAME + + " where " + + Conversation.STATUS + + " = ? and " + + Conversation.CONTACTJID + + " is not null order by " + + Conversation.CREATED + + " desc", + selectionArgs); while (cursor.moveToNext()) { final Conversation conversation = Conversation.fromCursor(cursor); if (conversation.getJid() instanceof InvalidJid) { @@ -1233,11 +1782,54 @@ public class DatabaseBackend extends SQLiteOpenHelper { final SQLiteDatabase db = this.getReadableDatabase(); final StringBuilder SQL = new StringBuilder(); final String[] selectionArgs; - SQL.append("SELECT " + Message.TABLENAME + ".*," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + "," + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.MODE + " FROM " + Message.TABLENAME + " JOIN " + Conversation.TABLENAME + " ON " + Message.TABLENAME + "." + Message.CONVERSATION + "=" + Conversation.TABLENAME + "." + Conversation.UUID + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + Message.ENCRYPTION + " NOT IN(" + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + "," + Message.ENCRYPTION_PGP + "," + Message.ENCRYPTION_DECRYPTION_FAILED + "," + Message.ENCRYPTION_AXOLOTL_FAILED + ") AND " + Message.TYPE + " IN(" + Message.TYPE_TEXT + "," + Message.TYPE_PRIVATE + ") AND messages_index.body MATCH ?"); + SQL.append( + "SELECT " + + Message.TABLENAME + + ".*," + + Conversation.TABLENAME + + "." + + Conversation.CONTACTJID + + "," + + Conversation.TABLENAME + + "." + + Conversation.ACCOUNT + + "," + + Conversation.TABLENAME + + "." + + Conversation.MODE + + " FROM " + + Message.TABLENAME + + " JOIN " + + Conversation.TABLENAME + + " ON " + + Message.TABLENAME + + "." + + Message.CONVERSATION + + "=" + + Conversation.TABLENAME + + "." + + Conversation.UUID + + " JOIN messages_index ON messages_index.rowid=messages.rowid WHERE " + + Message.ENCRYPTION + + " NOT IN(" + + Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE + + "," + + Message.ENCRYPTION_PGP + + "," + + Message.ENCRYPTION_DECRYPTION_FAILED + + "," + + Message.ENCRYPTION_AXOLOTL_FAILED + + ") AND " + + Message.TYPE + + " IN(" + + Message.TYPE_TEXT + + "," + + Message.TYPE_PRIVATE + + ") AND messages_index.body MATCH ?"); if (uuid == null) { - selectionArgs = new String[]{FtsUtils.toMatchString(term)}; + selectionArgs = new String[] {FtsUtils.toMatchString(term)}; } else { - selectionArgs = new String[]{FtsUtils.toMatchString(term), uuid}; + selectionArgs = new String[] {FtsUtils.toMatchString(term), uuid}; SQL.append(" AND " + Conversation.TABLENAME + '.' + Conversation.UUID + "=?"); } SQL.append(" ORDER BY " + Message.TIME_SENT + " DESC limit " + Config.MAX_SEARCH_RESULTS); @@ -1252,18 +1844,34 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (internal) { final String name = file.getName(); if (name.endsWith(".pgp")) { - selection = "(" + Message.RELATIVE_FILE_PATH + " IN(?,?) OR (" + Message.RELATIVE_FILE_PATH + "=? and encryption in(1,4))) and type in (1,2,5)"; - selectionArgs = new String[]{file.getAbsolutePath(), name, name.substring(0, name.length() - 4)}; + selection = + "(" + + Message.RELATIVE_FILE_PATH + + " IN(?,?) OR (" + + Message.RELATIVE_FILE_PATH + + "=? and encryption in(1,4))) and type in (1,2,5)"; + selectionArgs = + new String[] { + file.getAbsolutePath(), name, name.substring(0, name.length() - 4) + }; } else { selection = Message.RELATIVE_FILE_PATH + " IN(?,?) and type in (1,2,5)"; - selectionArgs = new String[]{file.getAbsolutePath(), name}; + selectionArgs = new String[] {file.getAbsolutePath(), name}; } } else { selection = Message.RELATIVE_FILE_PATH + "=? and type in (1,2,5)"; - selectionArgs = new String[]{file.getAbsolutePath()}; + selectionArgs = new String[] {file.getAbsolutePath()}; } final List uuids = new ArrayList<>(); - Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID}, selection, selectionArgs, null, null, null); + Cursor cursor = + db.query( + Message.TABLENAME, + new String[] {Message.UUID}, + selection, + selectionArgs, + null, + null, + null); while (cursor != null && cursor.moveToNext()) { uuids.add(cursor.getString(0)); } @@ -1281,7 +1889,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { contentValues.put(Message.DELETED, 1); db.beginTransaction(); for (String uuid : uuids) { - db.update(Message.TABLENAME, contentValues, where, new String[]{uuid}); + db.update(Message.TABLENAME, contentValues, where, new String[] {uuid}); } db.setTransactionSuccessful(); db.endTransaction(); @@ -1294,7 +1902,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { for (FilePathInfo info : files) { final ContentValues contentValues = new ContentValues(); contentValues.put(Message.DELETED, info.deleted ? 1 : 0); - db.update(Message.TABLENAME, contentValues, where, new String[]{info.uuid.toString()}); + db.update(Message.TABLENAME, contentValues, where, new String[] {info.uuid.toString()}); } db.setTransactionSuccessful(); db.endTransaction(); @@ -1302,10 +1910,20 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List getFilePathInfo() { final SQLiteDatabase db = this.getReadableDatabase(); - final Cursor cursor = db.query(Message.TABLENAME, new String[]{Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, "type in (1,2,5) and " + Message.RELATIVE_FILE_PATH + " is not null", null, null, null, null); + final Cursor cursor = + db.query( + Message.TABLENAME, + new String[] {Message.UUID, Message.RELATIVE_FILE_PATH, Message.DELETED}, + "type in (1,2,5) and " + Message.RELATIVE_FILE_PATH + " is not null", + null, + null, + null, + null); final List list = new ArrayList<>(); while (cursor != null && cursor.moveToNext()) { - list.add(new FilePathInfo(cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0)); + list.add( + new FilePathInfo( + cursor.getString(0), cursor.getString(1), cursor.getInt(2) > 0)); } if (cursor != null) { cursor.close(); @@ -1315,7 +1933,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List getRelativeFilePaths(String account, Jid jid, int limit) { SQLiteDatabase db = this.getReadableDatabase(); - final String SQL = "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and " + Message.RELATIVE_FILE_PATH + " is not null and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc"; + final String SQL = + "select uuid,relativeFilePath from messages where type in (1,2,5) and deleted=0 and" + + " " + + Message.RELATIVE_FILE_PATH + + " is not null and conversationUuid=(select uuid from conversations where" + + " accountUuid=? and (contactJid=? or contactJid like ?)) order by" + + " timeSent desc"; final String[] args = {account, jid.toString(), jid.toString() + "/%"}; Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args); List filesPaths = new ArrayList<>(); @@ -1391,15 +2015,51 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } + public Conversation findConversation(final String uuid) { + final var db = this.getReadableDatabase(); + final String[] selectionArgs = {uuid}; + try (final Cursor cursor = + db.query( + Conversation.TABLENAME, + null, + Conversation.UUID + "=?", + selectionArgs, + null, + null, + null)) { + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToFirst(); + final Conversation conversation = Conversation.fromCursor(cursor); + if (conversation.getJid() instanceof InvalidJid) { + return null; + } + return conversation; + } + } + public Conversation findConversation(final Account account, final Jid contactJid) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {account.getUuid(), - contactJid.asBareJid().toString() + "/%", - contactJid.asBareJid().toString() + final SQLiteDatabase db = this.getReadableDatabase(); + final String[] selectionArgs = { + account.getUuid(), + contactJid.asBareJid().toString() + "/%", + contactJid.asBareJid().toString() }; - try(final Cursor cursor = db.query(Conversation.TABLENAME, null, - Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID - + " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null)) { + try (final Cursor cursor = + db.query( + Conversation.TABLENAME, + null, + Conversation.ACCOUNT + + "=? AND (" + + Conversation.CONTACTJID + + " like ? OR " + + Conversation.CONTACTJID + + "=?)", + selectionArgs, + null, + null, + null)) { if (cursor.getCount() == 0) { return null; } @@ -1408,6 +2068,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (conversation.getJid() instanceof InvalidJid) { return null; } + conversation.setAccount(account); return conversation; } } @@ -1415,8 +2076,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void updateConversation(final Conversation conversation) { final SQLiteDatabase db = this.getWritableDatabase(); final String[] args = {conversation.getUuid()}; - db.update(Conversation.TABLENAME, conversation.getContentValues(), - Conversation.UUID + "=?", args); + db.update( + Conversation.TABLENAME, + conversation.getContentValues(), + Conversation.UUID + "=?", + args); } public List getAccounts() { @@ -1427,9 +2091,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { public List getAccountJids(final boolean enabledOnly) { final SQLiteDatabase db = this.getReadableDatabase(); final List jids = new ArrayList<>(); - final String[] columns = new String[]{Account.USERNAME, Account.SERVER}; + final String[] columns = new String[] {Account.USERNAME, Account.SERVER}; final String where = enabledOnly ? "not options & (1 <<1)" : null; - try (final Cursor cursor = db.query(Account.TABLENAME, columns, where, null, null, null, null)) { + try (final Cursor cursor = + db.query(Account.TABLENAME, columns, where, null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { jids.add(Jid.of(cursor.getString(0), cursor.getString(1), null)); } @@ -1509,7 +2174,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { final SQLiteDatabase db = this.getWritableDatabase(); db.beginTransaction(); for (Contact contact : roster.getContacts()) { - if (contact.getOption(Contact.Options.IN_ROSTER) || contact.hasAvatarOrPresenceName() || contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { + if (contact.getOption(Contact.Options.IN_ROSTER) + || contact.hasAvatarOrPresenceName() + || contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { db.insert(Contact.TABLENAME, null, contact.getContentValues()); } else { String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?"; @@ -1522,7 +2189,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { account.setRosterVersion(roster.getVersion()); updateAccount(account); long duration = SystemClock.elapsedRealtime() - start; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisted roster in " + duration + "ms"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": persisted roster in " + duration + "ms"); } public void deleteMessagesInConversation(Conversation conversation) { @@ -1534,7 +2203,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.delete("cheogram.webxdc_updates", Message.CONVERSATION + "=?", args); db.setTransactionSuccessful(); db.endTransaction(); - Log.d(Config.LOGTAG, "deleted " + num + " messages for " + conversation.getJid().asBareJid() + " in " + (SystemClock.elapsedRealtime() - start) + "ms"); + Log.d( + Config.LOGTAG, + "deleted " + + num + + " messages for " + + conversation.getJid().asBareJid() + + " in " + + (SystemClock.elapsedRealtime() - start) + + "ms"); } public void expireOldMessages(long timestamp) { @@ -1551,7 +2228,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { Cursor cursor = null; try { SQLiteDatabase db = this.getReadableDatabase(); - String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) and (conversations.mode=0 or (messages.serverMsgId not null and messages.type=4)) order by messages.timesent desc limit 1"; + String sql = + "select messages.timeSent,messages.serverMsgId from accounts join conversations" + + " on accounts.uuid=conversations.accountUuid join messages on" + + " conversations.uuid=messages.conversationUuid where accounts.uuid=? and" + + " (messages.status=0 or messages.carbon=1 or messages.serverMsgId not" + + " null) and (conversations.mode=0 or (messages.serverMsgId not null and" + + " messages.type=4)) order by messages.timesent desc limit 1"; String[] args = {account.getUuid()}; cursor = db.rawQuery(sql, args); if (cursor.getCount() == 0) { @@ -1570,7 +2253,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public long getLastTimeFingerprintUsed(Account account, String fingerprint) { - String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1"; + String SQL = + "select messages.timeSent from accounts join conversations on" + + " accounts.uuid=conversations.accountUuid join messages on" + + " conversations.uuid=messages.conversationUuid where accounts.uuid=? and" + + " messages.axolotl_fingerprint=? order by messages.timesent desc limit 1"; String[] args = {account.getUuid(), fingerprint}; Cursor cursor = getReadableDatabase().rawQuery(SQL, args); long time; @@ -1588,14 +2275,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] columns = {Conversation.ATTRIBUTES}; String selection = Conversation.ACCOUNT + "=?"; String[] args = {account.getUuid()}; - Cursor cursor = db.query(Conversation.TABLENAME, columns, selection, args, null, null, null); + Cursor cursor = + db.query(Conversation.TABLENAME, columns, selection, args, null, null, null); MamReference maxClearDate = new MamReference(0); while (cursor.moveToNext()) { try { final JSONObject o = new JSONObject(cursor.getString(0)); - maxClearDate = MamReference.max(maxClearDate, MamReference.fromAttribute(o.getString(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY))); + maxClearDate = + MamReference.max( + maxClearDate, + MamReference.fromAttribute( + o.getString(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY))); } catch (Exception e) { - //ignored + // ignored } } cursor.close(); @@ -1604,16 +2296,22 @@ public class DatabaseBackend extends SQLiteOpenHelper { private Cursor getCursorForSession(Account account, SignalProtocolAddress contact) { final SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {account.getUuid(), - contact.getName(), - Integer.toString(contact.getDeviceId())}; - return db.query(SQLiteAxolotlStore.SESSION_TABLENAME, + String[] selectionArgs = { + account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId()) + }; + return db.query( + SQLiteAxolotlStore.SESSION_TABLENAME, null, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ? AND " - + SQLiteAxolotlStore.DEVICE_ID + " = ? ", + SQLiteAxolotlStore.ACCOUNT + + " = ? AND " + + SQLiteAxolotlStore.NAME + + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + + " = ? ", selectionArgs, - null, null, null); + null, + null, + null); } public SessionRecord loadSession(Account account, SignalProtocolAddress contact) { @@ -1622,7 +2320,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (cursor.getCount() != 0) { cursor.moveToFirst(); try { - session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + session = + new SessionRecord( + Base64.decode( + cursor.getString( + cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), + Base64.DEFAULT)); } catch (IOException e) { cursor.close(); throw new AssertionError(e); @@ -1637,21 +2340,23 @@ public class DatabaseBackend extends SQLiteOpenHelper { return getSubDeviceSessions(db, account, contact); } - private List getSubDeviceSessions(SQLiteDatabase db, Account account, SignalProtocolAddress contact) { + private List getSubDeviceSessions( + SQLiteDatabase db, Account account, SignalProtocolAddress contact) { List devices = new ArrayList<>(); String[] columns = {SQLiteAxolotlStore.DEVICE_ID}; - String[] selectionArgs = {account.getUuid(), - contact.getName()}; - Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ?", - selectionArgs, - null, null, null); + String[] selectionArgs = {account.getUuid(), contact.getName()}; + Cursor cursor = + db.query( + SQLiteAxolotlStore.SESSION_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.NAME + " = ?", + selectionArgs, + null, + null, + null); while (cursor.moveToNext()) { - devices.add(cursor.getInt( - cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); + devices.add(cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID))); } cursor.close(); @@ -1662,12 +2367,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { List addresses = new ArrayList<>(); String[] colums = {"DISTINCT " + SQLiteAxolotlStore.NAME}; String[] selectionArgs = {account.getUuid()}; - Cursor cursor = getReadableDatabase().query(SQLiteAxolotlStore.SESSION_TABLENAME, - colums, - SQLiteAxolotlStore.ACCOUNT + " = ?", - selectionArgs, - null, null, null - ); + Cursor cursor = + getReadableDatabase() + .query( + SQLiteAxolotlStore.SESSION_TABLENAME, + colums, + SQLiteAxolotlStore.ACCOUNT + " = ?", + selectionArgs, + null, + null, + null); while (cursor.moveToNext()) { addresses.add(cursor.getString(0)); } @@ -1682,12 +2391,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { return count != 0; } - public void storeSession(Account account, SignalProtocolAddress contact, SessionRecord session) { + public void storeSession( + Account account, SignalProtocolAddress contact, SessionRecord session) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.NAME, contact.getName()); values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT)); + values.put( + SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(), Base64.DEFAULT)); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values); } @@ -1698,22 +2409,26 @@ public class DatabaseBackend extends SQLiteOpenHelper { } private void deleteSession(SQLiteDatabase db, Account account, SignalProtocolAddress contact) { - String[] args = {account.getUuid(), - contact.getName(), - Integer.toString(contact.getDeviceId())}; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.NAME + " = ? AND " - + SQLiteAxolotlStore.DEVICE_ID + " = ? ", + String[] args = { + account.getUuid(), contact.getName(), Integer.toString(contact.getDeviceId()) + }; + db.delete( + SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + + " = ? AND " + + SQLiteAxolotlStore.NAME + + " = ? AND " + + SQLiteAxolotlStore.DEVICE_ID + + " = ? ", args); } public void deleteAllSessions(Account account, SignalProtocolAddress contact) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {account.getUuid(), contact.getName()}; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.NAME + " = ?", + db.delete( + SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.NAME + " = ?", args); } @@ -1721,12 +2436,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {SQLiteAxolotlStore.KEY}; String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; - Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", - selectionArgs, - null, null, null); + Cursor cursor = + db.query( + SQLiteAxolotlStore.PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, + null, + null); return cursor; } @@ -1737,7 +2455,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + record = + new PreKeyRecord( + Base64.decode( + cursor.getString( + cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), + Base64.DEFAULT)); } catch (IOException e) { throw new AssertionError(e); } @@ -1757,7 +2480,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.ID, record.getId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); + values.put( + SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); } @@ -1765,9 +2489,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { public int deletePreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {account.getUuid(), Integer.toString(preKeyId)}; - return db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", + return db.delete( + SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", args); } @@ -1775,11 +2499,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {SQLiteAxolotlStore.KEY}; String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", - selectionArgs, - null, null, null); + Cursor cursor = + db.query( + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, + null, + null); return cursor; } @@ -1790,7 +2518,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + record = + new SignedPreKeyRecord( + Base64.decode( + cursor.getString( + cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), + Base64.DEFAULT)); } catch (IOException e) { throw new AssertionError(e); } @@ -1804,15 +2537,24 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {SQLiteAxolotlStore.KEY}; String[] selectionArgs = {account.getUuid()}; - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=?", - selectionArgs, - null, null, null); + Cursor cursor = + db.query( + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=?", + selectionArgs, + null, + null, + null); while (cursor.moveToNext()) { try { - prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); + prekeys.add( + new SignedPreKeyRecord( + Base64.decode( + cursor.getString( + cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), + Base64.DEFAULT))); } catch (IOException ignored) { } } @@ -1824,11 +2566,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { String[] columns = {"count(" + SQLiteAxolotlStore.KEY + ")"}; String[] selectionArgs = {account.getUuid()}; SQLiteDatabase db = this.getReadableDatabase(); - Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - columns, - SQLiteAxolotlStore.ACCOUNT + "=?", - selectionArgs, - null, null, null); + Cursor cursor = + db.query( + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + SQLiteAxolotlStore.ACCOUNT + "=?", + selectionArgs, + null, + null, + null); final int count; if (cursor.moveToFirst()) { count = cursor.getInt(0); @@ -1850,7 +2596,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.ID, record.getId()); - values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); + values.put( + SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(), Base64.DEFAULT)); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); } @@ -1858,9 +2605,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void deleteSignedPreKey(Account account, int signedPreKeyId) { SQLiteDatabase db = this.getWritableDatabase(); String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; - db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + "=? AND " - + SQLiteAxolotlStore.ID + "=?", + db.delete( + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?", args); } @@ -1869,7 +2616,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { return getIdentityKeyCursor(db, account, name, own); } - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, boolean own) { + private Cursor getIdentityKeyCursor( + SQLiteDatabase db, Account account, String name, boolean own) { return getIdentityKeyCursor(db, account, name, own, null); } @@ -1882,11 +2630,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { return getIdentityKeyCursor(db, account, null, null, fingerprint); } - private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { - String[] columns = {SQLiteAxolotlStore.TRUST, - SQLiteAxolotlStore.ACTIVE, - SQLiteAxolotlStore.LAST_ACTIVATION, - SQLiteAxolotlStore.KEY}; + private Cursor getIdentityKeyCursor( + SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) { + String[] columns = { + SQLiteAxolotlStore.TRUST, + SQLiteAxolotlStore.ACTIVE, + SQLiteAxolotlStore.LAST_ACTIVATION, + SQLiteAxolotlStore.KEY + }; ArrayList selectionArgs = new ArrayList<>(4); selectionArgs.add(account.getUuid()); String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?"; @@ -1902,11 +2653,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { selectionArgs.add(own ? "1" : "0"); selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?"; } - Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, - columns, - selectionString, - selectionArgs.toArray(new String[selectionArgs.size()]), - null, null, null); + Cursor cursor = + db.query( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + columns, + selectionString, + selectionArgs.toArray(new String[selectionArgs.size()]), + null, + null, + null); return cursor; } @@ -1923,9 +2678,20 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (cursor.getCount() != 0) { cursor.moveToFirst(); try { - identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)); + identityKeyPair = + new IdentityKeyPair( + Base64.decode( + cursor.getString( + cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), + Base64.DEFAULT)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().asBareJid() + ", address: " + name); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Encountered invalid IdentityKey in database for account" + + account.getJid().asBareJid() + + ", address: " + + name); } } cursor.close(); @@ -1937,7 +2703,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { return loadIdentityKeys(account, name, null); } - public Set loadIdentityKeys(Account account, String name, FingerprintStatus status) { + public Set loadIdentityKeys( + Account account, String name, FingerprintStatus status) { Set identityKeys = new HashSet<>(); Cursor cursor = getIdentityKeyCursor(account, name, false); @@ -1950,10 +2717,22 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (key != null) { identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0)); } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().asBareJid() + ", address: " + name); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Missing key (possibly preverified) in database for account" + + account.getJid().asBareJid() + + ", address: " + + name); } } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().asBareJid() + ", address: " + name); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Encountered invalid IdentityKey in database for account" + + account.getJid().asBareJid() + + ", address: " + + name); } } cursor.close(); @@ -1964,22 +2743,40 @@ public class DatabaseBackend extends SQLiteOpenHelper { public long numTrustedKeys(Account account, String name) { SQLiteDatabase db = getReadableDatabase(); String[] args = { - account.getUuid(), - name, - FingerprintStatus.Trust.TRUSTED.toString(), - FingerprintStatus.Trust.VERIFIED.toString(), - FingerprintStatus.Trust.VERIFIED_X509.toString() + account.getUuid(), + name, + FingerprintStatus.Trust.TRUSTED.toString(), + FingerprintStatus.Trust.VERIFIED.toString(), + FingerprintStatus.Trust.VERIFIED_X509.toString() }; - return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME, - SQLiteAxolotlStore.ACCOUNT + " = ?" - + " AND " + SQLiteAxolotlStore.NAME + " = ?" - + " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ?)" - + " AND " + SQLiteAxolotlStore.ACTIVE + " > 0", - args - ); + return DatabaseUtils.queryNumEntries( + db, + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + SQLiteAxolotlStore.ACCOUNT + + " = ?" + + " AND " + + SQLiteAxolotlStore.NAME + + " = ?" + + " AND (" + + SQLiteAxolotlStore.TRUST + + " = ? OR " + + SQLiteAxolotlStore.TRUST + + " = ? OR " + + SQLiteAxolotlStore.TRUST + + " = ?)" + + " AND " + + SQLiteAxolotlStore.ACTIVE + + " > 0", + args); } - private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) { + private void storeIdentityKey( + Account account, + String name, + boolean own, + String fingerprint, + String base64Serialized, + FingerprintStatus status) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); @@ -1988,7 +2785,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(SQLiteAxolotlStore.KEY, base64Serialized); values.putAll(status.toContentValues()); - String where = SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.NAME + "=? AND " + SQLiteAxolotlStore.FINGERPRINT + " =?"; + String where = + SQLiteAxolotlStore.ACCOUNT + + "=? AND " + + SQLiteAxolotlStore.NAME + + "=? AND " + + SQLiteAxolotlStore.FINGERPRINT + + " =?"; String[] whereArgs = {account.getUuid(), name, fingerprint}; int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, where, whereArgs); if (rows == 0) { @@ -1996,7 +2799,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } - public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) { + public void storePreVerification( + Account account, String name, String fingerprint, FingerprintStatus status) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid()); @@ -2020,36 +2824,43 @@ public class DatabaseBackend extends SQLiteOpenHelper { return status; } - public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) { + public boolean setIdentityKeyTrust( + Account account, String fingerprint, FingerprintStatus fingerprintStatus) { SQLiteDatabase db = this.getWritableDatabase(); return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus); } - private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) { - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; - int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(), - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs); + private boolean setIdentityKeyTrust( + SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) { + String[] selectionArgs = {account.getUuid(), fingerprint}; + int rows = + db.update( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + status.toContentValues(), + SQLiteAxolotlStore.ACCOUNT + + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + + " = ? ", + selectionArgs); return rows == 1; } - public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) { + public boolean setIdentityKeyCertificate( + Account account, String fingerprint, X509Certificate x509Certificate) { SQLiteDatabase db = this.getWritableDatabase(); - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; + String[] selectionArgs = {account.getUuid(), fingerprint}; try { ContentValues values = new ContentValues(); values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded()); - return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + SQLiteAxolotlStore.FINGERPRINT + " = ? ", - selectionArgs) == 1; + return db.update( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + values, + SQLiteAxolotlStore.ACCOUNT + + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + + " = ? ", + selectionArgs) + == 1; } catch (CertificateEncodingException e) { Log.d(Config.LOGTAG, "could not encode certificate"); return false; @@ -2058,25 +2869,34 @@ public class DatabaseBackend extends SQLiteOpenHelper { public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) { SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = { - account.getUuid(), - fingerprint - }; + String[] selectionArgs = {account.getUuid(), fingerprint}; String[] colums = {SQLiteAxolotlStore.CERTIFICATE}; - String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; - Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null); + String selection = + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; + Cursor cursor = + db.query( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, + colums, + selection, + selectionArgs, + null, + null, + null); if (cursor.getCount() < 1) { return null; } else { cursor.moveToFirst(); - byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); + byte[] certificate = + cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); cursor.close(); if (certificate == null || certificate.length == 0) { return null; } try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); + return (X509Certificate) + certificateFactory.generateCertificate( + new ByteArrayInputStream(certificate)); } catch (CertificateException e) { Log.d(Config.LOGTAG, "certificate exception " + e.getMessage()); return null; @@ -2084,17 +2904,31 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } - public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) { - storeIdentityKey(account, name, false, CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status); + public void storeIdentityKey( + Account account, String name, IdentityKey identityKey, FingerprintStatus status) { + storeIdentityKey( + account, + name, + false, + CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()), + Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), + status); } public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, account.getJid().asBareJid().toString(), true, CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false)); + storeIdentityKey( + account, + account.getJid().asBareJid().toString(), + true, + CryptoHelper.bytesToHex(identityKeyPair.getPublicKey().serialize()), + Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), + FingerprintStatus.createActiveVerified(false)); } - private void recreateAxolotlDb(SQLiteDatabase db) { - Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + Log.d( + Config.LOGTAG, + AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<"); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME); db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME); @@ -2107,36 +2941,70 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void wipeAxolotlDb(Account account) { String accountName = account.getUuid(); - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + Log.d( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + + accountName + + " <<<"); SQLiteDatabase db = this.getWritableDatabase(); - String[] deleteArgs = { - accountName - }; - db.delete(SQLiteAxolotlStore.SESSION_TABLENAME, + String[] deleteArgs = {accountName}; + db.delete( + SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); - db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME, + db.delete( + SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); - db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + db.delete( + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); - db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME, + db.delete( + SQLiteAxolotlStore.IDENTITIES_TABLENAME, SQLiteAxolotlStore.ACCOUNT + " = ?", deleteArgs); } - public List getFrequentContacts(int days) { - SQLiteDatabase db = this.getReadableDatabase(); - final String SQL = "select " + Conversation.TABLENAME + "." + Conversation.ACCOUNT + "," + Conversation.TABLENAME + "." + Conversation.CONTACTJID + " from " + Conversation.TABLENAME + " join " + Message.TABLENAME + " on conversations.uuid=messages.conversationUuid where messages.status!=0 and carbon==0 and conversations.mode=0 and messages.timeSent>=? group by conversations.uuid order by count(body) desc limit 4;"; - String[] whereArgs = new String[]{String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days))}; + public List getFrequentContacts(final int days) { + final var db = this.getReadableDatabase(); + final String SQL = + "select " + + Conversation.TABLENAME + + "." + + Conversation.UUID + + "," + + Conversation.TABLENAME + + "." + + Conversation.ACCOUNT + + "," + + Conversation.TABLENAME + + "." + + Conversation.CONTACTJID + + " from " + + Conversation.TABLENAME + + " join " + + Message.TABLENAME + + " on conversations.uuid=messages.conversationUuid where" + + " messages.status!=0 and carbon==0 and conversations.mode=0 and" + + " messages.timeSent>=? group by conversations.uuid order by count(body)" + + " desc limit 4;"; + String[] whereArgs = + new String[] { + String.valueOf(System.currentTimeMillis() - (Config.MILLISECONDS_IN_DAY * days)) + }; Cursor cursor = db.rawQuery(SQL, whereArgs); ArrayList contacts = new ArrayList<>(); while (cursor.moveToNext()) { try { - contacts.add(new ShortcutService.FrequentContact(cursor.getString(0), Jid.of(cursor.getString(1)))); - } catch (Exception e) { - Log.d(Config.LOGTAG, e.getMessage()); + contacts.add( + new ShortcutService.FrequentContact( + cursor.getString(0), + cursor.getString(1), + Jid.of(cursor.getString(2)))); + } catch (final Exception e) { + Log.e(Config.LOGTAG, "could not create frequent contact", e); } } cursor.close(); diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index ca9fb4dffb2bc9c7c79b6d4d83a7602f6bee2674..120f75ca0d8729f1bd7ab7b9b2ce92a1217744d7 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -33,7 +33,6 @@ import android.util.DisplayMetrics; import android.util.Pair; import android.util.Log; import android.util.LruCache; - import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.core.content.FileProvider; @@ -170,7 +169,8 @@ public class FileBackend { if (dimensions.getMin() > 720) { Log.d( Config.LOGTAG, - "do not consider video file with min width larger than 720 for size check"); + "do not consider video file with min width larger than 720 for size" + + " check"); continue; } } catch (final IOException | NotAVideoFile e) { @@ -300,7 +300,8 @@ public class FileBackend { return inSampleSize; } - private static Dimensions getVideoDimensions(Context context, Uri uri) throws NotAVideoFile, IOException { + private static Dimensions getVideoDimensions(Context context, Uri uri) + throws NotAVideoFile, IOException { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); try { mediaMetadataRetriever.setDataSource(context, uri); @@ -786,10 +787,10 @@ public class FileBackend { return is; } - public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException { + public void copyFileToPrivateStorage(final File file, final Uri uri) throws FileCopyException { final var parentDirectory = file.getParentFile(); if (parentDirectory != null && parentDirectory.mkdirs()) { - Log.d(Config.LOGTAG,"created directory "+parentDirectory.getAbsolutePath()); + Log.d(Config.LOGTAG, "created directory " + parentDirectory.getAbsolutePath()); } try { if (!file.createNewFile() && file.length() > 0) { @@ -829,9 +830,10 @@ public class FileBackend { } } - public void copyFileToPrivateStorage(Message message, Uri uri, String type) + public void copyFileToPrivateStorage(final Message message, final Uri uri, final String type) throws FileCopyException { - final 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) { @@ -2003,7 +2005,7 @@ public class FileBackend { return calcSampleSize(options, size); } - public void updateFileParams(Message message) { + public void updateFileParams(final Message message) { updateFileParams(message, null); } diff --git a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java index 0703ce3f6474442a7bbfec389a0818d4813a9b84..f86d000de50dd307b6ace02372ff0359761ee24e 100644 --- a/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java +++ b/src/main/java/eu/siacs/conversations/services/AttachFileToConversationRunnable.java @@ -5,18 +5,9 @@ import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; import android.util.Log; - import androidx.annotation.NonNull; - import com.otaliastudios.transcoder.Transcoder; import com.otaliastudios.transcoder.TranscoderListener; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; @@ -26,6 +17,11 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.TranscoderStrategies; +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; public class AttachFileToConversationRunnable implements Runnable, TranscoderListener { @@ -39,16 +35,26 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis private final long originalFileSize; private int currentProgress = -1; - AttachFileToConversationRunnable(XmppConnectionService xmppConnectionService, Uri uri, String type, Message message, UiCallback callback) { + AttachFileToConversationRunnable( + XmppConnectionService xmppConnectionService, + Uri uri, + String type, + Message message, + UiCallback callback) { this.uri = uri; this.type = type; this.mXmppConnectionService = xmppConnectionService; this.message = message; this.callback = callback; - mimeType = MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); - final int autoAcceptFileSize = mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); + mimeType = + MimeUtils.guessMimeTypeFromUriAndMime(mXmppConnectionService, uri, type); + final int autoAcceptFileSize = + mXmppConnectionService.getResources().getInteger(R.integer.auto_accept_filesize); this.originalFileSize = FileBackend.getFileSize(mXmppConnectionService, uri); - this.isVideoMessage = (mimeType != null && mimeType.startsWith("video/")) && originalFileSize > autoAcceptFileSize && !"uncompressed".equals(getVideoCompression()); + this.isVideoMessage = + (mimeType != null && mimeType.startsWith("video/")) + && originalFileSize > autoAcceptFileSize + && !"uncompressed".equals(getVideoCompression()); } boolean isVideoMessage() { @@ -75,7 +81,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis } } else { try { - mXmppConnectionService.getFileBackend().copyFileToPrivateStorage(message, uri, type); + mXmppConnectionService + .getFileBackend() + .copyFileToPrivateStorage(message, uri, type); mXmppConnectionService.getFileBackend().updateFileParams(message); if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { final PgpEngine pgpEngine = mXmppConnectionService.getPgpEngine(); @@ -87,16 +95,26 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis } else { mXmppConnectionService.sendMessage(message, () -> callback.success(message)); } - } catch (FileBackend.FileCopyException e) { + } catch (final FileBackend.FileCopyException e) { callback.error(e.getResId(), message); } } } + private void fallbackToProcessAsFile() { + final var file = mXmppConnectionService.getFileBackend().getFile(message); + if (file.exists() && file.delete()) { + Log.d(Config.LOGTAG, "deleted preexisting file " + file.getAbsolutePath()); + } + XmppConnectionService.FILE_ATTACHMENT_EXECUTOR.execute(this::processAsFile); + } + private void processAsVideo() throws FileNotFoundException { Log.d(Config.LOGTAG, "processing file as video"); mXmppConnectionService.startOngoingVideoTranscodingForegroundNotification(); - mXmppConnectionService.getFileBackend().setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4")); + mXmppConnectionService + .getFileBackend() + .setupRelativeFilePath(message, String.format("%s.%s", message.getUuid(), "mp4")); final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); if (Objects.requireNonNull(file.getParentFile()).mkdirs()) { Log.d(Config.LOGTAG, "created parent directory for video file"); @@ -106,16 +124,23 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis final Future future; try { - future = Transcoder.into(file.getAbsolutePath()). - addDataSource(mXmppConnectionService, uri) - .setVideoTrackStrategy(highQuality ? TranscoderStrategies.VIDEO_720P : TranscoderStrategies.VIDEO_360P) - .setAudioTrackStrategy(highQuality ? TranscoderStrategies.AUDIO_HQ : TranscoderStrategies.AUDIO_MQ) - .setListener(this) - .transcode(); + future = + Transcoder.into(file.getAbsolutePath()) + .addDataSource(mXmppConnectionService, uri) + .setVideoTrackStrategy( + highQuality + ? TranscoderStrategies.VIDEO_720P + : TranscoderStrategies.VIDEO_360P) + .setAudioTrackStrategy( + highQuality + ? TranscoderStrategies.AUDIO_HQ + : TranscoderStrategies.AUDIO_MQ) + .setListener(this) + .transcode(); } catch (final RuntimeException e) { // transcode can already throw if there is an invalid file format or a platform bug mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); - processAsFile(); + fallbackToProcessAsFile(); return; } try { @@ -125,9 +150,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis } catch (final ExecutionException e) { if (e.getCause() instanceof Error) { mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); - processAsFile(); + fallbackToProcessAsFile(); } else { - Log.d(Config.LOGTAG, "ignoring execution exception. Should get handled by onTranscodeFiled() instead", e); + Log.d(Config.LOGTAG, "ignoring execution exception. Handled by onTranscodeFiled()"); } } } @@ -137,7 +162,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis final int p = (int) Math.round(progress * 100); if (p > currentProgress) { currentProgress = p; - mXmppConnectionService.getNotificationService().updateFileAddingNotification(p, message); + mXmppConnectionService + .getNotificationService() + .updateFileAddingNotification(p, message); } } @@ -146,11 +173,15 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); final File file = mXmppConnectionService.getFileBackend().getFile(message); long convertedFileSize = mXmppConnectionService.getFileBackend().getFile(message).getSize(); - Log.d(Config.LOGTAG, "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize); + Log.d( + Config.LOGTAG, + "originalFileSize=" + originalFileSize + " convertedFileSize=" + convertedFileSize); if (originalFileSize != 0 && convertedFileSize >= originalFileSize) { if (file.delete()) { - Log.d(Config.LOGTAG, "original file size was smaller. deleting and processing as file"); - processAsFile(); + Log.d( + Config.LOGTAG, + "original file size was smaller. deleting and processing as file"); + fallbackToProcessAsFile(); return; } else { Log.d(Config.LOGTAG, "unable to delete converted file"); @@ -167,14 +198,14 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis @Override public void onTranscodeCanceled() { mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); - processAsFile(); + fallbackToProcessAsFile(); } @Override public void onTranscodeFailed(@NonNull final Throwable exception) { mXmppConnectionService.stopOngoingVideoTranscodingForegroundNotification(); Log.d(Config.LOGTAG, "video transcoding failed", exception); - processAsFile(); + fallbackToProcessAsFile(); } @Override @@ -182,7 +213,7 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis if (this.isVideoMessage()) { try { processAsVideo(); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { processAsFile(); } } else { @@ -195,7 +226,9 @@ public class AttachFileToConversationRunnable implements Runnable, TranscoderLis } public static String getVideoCompression(final Context context) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression)); + final SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); + return preferences.getString( + "video_compression", context.getResources().getString(R.string.video_compression)); } } diff --git a/src/main/java/eu/siacs/conversations/services/CallIntegration.java b/src/main/java/eu/siacs/conversations/services/CallIntegration.java index 37d8edb8c394d240694eb8e22402f17de946af2d..b6efdd85185d6ee53a7ac08b1ee444c025b83256 100644 --- a/src/main/java/eu/siacs/conversations/services/CallIntegration.java +++ b/src/main/java/eu/siacs/conversations/services/CallIntegration.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.services; import android.content.Context; import android.content.pm.PackageManager; -import android.media.AudioAttributes; import android.media.AudioManager; import android.media.ToneGenerator; import android.net.Uri; @@ -12,22 +11,18 @@ import android.telecom.CallEndpoint; import android.telecom.Connection; import android.telecom.DisconnectCause; import android.util.Log; - import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; - import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; - +import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; import eu.siacs.conversations.ui.util.MainThreadExecutor; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.Media; - import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -46,7 +41,7 @@ public class CallIntegration extends Connection { * SecurityException */ private static final List BROKEN_DEVICE_MODELS = - Arrays.asList("gtaxlwifi", "a5y17lte", "YT-X705F"); + Arrays.asList("gtaxlwifi", "a5y17lte", "YT-X705F", "HWAGS2"); /** * all Realme devices at least up to and including Android 11 are broken @@ -63,7 +58,6 @@ public class CallIntegration extends Connection { Arrays.asList("realme", "oppo", "oneplus"); public static final int DEFAULT_TONE_VOLUME = 60; - private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90; private final Context context; @@ -383,7 +377,7 @@ public class CallIntegration extends Connection { } } if (state == STATE_ACTIVE) { - playConnectedSound(); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_ANSWER, 100); } else if (state == STATE_DISCONNECTED) { final var audioManager = this.appRTCAudioManager; if (audioManager != null) { @@ -392,26 +386,10 @@ public class CallIntegration extends Connection { } } - private void playConnectedSound() { - final var audioAttributes = - new AudioAttributes.Builder() - .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL) - .build(); - final var mediaPlayer = - MediaPlayer.create( - context, - R.raw.connected, - audioAttributes, - AudioManager.AUDIO_SESSION_ID_GENERATE); - mediaPlayer.setVolume( - DEFAULT_MEDIA_PLAYER_VOLUME / 100f, DEFAULT_MEDIA_PLAYER_VOLUME / 100f); - mediaPlayer.start(); - } - public void success() { Log.d(Config.LOGTAG, "CallIntegration.success()"); - startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); - this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CONFIRM, 600); + this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 600); } public void accepted() { @@ -425,8 +403,8 @@ public class CallIntegration extends Connection { public void error() { Log.d(Config.LOGTAG, "CallIntegration.error()"); - startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375); - this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375); + startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CONFIRM, 600); + this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 600); } public void retracted() { @@ -530,13 +508,17 @@ public class CallIntegration extends Connection { return selfManaged(context); } - public static boolean selfManaged(final Context context) { + public static boolean selfManagedAvailable(final Context context) { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < 35 && hasSystemFeature(context) && isDeviceModelSupported(); } + public static boolean selfManaged(final Context context) { + return selfManagedAvailable(context) && new AppSettings(context).isCallIntegration(); + } + public static boolean hasSystemFeature(final Context context) { final var packageManager = context.getPackageManager(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -562,6 +544,10 @@ public class CallIntegration extends Connection { if ("umidigi".equals(manufacturer) && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) { return false; } + // SailfishOS's AppSupport do not support Call Integration + if (Build.MODEL.endsWith("(AppSupport)")) { + return false; + } return true; } diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 4096ce9a0a5c332f470c367d28cf03986899bfc5..484ed1a97a1dc15d19c6df1933033d7dd5ab306a 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -47,7 +47,6 @@ import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.graphics.drawable.IconCompat; - import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Splitter; @@ -55,7 +54,6 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; - import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -81,7 +79,6 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection; import eu.siacs.conversations.xmpp.jingle.Media; - import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -95,6 +92,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -131,7 +129,7 @@ public class NotificationService { private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel"; private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL_PREFIX = "incoming_calls_channel#"; - private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages"; + public static final String MESSAGES_NOTIFICATION_CHANNEL = "messages"; NotificationService(final XmppConnectionService service) { this.mXmppConnectionService = service; @@ -246,25 +244,8 @@ public class NotificationService { missedCallsChannel.setGroup("calls"); notificationManager.createNotificationChannel(missedCallsChannel); - final NotificationChannel messagesChannel = - new NotificationChannel( - MESSAGES_NOTIFICATION_CHANNEL, - c.getString(R.string.messages_channel_name), - NotificationManager.IMPORTANCE_HIGH); - messagesChannel.setShowBadge(true); - messagesChannel.setSound( - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), - new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .build()); - messagesChannel.setLightColor(LED_COLOR); - final int dat = 70; - final long[] pattern = {0, 3 * dat, dat, dat}; - messagesChannel.setVibrationPattern(pattern); - messagesChannel.enableVibration(true); - messagesChannel.enableLights(true); - messagesChannel.setGroup("chats"); + final var messagesChannel = + prepareMessagesChannel(mXmppConnectionService, MESSAGES_NOTIFICATION_CHANNEL); notificationManager.createNotificationChannel(messagesChannel); final NotificationChannel silentMessagesChannel = new NotificationChannel( @@ -295,6 +276,41 @@ public class NotificationService { notificationManager.createNotificationChannel(deliveryFailedChannel); } + @RequiresApi(api = Build.VERSION_CODES.R) + public static void createConversationChannel( + final Context context, final ShortcutInfoCompat shortcut) { + final var messagesChannel = prepareMessagesChannel(context, UUID.randomUUID().toString()); + messagesChannel.setName(shortcut.getShortLabel()); + messagesChannel.setConversationId(MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId()); + final var notificationManager = context.getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(messagesChannel); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel prepareMessagesChannel( + final Context context, final String id) { + final NotificationChannel messagesChannel = + new NotificationChannel( + id, + context.getString(R.string.messages_channel_name), + NotificationManager.IMPORTANCE_HIGH); + messagesChannel.setShowBadge(true); + messagesChannel.setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build()); + messagesChannel.setLightColor(LED_COLOR); + final int dat = 70; + final long[] pattern = {0, 3 * dat, dat, dat}; + messagesChannel.setVibrationPattern(pattern); + messagesChannel.enableVibration(true); + messagesChannel.enableLights(true); + messagesChannel.setGroup("chats"); + return messagesChannel; + } + @RequiresApi(api = Build.VERSION_CODES.O) private static void createInitialIncomingCallChannelIfNecessary(final Context context) { final var currentIteration = getCurrentIncomingCallChannelIteration(context); @@ -562,7 +578,8 @@ public class NotificationService { Log.d( Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() - + ": suppressing failed delivery notification because conversation is open"); + + ": suppressing failed delivery notification because conversation is" + + " open"); return; } final PendingIntent pendingIntent = createContentIntent(conversation); @@ -639,7 +656,7 @@ public class NotificationService { mXmppConnectionService, channelId); final Contact contact = id.getContact(); builder.addPerson(getPerson(contact)); - ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact); + ShortcutInfoCompat info = mXmppConnectionService.getShortcutService().getShortcutInfo(contact); builder.setShortcutInfo(info); if (Build.VERSION.SDK_INT >= 30) { mXmppConnectionService.getSystemService(ShortcutManager.class).pushDynamicShortcut(info.toShortcutInfo()); @@ -710,10 +727,11 @@ public class NotificationService { .build()); modifyIncomingCall(builder, id.account); final Notification notification = builder.build(); - notification.audioAttributes = new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) - .build(); + notification.audioAttributes = + new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) + .build(); notification.flags = notification.flags | Notification.FLAG_INSISTENT; notify(INCOMING_CALL_NOTIFICATION_ID, notification); } @@ -795,7 +813,8 @@ public class NotificationService { if (jingleRtpConnection == null) { return false; } - final var notificationManager = mXmppConnectionService.getSystemService(NotificationManager.class); + final var notificationManager = + mXmppConnectionService.getSystemService(NotificationManager.class); if (Iterables.any( Arrays.asList(notificationManager.getActiveNotifications()), n -> n.getId() == INCOMING_CALL_NOTIFICATION_ID)) { @@ -909,7 +928,8 @@ public class NotificationService { Log.d( Config.LOGTAG, conversational.getAccount().getJid().asBareJid() - + ": dismissed missed call because call was picked up on other device"); + + ": dismissed missed call because call was picked up on" + + " other device"); iterator.remove(); } } @@ -1458,12 +1478,15 @@ public class NotificationService { if (systemAccount != null) { notificationBuilder.addPerson(systemAccount.toString()); } - info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact); + info = + mXmppConnectionService + .getShortcutService() + .getShortcutInfo(contact, conversation.getUuid()); } else { info = mXmppConnectionService .getShortcutService() - .getShortcutInfoCompat(conversation.getMucOptions()); + .getShortcutInfo(conversation.getMucOptions()); } notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent()); notificationBuilder.setSmallIcon(R.drawable.ic_notification); @@ -1498,16 +1521,16 @@ public class NotificationService { } final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); bigPictureStyle.bigPicture(bitmap); - if (tmp.size() > 0) { - CharSequence text = getMergedBodies(tmp); - bigPictureStyle.setSummaryText(text); - builder.setContentText(text); - builder.setTicker(text); - } else { + if (tmp.isEmpty()) { final String description = UIHelper.getFileDescriptionString(mXmppConnectionService, message); builder.setContentText(description); builder.setTicker(description); + } else { + final CharSequence text = getMergedBodies(tmp); + bigPictureStyle.setSummaryText(text); + builder.setContentText(text); + builder.setTicker(text); } builder.setStyle(bigPictureStyle); } catch (final IOException e) { diff --git a/src/main/java/eu/siacs/conversations/services/ShortcutService.java b/src/main/java/eu/siacs/conversations/services/ShortcutService.java index 57607e23228fdd3ce9b011274542dcdbacb89736..547f8019a285ca88a9642cc0abfa9831132a94e3 100644 --- a/src/main/java/eu/siacs/conversations/services/ShortcutService.java +++ b/src/main/java/eu/siacs/conversations/services/ShortcutService.java @@ -2,17 +2,15 @@ package eu.siacs.conversations.services; import android.annotation.TargetApi; import android.content.Intent; -import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.graphics.Bitmap; -import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Build; +import android.os.PersistableBundle; import android.util.Log; - import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.drawable.IconCompat; import java.util.ArrayList; @@ -20,21 +18,30 @@ import java.util.HashMap; import java.util.List; import java.util.Set; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; + import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.StartConversationActivity; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor; import eu.siacs.conversations.xmpp.Jid; +import java.util.Collection; +import java.util.List; public class ShortcutService { private final XmppConnectionService xmppConnectionService; - private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName()); + private final ReplacingSerialSingleThreadExecutor replacingSerialSingleThreadExecutor = + new ReplacingSerialSingleThreadExecutor(ShortcutService.class.getSimpleName()); - public ShortcutService(XmppConnectionService xmppConnectionService) { + public ShortcutService(final XmppConnectionService xmppConnectionService) { this.xmppConnectionService = xmppConnectionService; } @@ -44,12 +51,7 @@ public class ShortcutService { public void refresh(final boolean forceUpdate) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - final Runnable r = new Runnable() { - @Override - public void run() { - refreshImpl(forceUpdate); - } - }; + final Runnable r = () -> refreshImpl(forceUpdate); replacingSerialSingleThreadExecutor.execute(r); } } @@ -57,71 +59,94 @@ public class ShortcutService { @TargetApi(25) public void report(Contact contact) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { - ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); + ShortcutManager shortcutManager = + xmppConnectionService.getSystemService(ShortcutManager.class); shortcutManager.reportShortcutUsed(getShortcutId(contact)); } } @TargetApi(25) - private void refreshImpl(boolean forceUpdate) { - List frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30); - HashMap accounts = new HashMap<>(); - for(Account account : xmppConnectionService.getAccounts()) { - accounts.put(account.getUuid(),account); - } - List contacts = new ArrayList<>(); - for(FrequentContact frequentContact : frequentContacts) { - Account account = accounts.get(frequentContact.account); + private void refreshImpl(final boolean forceUpdate) { + final var frequentContacts = xmppConnectionService.databaseBackend.getFrequentContacts(30); + final var accounts = + ImmutableMap.copyOf( + Maps.uniqueIndex(xmppConnectionService.getAccounts(), Account::getUuid)); + final var contactBuilder = new ImmutableMap.Builder(); + for (final var frequentContact : frequentContacts) { + final Account account = accounts.get(frequentContact.account); if (account != null) { - contacts.add(account.getRoster().getContact(frequentContact.contact)); + final var contact = account.getRoster().getContact(frequentContact.contact); + contactBuilder.put(frequentContact, contact); } } - ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); - boolean needsUpdate = forceUpdate || contactsChanged(contacts,shortcutManager.getDynamicShortcuts()); + final var contacts = contactBuilder.build(); + final var current = ShortcutManagerCompat.getDynamicShortcuts(xmppConnectionService); + boolean needsUpdate = forceUpdate || contactsChanged(contacts.values(), current); if (!needsUpdate) { - Log.d(Config.LOGTAG,"skipping shortcut update"); + Log.d(Config.LOGTAG, "skipping shortcut update"); return; } - List newDynamicShortCuts = new ArrayList<>(); - for (Contact contact : contacts) { - ShortcutInfo shortcut = getShortcutInfo(contact); - newDynamicShortCuts.add(shortcut); + final var newDynamicShortcuts = new ImmutableList.Builder(); + for (final var entry : contacts.entrySet()) { + final var contact = entry.getValue(); + final var conversation = entry.getKey().conversation; + final var shortcut = getShortcutInfo(contact, conversation); + newDynamicShortcuts.add(shortcut); } - if (shortcutManager.setDynamicShortcuts(newDynamicShortCuts)) { - Log.d(Config.LOGTAG,"updated dynamic shortcuts"); + if (ShortcutManagerCompat.setDynamicShortcuts( + xmppConnectionService, newDynamicShortcuts.build())) { + Log.d(Config.LOGTAG, "updated dynamic shortcuts"); } else { Log.d(Config.LOGTAG, "unable to update dynamic shortcuts"); } } - public ShortcutInfoCompat getShortcutInfoCompat(Contact contact) { - return new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact)) + public ShortcutInfoCompat getShortcutInfo(final Contact contact) { + final var conversation = xmppConnectionService.find(contact); + final var uuid = conversation == null ? null : conversation.getUuid(); + return getShortcutInfo(contact, uuid); + } + + public ShortcutInfoCompat getShortcutInfo(final Contact contact, final String conversation) { + final ShortcutInfoCompat.Builder builder = + new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(contact)) .setShortLabel(contact.getDisplayName()) .setIntent(getShortcutIntent(contact)) - .setIcon(IconCompat.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(contact))) - .setIsConversation() - .setCategories(Set.of("com.cheogram.android.SHARE_TARGET")) - .build(); + .setIsConversation(); + builder.setIcon( + IconCompat.createWithBitmap( + xmppConnectionService.getAvatarService().getRoundedShortcut(contact))); + if (conversation != null) { + setConversation(builder, conversation); + } + return builder.build(); } - public ShortcutInfoCompat getShortcutInfoCompat(final MucOptions mucOptions) { - return new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions)) + public ShortcutInfoCompat getShortcutInfo(final MucOptions mucOptions) { + final ShortcutInfoCompat.Builder builder = + new ShortcutInfoCompat.Builder(xmppConnectionService, getShortcutId(mucOptions)) .setShortLabel(mucOptions.getConversation().getName()) .setIntent(getShortcutIntent(mucOptions)) - .setIcon(IconCompat.createWithBitmap(xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions))) - .setIsConversation() - .setCategories(Set.of("com.cheogram.android.SHARE_TARGET")) - .build(); - } - - @TargetApi(Build.VERSION_CODES.N_MR1) - private ShortcutInfo getShortcutInfo(final Contact contact) { - return getShortcutInfoCompat(contact).toShortcutInfo(); - } - - private static boolean contactsChanged(List needles, List haystack) { - for(Contact needle : needles) { - if(!contactExists(needle,haystack)) { + .setIsConversation(); + builder.setIcon( + IconCompat.createWithBitmap( + xmppConnectionService.getAvatarService().getRoundedShortcut(mucOptions))); + setConversation(builder, mucOptions.getConversation().getUuid()); + return builder.build(); + } + + private static void setConversation( + final ShortcutInfoCompat.Builder builder, @NonNull final String conversation) { + builder.setCategories(ImmutableSet.of("eu.siacs.conversations.category.SHARE_TARGET")); + final var extras = new PersistableBundle(); + extras.putString(ConversationsActivity.EXTRA_CONVERSATION, conversation); + builder.setExtras(extras); + } + + private static boolean contactsChanged( + final Collection needles, final List haystack) { + for (final Contact needle : needles) { + if (!contactExists(needle, haystack)) { return true; } } @@ -129,17 +154,22 @@ public class ShortcutService { } @TargetApi(25) - private static boolean contactExists(Contact needle, List haystack) { - for(ShortcutInfo shortcutInfo : haystack) { - if (getShortcutId(needle).equals(shortcutInfo.getId()) && needle.getDisplayName().equals(shortcutInfo.getShortLabel())) { + private static boolean contactExists( + final Contact needle, final List haystack) { + for (final ShortcutInfoCompat shortcutInfo : haystack) { + final var label = shortcutInfo.getShortLabel(); + if (getShortcutId(needle).equals(shortcutInfo.getId()) + && needle.getDisplayName().equals(label.toString())) { return true; } } return false; } - private static String getShortcutId(Contact contact) { - return contact.getAccount().getJid().asBareJid().toEscapedString()+"#"+contact.getJid().asBareJid().toEscapedString(); + private static String getShortcutId(final Contact contact) { + return contact.getAccount().getJid().asBareJid().toEscapedString() + + "#" + + contact.getJid().asBareJid().toEscapedString(); } private static String getShortcutId(final MucOptions mucOptions) { @@ -180,12 +210,13 @@ public class ShortcutService { } @NonNull - public Intent createShortcut(Contact contact, boolean legacy) { + public Intent createShortcut(final Contact contact, final boolean legacy) { Intent intent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !legacy) { - ShortcutInfo shortcut = getShortcutInfo(contact); - ShortcutManager shortcutManager = xmppConnectionService.getSystemService(ShortcutManager.class); - intent = shortcutManager.createShortcutResultIntent(shortcut); + final var shortcut = getShortcutInfo(contact); + intent = + ShortcutManagerCompat.createShortcutResultIntent( + xmppConnectionService, shortcut); } else { intent = createShortcutResultIntent(contact); } @@ -193,7 +224,7 @@ public class ShortcutService { } @NonNull - private Intent createShortcutResultIntent(Contact contact) { + private Intent createShortcutResultIntent(final Contact contact) { AvatarService avatarService = xmppConnectionService.getAvatarService(); Bitmap icon = avatarService.getRoundedShortcutWithIcon(contact); Intent intent = new Intent(); @@ -204,13 +235,14 @@ public class ShortcutService { } public static class FrequentContact { + private final String conversation; private final String account; private final Jid contact; - public FrequentContact(String account, Jid contact) { + public FrequentContact(final String conversation, final String account, final Jid contact) { + this.conversation = conversation; this.account = account; this.contact = contact; } } - } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index fdd9e709123c5f37b6108df32c519dab9d012da0..6bd5f2ca28483e7932b87fb8968153fbdc218cb1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -5,7 +5,6 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.Manifest; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.KeyguardManager; import android.app.Notification; @@ -49,7 +48,6 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.LruCache; import android.util.Pair; - import androidx.annotation.BoolRes; import androidx.annotation.IntegerRes; import androidx.annotation.NonNull; @@ -64,8 +62,10 @@ import com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.io.Files; @@ -168,6 +168,7 @@ import eu.siacs.conversations.utils.EasyOnboardingInvite; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.FileUtils; import eu.siacs.conversations.utils.MessageUtils; +import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.QuickLoader; @@ -203,7 +204,38 @@ import eu.siacs.conversations.xmpp.mam.MamReference; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; import im.conversations.android.xmpp.model.stanza.Iq; +import java.io.File; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +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 me.leolin.shortcutbadger.ShortcutBadger; +import org.conscrypt.Conscrypt; +import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep; +import org.openintents.openpgp.IOpenPgpService2; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpServiceConnection; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; @@ -214,7 +246,8 @@ public class XmppConnectionService extends Service { public static final String ACTION_MARK_AS_READ = "mark_as_read"; public static final String ACTION_SNOOZE = "snooze"; public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification"; - public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification"; + public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = + "clear_missed_call_notification"; public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error"; public static final String ACTION_TRY_AGAIN = "try_again"; @@ -228,22 +261,30 @@ public class XmppConnectionService extends Service { public static final String ACTION_END_CALL = "end_call"; public static final String ACTION_STARTING_CALL = "starting_call"; public static final String ACTION_PROVISION_ACCOUNT = "provision_account"; - public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED = "call_integration_service_started"; - private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; - public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; + public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED = + "call_integration_service_started"; + private static final String ACTION_POST_CONNECTIVITY_CHANGE = + "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE"; + public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = + "eu.siacs.conversations.UNIFIED_PUSH_RENEW"; public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG"; private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp"; public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1); - private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); - private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); - - private final ScheduledExecutorService internalPingExecutor = Executors.newSingleThreadScheduledExecutor(); - private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression"); - private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter"); - private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader"); - private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor"); + private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor(); + public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor(); + + private final ScheduledExecutorService internalPingExecutor = + Executors.newSingleThreadScheduledExecutor(); + private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = + new SerialSingleThreadExecutor("VideoCompression"); + private final SerialSingleThreadExecutor mDatabaseWriterExecutor = + new SerialSingleThreadExecutor("DatabaseWriter"); + private final SerialSingleThreadExecutor mDatabaseReaderExecutor = + new SerialSingleThreadExecutor("DatabaseReader"); + private final SerialSingleThreadExecutor mNotificationExecutor = + new SerialSingleThreadExecutor("NotificationExecutor"); private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager(); private final IBinder mBinder = new XmppConnectionBinder(); private final List conversations = new CopyOnWriteArrayList<>(); @@ -251,15 +292,16 @@ 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 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, "received iq error: " + text); - } - } - }; + 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, "received iq error: " + text); + } + } + }; public DatabaseBackend databaseBackend; private Multimap mutedMucUsers; private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger"); @@ -273,70 +315,80 @@ public class XmppConnectionService extends Service { private MemorizingTrustManager mMemorizingTrustManager; private final NotificationService mNotificationService = new NotificationService(this); private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this); - private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this); + private final ChannelDiscoveryService mChannelDiscoveryService = + new ChannelDiscoveryService(this); private final ShortcutService mShortcutService = new ShortcutService(this); private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false); private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false); private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false); private final AtomicReference ongoingCall = new AtomicReference<>(); private final MessageGenerator mMessageGenerator = new MessageGenerator(this); - public OnContactStatusChanged onContactStatusChanged = (contact, online) -> { - Conversation conversation = find(getConversations(), contact); - if (conversation != null) { - if (online) { - if (contact.getPresences().size() == 1) { - sendUnsentMessages(conversation); + public OnContactStatusChanged onContactStatusChanged = + (contact, online) -> { + final var conversation = find(contact); + if (conversation == null) { + return; } - } - } - }; + if (online) { + if (contact.getPresences().size() == 1) { + sendUnsentMessages(conversation); + } + } + }; private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); private List accounts; - private final JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(this); + private final JingleConnectionManager mJingleConnectionManager = + new JingleConnectionManager(this); private final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this); private final AvatarService mAvatarService = new AvatarService(this); private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private final PushManagementService mPushManagementService = new PushManagementService(this); - private final QuickConversationsService mQuickConversationsService = new QuickConversationsService(this); - private final ConversationsFileObserver fileObserver = new ConversationsFileObserver( - Environment.getExternalStorageDirectory().getAbsolutePath() - ) { - @Override - public void onEvent(final int event, final File file) { - markFileDeleted(file); - } - }; - private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + private final QuickConversationsService mQuickConversationsService = + new QuickConversationsService(this); + private final ConversationsFileObserver fileObserver = + new ConversationsFileObserver( + Environment.getExternalStorageDirectory().getAbsolutePath()) { + @Override + public void onEvent(final int event, final File file) { + markFileDeleted(file); + } + }; + private final OnMessageAcknowledged mOnMessageAcknowledgedListener = + new OnMessageAcknowledged() { - @Override - public boolean onMessageAcknowledged(final Account account, final Jid to, final String id) { - if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { - final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length()); - mJingleConnectionManager.updateProposedSessionDiscovered( - account, - to, - sessionId, - JingleConnectionManager.DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED - ); - } - - - final Jid bare = to.asBareJid(); - - for (final Conversation conversation : getConversations()) { - if (conversation.getAccount() == account && conversation.getJid().asBareJid().equals(bare)) { - final Message message = conversation.findUnsentMessageWithUuid(id); - if (message != null) { - message.setStatus(Message.STATUS_SEND); - message.setErrorMessage(null); - databaseBackend.updateMessage(message, false); - return true; + @Override + public boolean onMessageAcknowledged( + final Account account, final Jid to, final String id) { + if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) { + final String sessionId = + id.substring( + JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX + .length()); + mJingleConnectionManager.updateProposedSessionDiscovered( + account, + to, + sessionId, + JingleConnectionManager.DeviceDiscoveryState + .SEARCHING_ACKNOWLEDGED); + } + + final Jid bare = to.asBareJid(); + + for (final Conversation conversation : getConversations()) { + if (conversation.getAccount() == account + && conversation.getJid().asBareJid().equals(bare)) { + final Message message = conversation.findUnsentMessageWithUuid(id); + if (message != null) { + message.setStatus(Message.STATUS_SEND); + message.setErrorMessage(null); + databaseBackend.updateMessage(message, false); + return true; + } + } } + return false; } - } - return false; - } - }; + }; private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false); @@ -348,136 +400,176 @@ public class XmppConnectionService extends Service { private int unreadCount = -1; - //Ui callback listeners - private final Set mOnConversationUpdates = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnShowErrorToasts = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnAccountUpdates = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnCaptchaRequested = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnRosterUpdates = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnUpdateBlocklist = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap()); - private final Set mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap()); - private final Set onJingleRtpConnectionUpdate = Collections.newSetFromMap(new WeakHashMap()); + // Ui callback listeners + private final Set mOnConversationUpdates = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnShowErrorToasts = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnAccountUpdates = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnCaptchaRequested = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnRosterUpdates = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnUpdateBlocklist = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnMucRosterUpdate = + Collections.newSetFromMap(new WeakHashMap()); + private final Set mOnKeyStatusUpdated = + Collections.newSetFromMap(new WeakHashMap()); + private final Set onJingleRtpConnectionUpdate = + Collections.newSetFromMap(new WeakHashMap()); private final Object LISTENER_LOCK = new Object(); - public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); - - private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private final LruCache, ServiceDiscoveryResult> discoCache = new LruCache<>(20); - private final OnStatusChanged statusListener = new OnStatusChanged() { - - @Override - public void onStatusChanged(final Account account) { - XmppConnection connection = account.getXmppConnection(); - updateAccountUi(); + private final LruCache, ServiceDiscoveryResult> discoCache = + new LruCache<>(20); + private final OnStatusChanged statusListener = + new OnStatusChanged() { - if (account.getStatus() == Account.State.ONLINE || account.getStatus().isError()) { - mQuickConversationsService.signalAccountStateChange(); - } + @Override + public void onStatusChanged(final Account account) { + XmppConnection connection = account.getXmppConnection(); + updateAccountUi(); - if (account.getStatus() == Account.State.ONLINE) { - synchronized (mLowPingTimeoutMode) { - if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode"); - } - } - if (account.setShowErrorNotification(true)) { - databaseBackend.updateAccount(account); - } - mMessageArchiveService.executePendingQueries(account); - if (connection != null && connection.getFeatures().csi()) { - if (checkListeners()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive"); - connection.sendInactive(); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//active"); - connection.sendActive(); - } - } - List conversations = getConversations(); - for (Conversation conversation : conversations) { - final boolean inProgressJoin; - synchronized (account.inProgressConferenceJoins) { - inProgressJoin = account.inProgressConferenceJoins.contains(conversation); + if (account.getStatus() == Account.State.ONLINE + || account.getStatus().isError()) { + mQuickConversationsService.signalAccountStateChange(); } - final boolean pendingJoin; - synchronized (account.pendingConferenceJoins) { - pendingJoin = account.pendingConferenceJoins.contains(conversation); - } - if (conversation.getAccount() == account - && !pendingJoin - && !inProgressJoin) { - sendUnsentMessages(conversation); - } - } - final List pendingLeaves; - synchronized (account.pendingConferenceLeaves) { - pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); - account.pendingConferenceLeaves.clear(); - } - for (Conversation conversation : pendingLeaves) { - leaveMuc(conversation); - } - final List pendingJoins; - synchronized (account.pendingConferenceJoins) { - pendingJoins = new ArrayList<>(account.pendingConferenceJoins); - account.pendingConferenceJoins.clear(); - } - for (Conversation conversation : pendingJoins) { - joinMuc(conversation); - } - fetchMamPreferences(account, null); - scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); - } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED || account.getStatus() == Account.State.LOGGED_OUT) { - resetSendingToWaiting(account); - if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now"); - reconnectAccount(account, true, false); - } else { - final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; - scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); - } - } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { - databaseBackend.updateAccount(account); - reconnectAccount(account, true, false); - } else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) { - resetSendingToWaiting(account); - if (connection != null && account.getStatus().isAttemptReconnect()) { - final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST - || hasJingleRtpConnection(account); - final int next = connection.getTimeToNextAttempt(aggressive); - final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account); - if (next <= 0) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. reconnecting now. lowPingTimeout=" + lowPingTimeoutMode); + if (account.getStatus() == Account.State.ONLINE) { + synchronized (mLowPingTimeoutMode) { + if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": leaving low ping timeout mode"); + } + } + if (account.setShowErrorNotification(true)) { + databaseBackend.updateAccount(account); + } + mMessageArchiveService.executePendingQueries(account); + if (connection != null && connection.getFeatures().csi()) { + if (checkListeners()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + " sending csi//inactive"); + connection.sendInactive(); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + " sending csi//active"); + connection.sendActive(); + } + } + List conversations = getConversations(); + for (Conversation conversation : conversations) { + final boolean inProgressJoin; + synchronized (account.inProgressConferenceJoins) { + inProgressJoin = + account.inProgressConferenceJoins.contains(conversation); + } + final boolean pendingJoin; + synchronized (account.pendingConferenceJoins) { + pendingJoin = account.pendingConferenceJoins.contains(conversation); + } + if (conversation.getAccount() == account + && !pendingJoin + && !inProgressJoin) { + sendUnsentMessages(conversation); + } + } + final List pendingLeaves; + synchronized (account.pendingConferenceLeaves) { + pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves); + account.pendingConferenceLeaves.clear(); + } + for (Conversation conversation : pendingLeaves) { + leaveMuc(conversation); + } + final List pendingJoins; + synchronized (account.pendingConferenceJoins) { + pendingJoins = new ArrayList<>(account.pendingConferenceJoins); + account.pendingConferenceJoins.clear(); + } + for (Conversation conversation : pendingJoins) { + joinMuc(conversation); + } + scheduleWakeUpCall( + Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode()); + } else if (account.getStatus() == Account.State.OFFLINE + || account.getStatus() == Account.State.DISABLED + || account.getStatus() == Account.State.LOGGED_OUT) { + resetSendingToWaiting(account); + if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": went into offline state during low ping mode." + + " reconnecting now"); + reconnectAccount(account, true, false); + } else { + final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2; + scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); + } + } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { + databaseBackend.updateAccount(account); reconnectAccount(account, true, false); - } else { - final int attempt = connection.getAttempt() + 1; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode+", aggressive="+aggressive); - scheduleWakeUpCall(next, account.getUuid().hashCode()); - if (aggressive) { - internalPingExecutor.schedule( - XmppConnectionService.this::manageAccountConnectionStatesInternal, - (next * 1000L) + 50, - TimeUnit.MILLISECONDS - ); + } else if (account.getStatus() != Account.State.CONNECTING + && account.getStatus() != Account.State.NO_INTERNET) { + resetSendingToWaiting(account); + if (connection != null && account.getStatus().isAttemptReconnect()) { + final boolean aggressive = + account.getStatus() == Account.State.SEE_OTHER_HOST + || hasJingleRtpConnection(account); + final int next = connection.getTimeToNextAttempt(aggressive); + final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account); + if (next <= 0) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error connecting account. reconnecting now." + + " lowPingTimeout=" + + lowPingTimeoutMode); + reconnectAccount(account, true, false); + } else { + final int attempt = connection.getAttempt() + 1; + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error connecting account. try again in " + + next + + "s for the " + + attempt + + " time. lowPingTimeout=" + + lowPingTimeoutMode + + ", aggressive=" + + aggressive); + scheduleWakeUpCall(next, account.getUuid().hashCode()); + if (aggressive) { + internalPingExecutor.schedule( + XmppConnectionService.this + ::manageAccountConnectionStatesInternal, + (next * 1000L) + 50, + TimeUnit.MILLISECONDS); + } + } } } + getNotificationService().updateErrorNotification(); } - } - getNotificationService().updateErrorNotification(); - } - }; + }; private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; private WakeLock wakeLock; private LruCache mDrawableCache; private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver(); - private final BroadcastReceiver mInternalRestrictedEventReceiver = new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS)); + private final BroadcastReceiver mInternalRestrictedEventReceiver = + new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS)); private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver(); private EmojiSearch emojiSearch = null; @@ -510,15 +602,16 @@ public class XmppConnectionService extends Service { return null; } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { if (this.mPgpEngine == null) { - this.mPgpEngine = new PgpEngine(new OpenPgpApi( - getApplicationContext(), - pgpServiceConnection.getService()), this); + this.mPgpEngine = + new PgpEngine( + new OpenPgpApi( + getApplicationContext(), pgpServiceConnection.getService()), + this); } return mPgpEngine; } else { return null; } - } public OpenPgpApi getOpenPgpApi() { @@ -617,7 +710,8 @@ public class XmppConnectionService extends Service { return this.mAvatarService; } - public void attachLocationToConversation(final Conversation conversation, final Uri uri, final String subject, final UiCallback callback) { + public void attachLocationToConversation( + final Conversation conversation, final Uri uri, final String subject, final UiCallback callback) { int encryption = conversation.getNextEncryption(); if (encryption == Message.ENCRYPTION_PGP) { encryption = Message.ENCRYPTION_DECRYPTED; @@ -634,7 +728,12 @@ public class XmppConnectionService extends Service { } } - public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final String subject, final UiCallback callback) { + public void attachFileToConversation( + final Conversation conversation, + final Uri uri, + final String type, + final String subject, + final UiCallback callback) { final Message message; if (conversation.getReplyTo() == null) { message = new Message(conversation, "", conversation.getNextEncryption()); @@ -653,7 +752,8 @@ public class XmppConnectionService extends Service { } Log.d(Config.LOGTAG, "attachFile: type=" + message.getType()); Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart()); - final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback); + final AttachFileToConversationRunnable runnable = + new AttachFileToConversationRunnable(this, uri, type, message, callback); if (runnable.isVideoMessage()) { VIDEO_COMPRESSION_EXECUTOR.execute(runnable); } else { @@ -661,7 +761,12 @@ public class XmppConnectionService extends Service { } } - public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final String subject, final UiCallback callback) { + public void attachImageToConversation( + final Conversation conversation, + final Uri uri, + final String type, + final String subject, + final UiCallback callback) { final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type); final String compressPictures = getCompressPicturesPreference(); @@ -798,7 +903,10 @@ public class XmppConnectionService extends Service { return c != null && c.getMode() == Conversational.MODE_MULTI; } - public void search(final List term, final String uuid, final OnSearchResultsAvailable onSearchResultsAvailable) { + public void search( + final List term, + final String uuid, + final OnSearchResultsAvailable onSearchResultsAvailable) { MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable); } @@ -807,9 +915,16 @@ public class XmppConnectionService extends Service { final var nomedia = getBooleanPreference("nomedia", R.bool.default_nomedia); fileBackend.setupNomedia(nomedia); final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction()); - final boolean needsForegroundService = intent != null && intent.getBooleanExtra(SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); + final boolean needsForegroundService = + intent != null + && intent.getBooleanExtra( + SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false); if (needsForegroundService) { - Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")"); + Log.d( + Config.LOGTAG, + "toggle forced foreground service after receiving event (action=" + + action + + ")"); toggleForegroundService(true, action.equals(ACTION_STARTING_CALL)); } final String uuid = intent == null ? null : intent.getStringExtra("uuid"); @@ -832,75 +947,93 @@ public class XmppConnectionService extends Service { logoutAndSave(true); return START_NOT_STICKY; case ACTION_CLEAR_MESSAGE_NOTIFICATION: - mNotificationExecutor.execute(() -> { - try { - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - mNotificationService.clearMessages(c); - } else { - mNotificationService.clearMessages(); - } - restoredFromDatabaseLatch.await(); + mNotificationExecutor.execute( + () -> { + try { + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + mNotificationService.clearMessages(c); + } else { + mNotificationService.clearMessages(); + } + restoredFromDatabaseLatch.await(); - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear message notification"); - } - }); + } catch (InterruptedException e) { + Log.d( + Config.LOGTAG, + "unable to process clear message notification"); + } + }); break; case ACTION_CLEAR_MISSED_CALL_NOTIFICATION: - mNotificationExecutor.execute(() -> { - try { - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - mNotificationService.clearMissedCalls(c); - } else { - mNotificationService.clearMissedCalls(); - } - restoredFromDatabaseLatch.await(); + mNotificationExecutor.execute( + () -> { + try { + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + mNotificationService.clearMissedCalls(c); + } else { + mNotificationService.clearMissedCalls(); + } + restoredFromDatabaseLatch.await(); - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process clear missed call notification"); - } - }); + } catch (InterruptedException e) { + Log.d( + Config.LOGTAG, + "unable to process clear missed call notification"); + } + }); break; - case ACTION_DISMISS_CALL: { - if (intent == null) { + case ACTION_DISMISS_CALL: + { + if (intent == null) { + break; + } + final String sessionId = + intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); + Log.d( + Config.LOGTAG, + "received intent to dismiss call with session id " + sessionId); + mJingleConnectionManager.rejectRtpSession(sessionId); break; } - final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId); - mJingleConnectionManager.rejectRtpSession(sessionId); - break; - } case TorServiceUtils.ACTION_STATUS: - final String status = intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS); - //TODO port and host are in 'extras' - but this may not be a reliable source? + final String status = + intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS); + // TODO port and host are in 'extras' - but this may not be a reliable source? if ("ON".equals(status)) { handleOrbotStartedEvent(); return START_STICKY; } break; - case ACTION_END_CALL: { - if (intent == null) { - break; - } - final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); - Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId); - mJingleConnectionManager.endRtpSession(sessionId); - } - break; - case ACTION_PROVISION_ACCOUNT: { - if (intent == null) { - break; + case ACTION_END_CALL: + { + if (intent == null) { + break; + } + final String sessionId = + intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID); + Log.d( + Config.LOGTAG, + "received intent to end call with session id " + sessionId); + mJingleConnectionManager.endRtpSession(sessionId); } - final String address = intent.getStringExtra("address"); - final String password = intent.getStringExtra("password"); - if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) { + break; + case ACTION_PROVISION_ACCOUNT: + { + if (intent == null) { + break; + } + final String address = intent.getStringExtra("address"); + final String password = intent.getStringExtra("password"); + if (QuickConversationsService.isQuicksy() + || Strings.isNullOrEmpty(address) + || Strings.isNullOrEmpty(password)) { + break; + } + provisionAccount(address, password); break; } - provisionAccount(address, password); - break; - } case ACTION_DISMISS_ERROR_NOTIFICATIONS: dismissErrorNotifications(); break; @@ -908,55 +1041,75 @@ public class XmppConnectionService extends Service { resetAllAttemptCounts(false, true); break; case ACTION_REPLY_TO_CONVERSATION: - final Bundle remoteInput = intent == null ? null : RemoteInput.getResultsFromIntent(intent); + final Bundle remoteInput = + intent == null ? null : RemoteInput.getResultsFromIntent(intent); if (remoteInput == null) { break; } final CharSequence body = remoteInput.getCharSequence("text_reply"); - final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false); + final boolean dismissNotification = + intent.getBooleanExtra("dismiss_notification", false); final String lastMessageUuid = intent.getStringExtra("last_message_uuid"); if (body == null || body.length() <= 0) { break; } - mNotificationExecutor.execute(() -> { - try { - restoredFromDatabaseLatch.await(); - final Conversation c = findConversationByUuid(uuid); - if (c != null) { - directReply(c, body.toString(), lastMessageUuid, dismissNotification); - } - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process direct reply"); - } - }); + mNotificationExecutor.execute( + () -> { + try { + restoredFromDatabaseLatch.await(); + final Conversation c = findConversationByUuid(uuid); + if (c != null) { + directReply( + c, + body.toString(), + lastMessageUuid, + dismissNotification); + } + } catch (InterruptedException e) { + Log.d(Config.LOGTAG, "unable to process direct reply"); + } + }); break; case ACTION_MARK_AS_READ: - mNotificationExecutor.execute(() -> { - final Conversation c = findConversationByUuid(uuid); - if (c == null) { - Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")"); - return; - } - try { - restoredFromDatabaseLatch.await(); - sendReadMarker(c, null); - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName()); - } - - }); + mNotificationExecutor.execute( + () -> { + final Conversation c = findConversationByUuid(uuid); + if (c == null) { + Log.d( + Config.LOGTAG, + "received mark read intent for unknown conversation (" + + uuid + + ")"); + return; + } + try { + restoredFromDatabaseLatch.await(); + sendReadMarker(c, null); + } catch (InterruptedException e) { + Log.d( + Config.LOGTAG, + "unable to process notification read marker for" + + " conversation " + + c.getName()); + } + }); break; case ACTION_SNOOZE: - mNotificationExecutor.execute(() -> { - final Conversation c = findConversationByUuid(uuid); - if (c == null) { - Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")"); - return; - } - c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); - mNotificationService.clearMessages(c); - updateConversation(c); - }); + mNotificationExecutor.execute( + () -> { + final Conversation c = findConversationByUuid(uuid); + if (c == null) { + Log.d( + Config.LOGTAG, + "received snooze intent for unknown conversation (" + + uuid + + ")"); + return; + } + c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000); + mNotificationService.clearMessages(c); + updateConversation(c); + }); case AudioManager.RINGER_MODE_CHANGED_ACTION: case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED: if (dndOnSilentMode()) { @@ -983,12 +1136,16 @@ public class XmppConnectionService extends Service { final Messenger messenger = intent.getParcelableExtra("messenger"); final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger; if (messenger != null && application != null && instance != null) { - pushTargetMessenger = new UnifiedPushBroker.PushTargetMessenger(new UnifiedPushDatabase.PushTarget(application, instance),messenger); - Log.d(Config.LOGTAG,"found push target messenger"); + pushTargetMessenger = + new UnifiedPushBroker.PushTargetMessenger( + new UnifiedPushDatabase.PushTarget(application, instance), + messenger); + Log.d(Config.LOGTAG, "found push target messenger"); } else { pushTargetMessenger = null; } - final Optional transport = renewUnifiedPushEndpoints(pushTargetMessenger); + final Optional transport = + renewUnifiedPushEndpoints(pushTargetMessenger); if (instance != null && transport.isPresent()) { unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get()); } @@ -1078,16 +1235,17 @@ public class XmppConnectionService extends Service { } if (pingNow) { for (final Account account : pingCandidates) { + final var connection = account.getXmppConnection(); final boolean lowTimeout = isInLowPingTimeoutMode(account); - account.getXmppConnection().sendPing(); + final var delta = + (SystemClock.elapsedRealtime() - connection.getLastPacketReceived()) + / 1000L; + connection.sendPing(); Log.d( Config.LOGTAG, - account.getJid().asBareJid() - + " send ping (action=" - + action - + ",lowTimeout=" - + lowTimeout - + ")"); + String.format( + "%s: send ping (action=%s,lowTimeout=%s,interval=%s)", + account.getJid().asBareJid(), action, lowTimeout, delta)); scheduleWakeUpCall( lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode()); @@ -1138,10 +1296,16 @@ public class XmppConnectionService extends Service { } } - private boolean processAccountState(final Account account, final boolean interactive, final boolean isUiAction, final boolean isAccountPushed, final HashSet pingCandidates) { + private boolean processAccountState( + final Account account, + final boolean interactive, + final boolean isUiAction, + final boolean isAccountPushed, + final HashSet pingCandidates) { if (!account.getStatus().isAttemptReconnect()) { return false; } + final var requestCode = account.getUuid().hashCode(); if (!hasInternetConnection()) { account.setStatus(Account.State.NO_INTERNET); statusListener.onStatusChanged(account); @@ -1154,31 +1318,44 @@ public class XmppConnectionService extends Service { synchronized (mLowPingTimeoutMode) { long lastReceived = account.getXmppConnection().getLastPacketReceived(); long lastSent = account.getXmppConnection().getLastPingSent(); - long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000; - long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime(); - int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000; + long pingInterval = + isUiAction + ? Config.PING_MIN_INTERVAL * 1000 + : Config.PING_MAX_INTERVAL * 1000; + long msToNextPing = + (Math.max(lastReceived, lastSent) + pingInterval) + - SystemClock.elapsedRealtime(); + int pingTimeout = + mLowPingTimeoutMode.contains(account.getJid().asBareJid()) + ? Config.LOW_PING_TIMEOUT * 1000 + : Config.PING_TIMEOUT * 1000; long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime(); if (lastSent > lastReceived) { if (pingTimeoutIn < 0) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout"); this.reconnectAccount(account, true, interactive); } else { - int secs = (int) (pingTimeoutIn / 1000); - this.scheduleWakeUpCall(secs, account.getUuid().hashCode()); + this.scheduleWakeUpCall(pingTimeoutIn, requestCode); } } else { pingCandidates.add(account); if (isAccountPushed) { if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": entering low ping timeout mode"); } return true; } else if (msToNextPing <= 0) { return true; } else { - this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); + this.scheduleWakeUpCall(msToNextPing, requestCode); if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": leaving low ping timeout mode"); } } } @@ -1186,23 +1363,23 @@ public class XmppConnectionService extends Service { } else if (account.getStatus() == Account.State.OFFLINE) { reconnectAccount(account, true, interactive); } else if (account.getStatus() == Account.State.CONNECTING) { - long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; - long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000; - long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco; - long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect; - if (!areMessagesInitialized()) return false; // No point in thrashing a reconnect while still loading - if (timeout < 0) { - Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")"); - account.getXmppConnection().resetAttemptCount(false); - reconnectAccount(account, true, interactive); + final var connection = account.getXmppConnection(); + final var connectionDuration = connection.getConnectionDuration(); + final var discoDuration = connection.getDiscoDuration(); + final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration; + final var discoTimeout = Config.CONNECT_DISCO_TIMEOUT * 1000L - discoDuration; + if (connectionTimeout < 0) { + connection.triggerConnectionTimeout(); } else if (discoTimeout < 0) { - account.getXmppConnection().sendDiscoTimeout(); - scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode()); + connection.sendDiscoTimeout(); + scheduleWakeUpCall(discoTimeout, requestCode); } else { - scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode()); + scheduleWakeUpCall(Math.min(connectionTimeout, discoTimeout), requestCode); } } else { - final boolean aggressive = account.getStatus() == Account.State.SEE_OTHER_HOST || hasJingleRtpConnection(account); + final boolean aggressive = + account.getStatus() == Account.State.SEE_OTHER_HOST + || hasJingleRtpConnection(account); if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) { reconnectAccount(account, true, interactive); } @@ -1212,7 +1389,7 @@ public class XmppConnectionService extends Service { } private void toggleSoftDisabled(final boolean softDisabled) { - for(final Account account : this.accounts) { + for (final Account account : this.accounts) { if (account.isEnabled()) { if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) { updateAccount(account); @@ -1221,7 +1398,8 @@ public class XmppConnectionService extends Service { } } - public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) { + public boolean processUnifiedPushMessage( + final Account account, final Jid transport, final Element push) { return unifiedPushBroker.processPushMessage(account, transport, push); } @@ -1229,8 +1407,13 @@ public class XmppConnectionService extends Service { mChannelDiscoveryService.initializeMuclumbusService(); } - public void discoverChannels(String query, ChannelDiscoveryService.Method method, Map mucServices, ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) { - mChannelDiscoveryService.discover(Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound); + public void discoverChannels( + String query, + ChannelDiscoveryService.Method method, + Map mucServices, + ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) { + mChannelDiscoveryService.discover( + Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound); } public boolean isDataSaverDisabled() { @@ -1266,26 +1449,25 @@ public class XmppConnectionService extends Service { } message.markUnread(); if (message.getEncryption() == Message.ENCRYPTION_PGP) { - getPgpEngine().encrypt(message, new UiCallback() { - @Override - public void success(Message message) { - if (dismissAfterReply) { - markRead((Conversation) message.getConversation(), true); - } else { - mNotificationService.pushFromDirectReply(message); - } - } - - @Override - public void error(int errorCode, Message object) { - - } + getPgpEngine() + .encrypt( + message, + new UiCallback() { + @Override + public void success(Message message) { + if (dismissAfterReply) { + markRead((Conversation) message.getConversation(), true); + } else { + mNotificationService.pushFromDirectReply(message); + } + } - @Override - public void userInputRequired(PendingIntent pi, Message object) { + @Override + public void error(int errorCode, Message object) {} - } - }); + @Override + public void userInputRequired(PendingIntent pi, Message object) {} + }); } else { sendMessage(message); if (dismissAfterReply) { @@ -1301,19 +1483,25 @@ public class XmppConnectionService extends Service { } private boolean manuallyChangePresence() { - return getBooleanPreference(AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); + return getBooleanPreference( + AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); } private boolean treatVibrateAsSilent() { - return getBooleanPreference(AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent); + return getBooleanPreference( + AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent); } private boolean awayWhenScreenLocked() { - return getBooleanPreference(AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off); + return getBooleanPreference( + AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off); } private String getCompressPicturesPreference() { - return getPreferences().getString("picture_compression", getResources().getString(R.string.picture_compression)); + return getPreferences() + .getString( + "picture_compression", + getResources().getString(R.string.picture_compression)); } private Presence.Status getTargetPresence() { @@ -1341,10 +1529,16 @@ public class XmppConnectionService extends Service { private boolean isPhoneSilenced() { final NotificationManager notificationManager = getSystemService(NotificationManager.class); - final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter(); + final int filter = + notificationManager == null + ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN + : notificationManager.getCurrentInterruptionFilter(); final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY; final AudioManager audioManager = getSystemService(AudioManager.class); - final int ringerMode = audioManager == null ? AudioManager.RINGER_MODE_NORMAL : audioManager.getRingerMode(); + final int ringerMode = + audioManager == null + ? AudioManager.RINGER_MODE_NORMAL + : audioManager.getRingerMode(); try { if (treatVibrateAsSilent()) { return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL; @@ -1352,7 +1546,9 @@ public class XmppConnectionService extends Service { return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT; } } catch (final Throwable throwable) { - Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")"); + Log.d( + Config.LOGTAG, + "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")"); return notificationDnd; } } @@ -1376,7 +1572,9 @@ public class XmppConnectionService extends Service { private void dismissErrorNotifications() { for (final Account account : this.accounts) { if (account.hasErrorStatus()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": dismissing error notification"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": dismissing error notification"); if (account.setShowErrorNotification(false)) { mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account)); } @@ -1390,41 +1588,50 @@ public class XmppConnectionService extends Service { public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) { mLastExpiryRun.set(SystemClock.elapsedRealtime()); - mDatabaseWriterExecutor.execute(() -> { - long timestamp = getAutomaticMessageDeletionDate(); - if (timestamp > 0) { - databaseBackend.expireOldMessages(timestamp); - synchronized (XmppConnectionService.this.conversations) { - for (Conversation conversation : XmppConnectionService.this.conversations) { - conversation.expireOldMessages(timestamp); - if (resetHasMessagesLeftOnServer) { - conversation.messagesLoaded.set(true); - conversation.setHasMessagesLeftOnServer(true); + mDatabaseWriterExecutor.execute( + () -> { + long timestamp = getAutomaticMessageDeletionDate(); + if (timestamp > 0) { + databaseBackend.expireOldMessages(timestamp); + synchronized (XmppConnectionService.this.conversations) { + for (Conversation conversation : + XmppConnectionService.this.conversations) { + conversation.expireOldMessages(timestamp); + if (resetHasMessagesLeftOnServer) { + conversation.messagesLoaded.set(true); + conversation.setHasMessagesLeftOnServer(true); + } + } } + updateConversationUi(); } - } - updateConversationUi(); - } - }); + }); } public boolean hasInternetConnection() { - final ConnectivityManager cm = ContextCompat.getSystemService(this, ConnectivityManager.class); + final ConnectivityManager cm = + ContextCompat.getSystemService(this, ConnectivityManager.class); if (cm == null) { - return true; //if internet connection can not be checked it is probably best to just try + return true; // if internet connection can not be checked it is probably best to just + // try } try { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { final Network activeNetwork = cm.getActiveNetwork(); - final NetworkCapabilities capabilities = activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork); - return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + final NetworkCapabilities capabilities = + activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork); + return capabilities != null + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); } else { final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - return networkInfo != null && (networkInfo.isConnected() || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET); + return networkInfo != null + && (networkInfo.isConnected() + || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET); } } catch (final RuntimeException e) { Log.d(Config.LOGTAG, "unable to check for internet connection", e); - return true; //if internet connection can not be checked it is probably best to just try + return true; // if internet connection can not be checked it is probably best to just + // try } } @@ -1470,7 +1677,8 @@ public class XmppConnectionService extends Service { } }; if (mLastActivity == 0) { - mLastActivity = getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis()); + mLastActivity = + getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis()); } Log.d(Config.LOGTAG, "initializing database..."); @@ -1506,22 +1714,30 @@ public class XmppConnectionService extends Service { FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles); } if (Config.supportOpenPgp()) { - this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() { - @Override - public void onBound(final IOpenPgpService2 service) { - for (Account account : accounts) { - final PgpDecryptionService pgp = account.getPgpDecryptionService(); - if (pgp != null) { - pgp.continueDecryption(true); - } - } - } + this.pgpServiceConnection = + new OpenPgpServiceConnection( + this, + "org.sufficientlysecure.keychain", + new OpenPgpServiceConnection.OnBound() { + @Override + public void onBound(final IOpenPgpService2 service) { + for (Account account : accounts) { + final PgpDecryptionService pgp = + account.getPgpDecryptionService(); + if (pgp != null) { + pgp.continueDecryption(true); + } + } + } - @Override - public void onError(final Exception exception) { - Log.e(Config.LOGTAG,"could not bind to OpenKeyChain", exception); - } - }); + @Override + public void onError(final Exception exception) { + Log.e( + Config.LOGTAG, + "could not bind to OpenKeyChain", + exception); + } + }); this.pgpServiceConnection.bindToService(); } @@ -1557,32 +1773,39 @@ public class XmppConnectionService extends Service { toggleForegroundService(); rescanStickers(); cleanupCache(); - internalPingExecutor.scheduleAtFixedRate(this::manageAccountConnectionStatesInternal,10,10,TimeUnit.SECONDS); + internalPingExecutor.scheduleWithFixedDelay( + this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS); final SharedPreferences sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(this); - sharedPreferences.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, @Nullable String key) { - Log.d(Config.LOGTAG,"preference '"+key+"' has changed"); - if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) { - toggleForegroundService(); - } - } - }); + sharedPreferences.registerOnSharedPreferenceChangeListener( + new SharedPreferences.OnSharedPreferenceChangeListener() { + @Override + public void onSharedPreferenceChanged( + SharedPreferences sharedPreferences, @Nullable String key) { + Log.d(Config.LOGTAG, "preference '" + key + "' has changed"); + if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) { + toggleForegroundService(); + } + } + }); } - private void checkForDeletedFiles() { if (destroyed) { - Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed"); + Log.d( + Config.LOGTAG, + "Do not check for deleted files because service has been destroyed"); return; } final long start = SystemClock.elapsedRealtime(); - final List relativeFilePaths = databaseBackend.getFilePathInfo(); + final List relativeFilePaths = + databaseBackend.getFilePathInfo(); final List changed = new ArrayList<>(); for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) { if (destroyed) { - Log.d(Config.LOGTAG, "Stop checking for deleted files because service has been destroyed"); + Log.d( + Config.LOGTAG, + "Stop checking for deleted files because service has been destroyed"); return; } final File file = fileBackend.getFileForPath(filePath.path); @@ -1591,7 +1814,15 @@ public class XmppConnectionService extends Service { } } final long duration = SystemClock.elapsedRealtime() - start; - Log.d(Config.LOGTAG, "found " + changed.size() + " changed files on start up. total=" + relativeFilePaths.size() + ". (" + duration + "ms)"); + Log.d( + Config.LOGTAG, + "found " + + changed.size() + + " changed files on start up. total=" + + relativeFilePaths.size() + + ". (" + + duration + + "ms)"); if (changed.size() > 0) { databaseBackend.markFilesAsChanged(changed); markChangedFiles(changed); @@ -1599,15 +1830,19 @@ public class XmppConnectionService extends Service { } public void startContactObserver() { - getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, new ContentObserver(null) { - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - if (restoredFromDatabaseLatch.getCount() == 0) { - loadPhoneContacts(); - } - } - }); + getContentResolver() + .registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, + true, + new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + if (restoredFromDatabaseLatch.getCount() == 0) { + loadPhoneContacts(); + } + } + }); } @Override @@ -1626,7 +1861,7 @@ public class XmppConnectionService extends Service { unregisterReceiver(this.mInternalRestrictedEventReceiver); unregisterReceiver(this.mInternalScreenEventReceiver); } catch (final IllegalArgumentException e) { - //ignored + // ignored } destroyed = false; fileObserver.stopWatching(); @@ -1651,7 +1886,7 @@ public class XmppConnectionService extends Service { try { unregisterReceiver(this.mInternalScreenEventReceiver); } catch (IllegalArgumentException e) { - //ignored + // ignored } } } @@ -1660,7 +1895,8 @@ public class XmppConnectionService extends Service { toggleForegroundService(false, false); } - public void setOngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { + public void setOngoingCall( + AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { ongoingCall.set(new OngoingCall(id, media, reconnecting)); toggleForegroundService(false, true); } @@ -1753,13 +1989,18 @@ public class XmppConnectionService extends Service { } public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() { - return !mOngoingVideoTranscoding.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts(); + return !mOngoingVideoTranscoding.get() + && ongoingCall.get() == null + && Compatibility.keepForegroundService(this) + && hasEnabledAccounts(); } @Override public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); - if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mOngoingVideoTranscoding.get() || ongoingCall.get() != null) { + if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) + || mOngoingVideoTranscoding.get() + || ongoingCall.get() != null) { Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated"); } else { this.logoutAndSave(false); @@ -1788,17 +2029,27 @@ public class XmppConnectionService extends Service { if (alarmManager == null) { return; } - final long triggerAtMillis = SystemClock.elapsedRealtime() + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000); + final long triggerAtMillis = + SystemClock.elapsedRealtime() + + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000); final Intent intent = new Intent(this, SystemEventReceiver.class); intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE); try { - final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast( + this, + 1, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); + alarmManager.setAndAllowWhileIdle( + AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); } else { - alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent); } } catch (RuntimeException e) { Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e); @@ -1806,11 +2057,12 @@ public class XmppConnectionService extends Service { } public void scheduleWakeUpCall(final int seconds, final int requestCode) { - final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L; - final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (alarmManager == null) { - return; - } + scheduleWakeUpCall((seconds < 0 ? 1 : seconds + 1) * 1000L, requestCode); + } + + public void scheduleWakeUpCall(final long milliSeconds, final int requestCode) { + final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds; + final var alarmManager = getSystemService(AlarmManager.class); final Intent intent = new Intent(this, SystemEventReceiver.class); intent.setAction(ACTION_PING); try { @@ -1818,12 +2070,11 @@ public class XmppConnectionService extends Service { PendingIntent.getBroadcast( this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE); alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); - } catch (RuntimeException e) { + } catch (final RuntimeException e) { Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e); } } - @TargetApi(Build.VERSION_CODES.M) private void scheduleNextIdlePing() { long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000; final var now = System.currentTimeMillis(); @@ -1840,10 +2091,17 @@ public class XmppConnectionService extends Service { final Intent intent = new Intent(this, SystemEventReceiver.class); intent.setAction(ACTION_IDLE_PING); try { - final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s() - ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT); - alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); + final PendingIntent pendingIntent = + PendingIntent.getBroadcast( + this, + 0, + intent, + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.setAndAllowWhileIdle( + AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent); } catch (RuntimeException e) { Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e); } @@ -1899,10 +2157,16 @@ public class XmppConnectionService extends Service { final Conversation conversation = (Conversation) message.getConversation(); account.deactivateGracePeriod(); - if (QuickConversationsService.isQuicksy() && conversation.getMode() == Conversation.MODE_SINGLE) { + if (QuickConversationsService.isQuicksy() + && conversation.getMode() == Conversation.MODE_SINGLE) { final Contact contact = conversation.getContact(); if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": adding " + contact.getJid() + " on sending message"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": adding " + + contact.getJid() + + " on sending message"); createContact(contact, true); } } @@ -1912,8 +2176,11 @@ public class XmppConnectionService extends Service { boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); - if (message.getEncryption() != Message.ENCRYPTION_NONE && conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous()) { - if (conversation.setAttribute(Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) { + if (message.getEncryption() != Message.ENCRYPTION_NONE + && conversation.getMode() == Conversation.MODE_MULTI + && conversation.isPrivateAndNonAnonymous()) { + if (conversation.setAttribute( + Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) { databaseBackend.updateConversation(conversation); } } @@ -2043,7 +2310,8 @@ public class XmppConnectionService extends Service { switch (message.getEncryption()) { case Message.ENCRYPTION_NONE: if (message.needsUploading()) { - if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize()) + if (account.httpUploadAvailable( + fileBackend.getFile(message, false).getSize()) || conversation.getMode() == Conversation.MODE_MULTI || message.fixCounterpart()) { this.sendFileMessage(message, delay, cb); @@ -2058,7 +2326,8 @@ public class XmppConnectionService extends Service { case Message.ENCRYPTION_PGP: case Message.ENCRYPTION_DECRYPTED: if (message.needsUploading()) { - if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize()) + if (account.httpUploadAvailable( + fileBackend.getFile(message, false).getSize()) || conversation.getMode() == Conversation.MODE_MULTI || message.fixCounterpart()) { this.sendFileMessage(message, delay, cb); @@ -2073,7 +2342,8 @@ public class XmppConnectionService extends Service { case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); if (message.needsUploading()) { - if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize()) + if (account.httpUploadAvailable( + fileBackend.getFile(message, false).getSize()) || conversation.getMode() == Conversation.MODE_MULTI || message.fixCounterpart()) { this.sendFileMessage(message, delay, cb); @@ -2082,7 +2352,8 @@ public class XmppConnectionService extends Service { break; } } else { - XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message); + XmppAxolotlMessage axolotlMessage = + account.getAxolotlService().fetchAxolotlMessageFromCache(message); if (axolotlMessage == null) { account.getAxolotlService().preparePayloadMessage(message, delay); } else { @@ -2090,11 +2361,11 @@ public class XmppConnectionService extends Service { } } break; - } if (packet != null) { if (account.getXmppConnection().getFeatures().sm() - || (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) { + || (conversation.getMode() == Conversation.MODE_MULTI + && message.getCounterpart().isBareJid())) { message.setStatus(Message.STATUS_UNSEND); } else { message.setStatus(Message.STATUS_SEND); @@ -2106,7 +2377,7 @@ public class XmppConnectionService extends Service { if (!message.needsUploading()) { String pgpBody = message.getEncryptedBody(); String decryptedBody = message.getBody(); - message.setBody(pgpBody); //TODO might throw NPE + message.setBody(pgpBody); // TODO might throw NPE message.setEncryption(Message.ENCRYPTION_PGP); if (message.edited()) { message.setBody(decryptedBody); @@ -2140,7 +2411,8 @@ public class XmppConnectionService extends Service { } } - boolean mucMessage = conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage(); + boolean mucMessage = + conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage(); if (mucMessage) { message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid()); } @@ -2191,7 +2463,13 @@ public class XmppConnectionService extends Service { final boolean pending = account.pendingConferenceJoins.contains(conversation); final boolean inProgressJoin = inProgress || pending; if (inProgressJoin) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": holding back message to group. inProgress=" + inProgress + ", pending=" + pending); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": holding back message to group. inProgress=" + + inProgress + + ", pending=" + + pending); } return inProgressJoin; } else { @@ -2241,11 +2519,16 @@ public class XmppConnectionService extends Service { return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN); } - public void requestEasyOnboardingInvite(final Account account, final EasyOnboardingInvite.OnInviteRequested callback) { + public void requestEasyOnboardingInvite( + final Account account, final EasyOnboardingInvite.OnInviteRequested callback) { final XmppConnection connection = account.getXmppConnection(); - final Jid jid = connection == null ? null : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE); + final Jid jid = + connection == null + ? null + : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE); if (jid == null) { - callback.inviteRequestFailed(getString(R.string.server_does_not_support_easy_onboarding_invites)); + callback.inviteRequestFailed( + getString(R.string.server_does_not_support_easy_onboarding_invites)); return; } final Iq request = new Iq(Iq.Type.SET); @@ -2253,57 +2536,72 @@ public class XmppConnectionService extends Service { final Element command = request.addChild("command", Namespace.COMMANDS); command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE); command.setAttribute("action", "execute"); - 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) { - final Data data = Data.parse(x); - final String uri = data.getValue("uri"); - final String landingUrl = data.getValue("landing-url"); - if (uri != null) { - final EasyOnboardingInvite invite = new EasyOnboardingInvite(jid.getDomain().toEscapedString(), uri, landingUrl); - callback.inviteRequested(invite); - return; + 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) { + final Data data = Data.parse(x); + final String uri = data.getValue("uri"); + final String landingUrl = data.getValue("landing-url"); + if (uri != null) { + final EasyOnboardingInvite invite = + new EasyOnboardingInvite( + jid.getDomain().toEscapedString(), uri, landingUrl); + callback.inviteRequested(invite); + return; + } + } + callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite)); + Log.d(Config.LOGTAG, response.toString()); + } else if (response.getType() == Iq.Type.ERROR) { + callback.inviteRequestFailed(IqParser.errorMessage(response)); + } else { + callback.inviteRequestFailed(getString(R.string.remote_server_timeout)); } - } - callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite)); - Log.d(Config.LOGTAG, response.toString()); - } else if (response.getType() == Iq.Type.ERROR) { - callback.inviteRequestFailed(IqParser.errorMessage(response)); - } else { - callback.inviteRequestFailed(getString(R.string.remote_server_timeout)); - } - }); - + }); } public void fetchBookmarks(final Account account) { final Iq iqPacket = new Iq(Iq.Type.GET); final Element query = iqPacket.query("jabber:iq:private"); query.addChild("storage", Namespace.BOOKMARKS); - 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(account, bookmarks, false); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not fetch bookmarks"); - } - }; + 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(account, bookmarks, false); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not fetch bookmarks"); + } + }; sendIqPacket(account, iqPacket, callback); } public void fetchBookmarks2(final Account account) { 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, account); - processBookmarksInitial(account, bookmarks, true); - } - }); + sendIqPacket( + account, + retrieve, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { + final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB); + final Map bookmarks = + Bookmark.parseFromPubSub(pubsub, account); + processBookmarksInitial(account, bookmarks, true); + } + }); } public void fetchMessageDisplayedSynchronization(final Account account) { @@ -2379,7 +2677,8 @@ public class XmppConnectionService extends Service { return true; } - public void processBookmarksInitial(final Account account, final Map bookmarks, final boolean pep) { + public void processBookmarksInitial( + final Account account, final Map bookmarks, final boolean pep) { final Set previousBookmarks = account.getBookmarkedJids(); for (final Bookmark bookmark : bookmarks.values()) { previousBookmarks.remove(bookmark.getJid().asBareJid()); @@ -2423,21 +2722,35 @@ public class XmppConnectionService extends Service { } bookmark.setConversation(conversation); if (pep && !bookmark.autojoin()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conference (" + conversation.getJid() + ") after receiving pep"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": archiving conference (" + + conversation.getJid() + + ") after receiving pep"); archiveConversation(conversation, false); } else { final MucOptions mucOptions = conversation.getMucOptions(); if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) { final String current = mucOptions.getActualNick(); - final String proposed = mucOptions.getProposedNick(); + final String proposed = mucOptions.getProposedNickPure(); if (current != null && !current.equals(proposed)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": proposed nick changed after bookmark push " + current + "->" + proposed); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": proposed nick changed after bookmark push " + + current + + "->" + + proposed); joinMuc(conversation); } + } else { + checkMucRequiresRename(conversation); } } } else if (bookmark.autojoin()) { - conversation = findOrCreateConversation(account, bookmark.getFullJid(), true, true, false); + conversation = + findOrCreateConversation(account, bookmark.getFullJid(), true, true, false); bookmark.setConversation(conversation); } } @@ -2446,15 +2759,40 @@ public class XmppConnectionService extends Service { processModifiedBookmark(bookmark, true); } + public void ensureBookmarkIsAutoJoin(final Conversation conversation) { + final var account = conversation.getAccount(); + final var existingBookmark = conversation.getBookmark(); + if (existingBookmark == null) { + final var bookmark = new Bookmark(account, conversation.getJid().asBareJid()); + bookmark.setAutojoin(true); + createBookmark(account, bookmark); + } else { + if (existingBookmark.autojoin()) { + return; + } + existingBookmark.setAutojoin(true); + createBookmark(account, existingBookmark); + } + } + public void createBookmark(final Account account, final Bookmark bookmark) { account.putBookmark(bookmark); final XmppConnection connection = account.getXmppConnection(); if (connection == null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid()+": no connection. ignoring bookmark creation"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": no connection. ignoring bookmark creation"); } else if (connection.getFeatures().bookmarks2()) { - Log.d(Config.LOGTAG,account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2"); final Element item = mIqGenerator.publishBookmarkItem(bookmark); - pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems()); + pushNodeAndEnforcePublishOptions( + account, + Namespace.BOOKMARKS2, + item, + bookmark.getJid().asBareJid().toEscapedString(), + PublishOptions.persistentWhitelistAccessMaxItems()); } else if (connection.getFeatures().bookmarksConversion()) { pushBookmarksPep(account); } else { @@ -2471,13 +2809,24 @@ public class XmppConnectionService extends Service { if (connection == null) return; if (connection.getFeatures().bookmarks2()) { - 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, (response) -> { - if (response.getType() == Iq.Type.ERROR) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition()); - } - }); + 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, + (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()) { pushBookmarksPep(account); } else { @@ -2506,49 +2855,84 @@ public class XmppConnectionService extends Service { for (final Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } - pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess()); - - } - - private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options) { + pushNodeAndEnforcePublishOptions( + account, + Namespace.BOOKMARKS, + storage, + "current", + PublishOptions.persistentWhitelistAccess()); + } + + private void pushNodeAndEnforcePublishOptions( + final Account account, + final String node, + final Element element, + final String id, + final Bundle options) { pushNodeAndEnforcePublishOptions(account, node, element, id, options, true); - } - private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options, final boolean retry) { + private void pushNodeAndEnforcePublishOptions( + final Account account, + final String node, + final Element element, + final String id, + final Bundle options, + final boolean retry) { 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)) { - pushNodeConfiguration(account, node, options, new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - pushNodeAndEnforcePublishOptions(account, node, element, id, options, false); + sendIqPacket( + account, + packet, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { + return; } + if (retry && PublishOptions.preconditionNotMet(response)) { + pushNodeConfiguration( + account, + node, + options, + new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + pushNodeAndEnforcePublishOptions( + account, node, element, id, options, false); + } - @Override - public void onPushFailed() { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to push node configuration (" + node + ")"); + @Override + public void onPushFailed() { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to push node configuration (" + + node + + ")"); + } + }); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error publishing " + + node + + " (retry=" + + retry + + ") " + + response); } }); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing "+node+" (retry=" + retry + ") " + response); - } - }); } private void restoreFromDatabase() { synchronized (this.conversations) { - final Map accountLookupTable = new Hashtable<>(); - for (Account account : this.accounts) { - accountLookupTable.put(account.getUuid(), account); - } + final Map accountLookupTable = + ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid)); Log.d(Config.LOGTAG, "restoring conversations..."); final long startTimeConversationsRestore = SystemClock.elapsedRealtime(); - this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); - for (Iterator iterator = conversations.listIterator(); iterator.hasNext(); ) { + this.conversations.addAll( + databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); + for (Iterator iterator = conversations.listIterator(); + iterator.hasNext(); ) { Conversation conversation = iterator.next(); Account account = accountLookupTable.get(conversation.getAccountUuid()); if (account != null) { @@ -2609,34 +2993,38 @@ public class XmppConnectionService extends Service { } public void loadPhoneContacts() { - mContactMergerExecutor.execute(() -> { - final Map contacts = JabberIdContact.load(this); - Log.d(Config.LOGTAG, "start merging phone contacts with roster"); - for (final Account account : accounts) { - final List withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class); - for (final JabberIdContact jidContact : contacts.values()) { - final Contact contact = account.getRoster().getContact(jidContact.getJid()); - boolean needsCacheClean = contact.setPhoneContact(jidContact); - if (needsCacheClean) { - getAvatarService().clear(contact); - } - withSystemAccounts.remove(contact); - } - for (final Contact contact : withSystemAccounts) { - boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class); - if (needsCacheClean) { - getAvatarService().clear(contact); + mContactMergerExecutor.execute( + () -> { + final Map contacts = JabberIdContact.load(this); + Log.d(Config.LOGTAG, "start merging phone contacts with roster"); + for (final Account account : accounts) { + final List withSystemAccounts = + account.getRoster().getWithSystemAccounts(JabberIdContact.class); + for (final JabberIdContact jidContact : contacts.values()) { + final Contact contact = + account.getRoster().getContact(jidContact.getJid()); + boolean needsCacheClean = contact.setPhoneContact(jidContact); + if (needsCacheClean) { + getAvatarService().clear(contact); + } + withSystemAccounts.remove(contact); + } + for (final Contact contact : withSystemAccounts) { + boolean needsCacheClean = + contact.unsetPhoneContact(JabberIdContact.class); + if (needsCacheClean) { + getAvatarService().clear(contact); + } + } } - } - } - Log.d(Config.LOGTAG, "finished merging phone contacts"); - mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true)); - updateRosterUi(UpdateRosterReason.INIT); - mQuickConversationsService.considerSync(); - }); + Log.d(Config.LOGTAG, "finished merging phone contacts"); + mShortcutService.refresh( + mInitialAddressbookSyncCompleted.compareAndSet(false, true)); + updateRosterUi(UpdateRosterReason.PUSH); + mQuickConversationsService.considerSync(); + }); } - public void syncRoster(final Account account) { mRosterSyncTaskManager.execute(account, () -> { unregisterPhoneAccounts(account); @@ -2658,7 +3046,14 @@ public class XmppConnectionService extends Service { } final boolean isInternalFile = fileBackend.isInternalFile(file); final List uuids = databaseBackend.markFileAsDeleted(file, isInternalFile); - Log.d(Config.LOGTAG, "deleted file " + file.getAbsolutePath() + " internal=" + isInternalFile + ", database hits=" + uuids.size()); + Log.d( + Config.LOGTAG, + "deleted file " + + file.getAbsolutePath() + + " internal=" + + isInternalFile + + ", database hits=" + + uuids.size()); markUuidsAsDeletedFiles(uuids); } @@ -2689,11 +3084,13 @@ public class XmppConnectionService extends Service { populateWithOrderedConversations(list, true, true); } - public void populateWithOrderedConversations(final List list, final boolean includeNoFileUpload) { + public void populateWithOrderedConversations( + final List list, final boolean includeNoFileUpload) { populateWithOrderedConversations(list, includeNoFileUpload, true); } - public void populateWithOrderedConversations(final List list, final boolean includeNoFileUpload, final boolean sort) { + public void populateWithOrderedConversations( + final List list, final boolean includeNoFileUpload, final boolean sort) { final List orderedUuids; if (sort) { orderedUuids = null; @@ -2709,63 +3106,85 @@ public class XmppConnectionService extends Service { } else { for (Conversation conversation : getConversations()) { if (conversation.getMode() == Conversation.MODE_SINGLE - || (conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating())) { + || (conversation.getAccount().httpUploadAvailable() + && conversation.getMucOptions().participating())) { list.add(conversation); } } } try { if (orderedUuids != null) { - Collections.sort(list, (a, b) -> { - final int indexA = orderedUuids.indexOf(a.getUuid()); - final int indexB = orderedUuids.indexOf(b.getUuid()); - if (indexA == -1 || indexB == -1 || indexA == indexB) { - return a.compareTo(b); - } - return indexA - indexB; - }); + Collections.sort( + list, + (a, b) -> { + final int indexA = orderedUuids.indexOf(a.getUuid()); + final int indexB = orderedUuids.indexOf(b.getUuid()); + if (indexA == -1 || indexB == -1 || indexA == indexB) { + return a.compareTo(b); + } + return indexA - indexB; + }); } else { Collections.sort(list); } } catch (IllegalArgumentException e) { - //ignore + // ignore } } - public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { - if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback) || conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + public void loadMoreMessages( + final Conversation conversation, + final long timestamp, + final OnMoreMessagesLoaded callback) { + if (XmppConnectionService.this + .getMessageArchiveService() + .queryInProgress(conversation, callback)) { return; } else if (timestamp == 0) { return; } - Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); - final Runnable runnable = () -> { - final Account account = conversation.getAccount(); - List messages = databaseBackend.getMessages(conversation, 50, timestamp); - if (messages.size() > 0) { - conversation.addAll(0, messages); - callback.onMoreMessagesLoaded(messages.size(), conversation); - } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected() - && conversation.getLastClearHistory().getTimestamp() == 0) { - final boolean mamAvailable; - if (conversation.getMode() == Conversation.MODE_SINGLE) { - mamAvailable = account.getXmppConnection().getFeatures().mam() && !conversation.getContact().isBlocked(); - } else { - mamAvailable = conversation.getMucOptions().mamSupport(); - } - if (mamAvailable) { - MessageArchiveService.Query query = getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false); - if (query != null) { - query.setCallback(callback); - callback.informUser(R.string.fetching_history_from_server); - } else { - callback.informUser(R.string.not_fetching_history_retention_period); + Log.d( + Config.LOGTAG, + "load more messages for " + + conversation.getName() + + " prior to " + + MessageGenerator.getTimestamp(timestamp)); + final Runnable runnable = + () -> { + final Account account = conversation.getAccount(); + List messages = + databaseBackend.getMessages(conversation, 50, timestamp); + if (messages.size() > 0) { + conversation.addAll(0, messages); + callback.onMoreMessagesLoaded(messages.size(), conversation); + } else if (conversation.hasMessagesLeftOnServer() + && account.isOnlineAndConnected() + && conversation.getLastClearHistory().getTimestamp() == 0) { + final boolean mamAvailable; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + mamAvailable = + account.getXmppConnection().getFeatures().mam() + && !conversation.getContact().isBlocked(); + } else { + mamAvailable = conversation.getMucOptions().mamSupport(); + } + if (mamAvailable) { + MessageArchiveService.Query query = + getMessageArchiveService() + .query( + conversation, + new MamReference(0), + timestamp, + false); + if (query != null) { + query.setCallback(callback); + callback.informUser(R.string.fetching_history_from_server); + } else { + callback.informUser(R.string.not_fetching_history_retention_period); + } + } } - - } - } - }; + }; mDatabaseReaderExecutor.execute(runnable); } @@ -2773,9 +3192,9 @@ public class XmppConnectionService extends Service { return this.accounts; } - /** - * This will find all conferences with the contact as member and also the conference that is the contact (that 'fake' contact is used to store the avatar) + * This will find all conferences with the contact as member and also the conference that is the + * contact (that 'fake' contact is used to store the avatar) */ public List findAllConferencesWith(Contact contact) { final ArrayList results = new ArrayList<>(); @@ -2784,15 +3203,16 @@ public class XmppConnectionService extends Service { continue; } final MucOptions mucOptions = c.getMucOptions(); - if (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || (mucOptions != null && mucOptions.isContactInRoom(contact))) { + if (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) + || (mucOptions != null && mucOptions.isContactInRoom(contact))) { results.add(c); } } return results; } - public Conversation find(final Iterable haystack, final Contact contact) { - for (final Conversation conversation : haystack) { + public Conversation find(final Contact contact) { + for (final Conversation conversation : this.conversations) { if (conversation.getContact() == contact) { return conversation; } @@ -2800,7 +3220,8 @@ public class XmppConnectionService extends Service { return null; } - public Conversation find(final Iterable haystack, final Account account, final Jid jid) { + public Conversation find( + final Iterable haystack, final Account account, final Jid jid) { if (jid == null) { return null; } @@ -2896,11 +3317,17 @@ public class XmppConnectionService extends Service { }); } - public Conversation findOrCreateConversation(Account account, Jid jid, boolean muc, final boolean async) { + public Conversation findOrCreateConversation( + Account account, Jid jid, boolean muc, final boolean async) { return this.findOrCreateConversation(account, jid, muc, false, async); } - public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final boolean async) { + public Conversation findOrCreateConversation( + final Account account, + final Jid jid, + final boolean muc, + final boolean joinAfterCreate, + final boolean async) { return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null); } @@ -2908,73 +3335,57 @@ public class XmppConnectionService extends Service { return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null); } - public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async, final String password) { + public Conversation findOrCreateConversation( + final Account account, + final Jid jid, + final boolean muc, + final boolean joinAfterCreate, + final MessageArchiveService.Query query, + final boolean async, + final String password) { synchronized (this.conversations) { - Conversation conversation = find(account, jid); - if (conversation != null) { - return conversation; + final var cached = find(account, jid); + if (cached != null) { + return cached; } - conversation = databaseBackend.findConversation(account, jid); + final var existing = databaseBackend.findConversation(account, jid); + final Conversation conversation; final boolean loadMessagesFromDb; - if (conversation != null) { - conversation.setStatus(Conversation.STATUS_AVAILABLE); - conversation.setAccount(account); - if (muc) { - conversation.setMode(Conversation.MODE_MULTI); - conversation.setContactJid(jid); - if (password != null) conversation.getMucOptions().setPassword(password); - } else { - conversation.setMode(Conversation.MODE_SINGLE); - conversation.setContactJid(jid.asBareJid()); - } - databaseBackend.updateConversation(conversation); - loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false); + if (existing != null) { + conversation = existing; + if (password != null) conversation.getMucOptions().setPassword(password); + loadMessagesFromDb = restoreFromArchive(conversation, jid, muc); } else { String conversationName; - Contact contact = account.getRoster().getContact(jid); + final Contact contact = account.getRoster().getContact(jid); if (contact != null) { conversationName = contact.getDisplayName(); } else { conversationName = jid.getLocal(); } if (muc) { - conversation = new Conversation(conversationName, account, jid, - Conversation.MODE_MULTI); - if (password != null) conversation.getMucOptions().setPassword(password); + conversation = + new Conversation( + conversationName, account, jid, Conversation.MODE_MULTI); } else { - conversation = new Conversation(conversationName, account, jid.asBareJid(), - Conversation.MODE_SINGLE); - } + conversation = + new Conversation( + conversationName, + account, + jid.asBareJid(), + Conversation.MODE_SINGLE); + } + if (password != null) conversation.getMucOptions().setPassword(password); this.databaseBackend.createConversation(conversation); loadMessagesFromDb = false; } - final Conversation c = conversation; - final Runnable runnable = () -> { - if (loadMessagesFromDb) { - c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE)); - updateConversationUi(); - c.messagesLoaded.set(true); - } - if (account.getXmppConnection() != null - && !c.getContact().isBlocked() - && account.getXmppConnection().getFeatures().mam() - && !muc) { - if (query == null) { - mMessageArchiveService.query(c); - } else { - if (query.getConversation() == null) { - mMessageArchiveService.query(c, query.getStart(), query.isCatchup()); - } - } - } - if (joinAfterCreate) { - joinMuc(c); - } - }; if (async) { - mDatabaseReaderExecutor.execute(runnable); + mDatabaseReaderExecutor.execute( + () -> + postProcessConversation( + conversation, loadMessagesFromDb, joinAfterCreate, query)); } else { - runnable.run(); + postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query); } this.conversations.add(conversation); updateConversationUi(); @@ -2982,11 +3393,93 @@ public class XmppConnectionService extends Service { } } + public Conversation findConversationByUuidReliable(final String uuid) { + final var cached = findConversationByUuid(uuid); + if (cached != null) { + return cached; + } + final var existing = databaseBackend.findConversation(uuid); + if (existing == null) { + return null; + } + Log.d( + Config.LOGTAG, + existing.getJid().asBareJid() + + ": restoring conversation with " + + existing.getJid() + + " from DB"); + final Map accounts = + ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid)); + existing.setAccount(accounts.get(existing.getAccountUuid())); + final var loadMessagesFromDb = restoreFromArchive(existing); + mDatabaseReaderExecutor.execute( + () -> + postProcessConversation( + existing, + loadMessagesFromDb, + existing.getMode() == Conversational.MODE_MULTI, + null)); + this.conversations.add(existing); + if (existing.getMode() == Conversational.MODE_MULTI) { + ensureBookmarkIsAutoJoin(existing); + } + updateConversationUi(); + return existing; + } + + private boolean restoreFromArchive( + final Conversation conversation, final Jid jid, final boolean muc) { + if (muc) { + conversation.setMode(Conversation.MODE_MULTI); + conversation.setContactJid(jid); + } else { + conversation.setMode(Conversation.MODE_SINGLE); + conversation.setContactJid(jid.asBareJid()); + } + return restoreFromArchive(conversation); + } + + private boolean restoreFromArchive(final Conversation conversation) { + conversation.setStatus(Conversation.STATUS_AVAILABLE); + databaseBackend.updateConversation(conversation); + return conversation.messagesLoaded.compareAndSet(true, false); + } + + private void postProcessConversation( + final Conversation c, + final boolean loadMessagesFromDb, + final boolean joinAfterCreate, + final MessageArchiveService.Query query) { + final var singleMode = c.getMode() == Conversational.MODE_SINGLE; + final var account = c.getAccount(); + if (loadMessagesFromDb) { + c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE)); + updateConversationUi(); + c.messagesLoaded.set(true); + } + if (account.getXmppConnection() != null + && !c.getContact().isBlocked() + && account.getXmppConnection().getFeatures().mam() + && singleMode) { + if (query == null) { + mMessageArchiveService.query(c); + } else { + if (query.getConversation() == null) { + mMessageArchiveService.query(c, query.getStart(), query.isCatchup()); + } + } + } + if (joinAfterCreate) { + joinMuc(c); + } + } + public void archiveConversation(Conversation conversation) { archiveConversation(conversation, true); } - private void archiveConversation(Conversation conversation, final boolean maySynchronizeWithBookmarks) { + private void archiveConversation( + Conversation conversation, final boolean maySynchronizeWithBookmarks) { if (isOnboarding()) return; getNotificationService().clear(conversation); @@ -3011,7 +3504,9 @@ public class XmppConnectionService extends Service { deregisterWithMuc(conversation); leaveMuc(conversation); } else { - if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + if (conversation + .getContact() + .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { stopPresenceUpdatesTo(conversation.getContact()); } } @@ -3042,15 +3537,23 @@ public class XmppConnectionService extends Service { private void syncEnabledAccountSetting() { final boolean hasEnabledAccounts = hasEnabledAccounts(); - getPreferences().edit().putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply(); + getPreferences() + .edit() + .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts) + .apply(); toggleSetProfilePictureActivity(hasEnabledAccounts); } private void toggleSetProfilePictureActivity(final boolean enabled) { try { - final ComponentName name = new ComponentName(this, ChooseAccountForProfilePictureActivity.class); - final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP); + final ComponentName name = + new ComponentName(this, ChooseAccountForProfilePictureActivity.class); + final int targetState = + enabled + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + getPackageManager() + .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP); } catch (IllegalStateException e) { Log.d(Config.LOGTAG, "unable to toggle profile picture activity"); } @@ -3060,7 +3563,8 @@ public class XmppConnectionService extends Service { return this.unifiedPushBroker.reconfigurePushDistributor(); } - private Optional renewUnifiedPushEndpoints(final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) { + private Optional renewUnifiedPushEndpoints( + final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) { return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger); } @@ -3081,48 +3585,55 @@ public class XmppConnectionService extends Service { } public void createAccountFromKey(final String alias, final OnAccountCreated callback) { - new Thread(() -> { - try { - final X509Certificate[] chain = KeyChain.getCertificateChain(this, alias); - final X509Certificate cert = chain != null && chain.length > 0 ? chain[0] : null; - if (cert == null) { - callback.informUser(R.string.unable_to_parse_certificate); - return; - } - Pair info = CryptoHelper.extractJidAndName(cert); - if (info == null) { - callback.informUser(R.string.certificate_does_not_contain_jid); - return; - } - if (findAccountByJid(info.first) == null) { - final Account account = new Account(info.first, ""); - account.setPrivateKeyAlias(alias); - account.setOption(Account.OPTION_DISABLED, true); - account.setOption(Account.OPTION_FIXED_USERNAME, true); - account.setDisplayName(info.second); - createAccount(account); - callback.onAccountCreated(account); - if (Config.X509_VERIFICATION) { - try { - getMemorizingTrustManager().getNonInteractive(account.getServer(), null, 0, null).checkClientTrusted(chain, "RSA"); - } catch (CertificateException e) { - callback.informUser(R.string.certificate_chain_is_not_trusted); - } - } - } else { - callback.informUser(R.string.account_already_exists); - } - } catch (Exception e) { - callback.informUser(R.string.unable_to_parse_certificate); - } - }).start(); - + new Thread( + () -> { + try { + final X509Certificate[] chain = + KeyChain.getCertificateChain(this, alias); + final X509Certificate cert = + chain != null && chain.length > 0 ? chain[0] : null; + if (cert == null) { + callback.informUser(R.string.unable_to_parse_certificate); + return; + } + Pair info = CryptoHelper.extractJidAndName(cert); + if (info == null) { + callback.informUser(R.string.certificate_does_not_contain_jid); + return; + } + if (findAccountByJid(info.first) == null) { + final Account account = new Account(info.first, ""); + account.setPrivateKeyAlias(alias); + account.setOption(Account.OPTION_DISABLED, true); + account.setOption(Account.OPTION_FIXED_USERNAME, true); + account.setDisplayName(info.second); + createAccount(account); + callback.onAccountCreated(account); + if (Config.X509_VERIFICATION) { + try { + getMemorizingTrustManager() + .getNonInteractive(account.getServer(), null, 0, null) + .checkClientTrusted(chain, "RSA"); + } catch (CertificateException e) { + callback.informUser( + R.string.certificate_chain_is_not_trusted); + } + } + } else { + callback.informUser(R.string.account_already_exists); + } + } catch (Exception e) { + callback.informUser(R.string.unable_to_parse_certificate); + } + }) + .start(); } public void updateKeyInAccount(final Account account, final String alias) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias); try { - X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias); + X509Certificate[] chain = + KeyChain.getCertificateChain(XmppConnectionService.this, alias); Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain"); Pair info = CryptoHelper.extractJidAndName(chain[0]); if (info == null) { @@ -3135,7 +3646,9 @@ public class XmppConnectionService extends Service { databaseBackend.updateAccount(account); if (Config.X509_VERIFICATION) { try { - getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA"); + getMemorizingTrustManager() + .getNonInteractive() + .checkClientTrusted(chain, "RSA"); } catch (CertificateException e) { showErrorToastInUi(R.string.certificate_chain_is_not_trusted); } @@ -3175,32 +3688,41 @@ public class XmppConnectionService extends Service { } } - public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { + public void updateAccountPasswordOnServer( + final Account account, + final String newPassword, + final OnAccountPasswordChanged callback) { 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(); - } - }); + 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(); + } + }); } public void unregisterAccount(final Account account, final Consumer callback) { final Iq iqPacket = new Iq(Iq.Type.SET); - final Element query = iqPacket.addChild("query",Namespace.REGISTER); + final Element query = iqPacket.addChild("query", Namespace.REGISTER); query.addChild("remove"); - sendIqPacket(account, iqPacket, (response) -> { - if (response.getType() == Iq.Type.RESULT) { - deleteAccount(account); - callback.accept(true); - } else { - callback.accept(false); - } - }); + sendIqPacket( + account, + iqPacket, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { + deleteAccount(account); + callback.accept(true); + } else { + callback.accept(false); + } + }); } public void deleteAccount(final Account account) { @@ -3229,11 +3751,14 @@ public class XmppConnectionService extends Service { if (account.getXmppConnection() != null) { new Thread(() -> disconnect(account, !connected)).start(); } - final Runnable runnable = () -> { - if (!databaseBackend.deleteAccount(account)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete account"); - } - }; + final Runnable runnable = + () -> { + if (!databaseBackend.deleteAccount(account)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": unable to delete account"); + } + }; mDatabaseWriterExecutor.execute(runnable); this.accounts.remove(account); if (CallIntegration.hasSystemFeature(this)) { @@ -3252,7 +3777,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnConversationUpdates.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as ConversationListChangedListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as ConversationListChangedListener"); } this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0); } @@ -3278,7 +3806,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnShowErrorToasts.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnShowErrorToastListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnShowErrorToastListener"); } } if (remainingListeners) { @@ -3302,7 +3833,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnAccountUpdates.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnAccountListChangedtListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnAccountListChangedtListener"); } } if (remainingListeners) { @@ -3326,7 +3860,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnCaptchaRequested.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnCaptchaRequestListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnCaptchaRequestListener"); } } if (remainingListeners) { @@ -3350,7 +3887,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnRosterUpdates.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnRosterUpdateListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnRosterUpdateListener"); } } if (remainingListeners) { @@ -3374,7 +3914,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnUpdateBlocklist.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnUpdateBlocklistListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnUpdateBlocklistListener"); } } if (remainingListeners) { @@ -3398,7 +3941,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnKeyStatusUpdated.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnKeyStatusUpdateListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnKeyStatusUpdateListener"); } } if (remainingListeners) { @@ -3422,7 +3968,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.onJingleRtpConnectionUpdate.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnJingleRtpConnectionUpdate"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnJingleRtpConnectionUpdate"); } } if (remainingListeners) { @@ -3446,7 +3995,10 @@ public class XmppConnectionService extends Service { synchronized (LISTENER_LOCK) { remainingListeners = checkListeners(); if (!this.mOnMucRosterUpdate.add(listener)) { - Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnMucRosterListener"); + Log.w( + Config.LOGTAG, + listener.getClass().getName() + + " is already registered as OnMucRosterListener"); } } if (remainingListeners) { @@ -3496,7 +4048,10 @@ public class XmppConnectionService extends Service { connection.sendActive(); } if (broadcastLastActivity) { - sendPresence(account, false); //send new presence but don't include idle because we are not + sendPresence( + account, + false); // send new presence but don't include idle because we are + // not } } } @@ -3532,7 +4087,8 @@ public class XmppConnectionService extends Service { public void connectMultiModeConversations(Account account) { List conversations = getConversations(); for (Conversation conversation : conversations) { - if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { + if (conversation.getMode() == Conversation.MODE_MULTI + && conversation.getAccount() == account) { joinMuc(conversation); } } @@ -3542,13 +4098,19 @@ public class XmppConnectionService extends Service { final Account account = conversation.getAccount(); synchronized (account.inProgressConferenceJoins) { if (account.inProgressConferenceJoins.contains(conversation)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": canceling muc self ping because join is already under way"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": canceling muc self ping because join is already under way"); return; } } synchronized (account.inProgressConferencePings) { if (!account.inProgressConferencePings.add(conversation)) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": canceling muc self ping because ping is already under way"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": canceling muc self ping because ping is already under way"); return; } } @@ -3556,23 +4118,45 @@ public class XmppConnectionService extends Service { final Iq ping = new Iq(Iq.Type.GET); ping.setTo(self); ping.addChild("ping", Namespace.PING); - 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, account.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error"); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin"); - joinMuc(conversation); - } - } 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); - } - }); + 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, + account.getJid().asBareJid() + + ": ping to " + + self + + " came back as ignorable error"); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": ping to " + + self + + " failed. attempting rejoin"); + joinMuc(conversation); + } + } 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); + } + }); } + public void joinMuc(Conversation conversation) { joinMuc(conversation, null, false); } @@ -3585,7 +4169,10 @@ public class XmppConnectionService extends Service { joinMuc(conversation, onConferenceJoined, false); } - private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) { + private void joinMuc( + Conversation conversation, + final OnConferenceJoined onConferenceJoined, + final boolean followedInvite) { final Account account = conversation.getAccount(); synchronized (account.pendingConferenceJoins) { account.pendingConferenceJoins.remove(conversation); @@ -3605,101 +4192,135 @@ public class XmppConnectionService extends Service { conversation.getMucOptions().flagNoAutoPushConfiguration(); } conversation.setHasMessagesLeftOnServer(false); - fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() { - - private void join(Conversation conversation) { - Account account = conversation.getAccount(); - final MucOptions mucOptions = conversation.getMucOptions(); - - if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - } - mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); - updateConversationUi(); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - return; - } + fetchConferenceConfiguration( + conversation, + new OnConferenceConfigurationFetched() { + + private void join(Conversation conversation) { + Account account = conversation.getAccount(); + final MucOptions mucOptions = conversation.getMucOptions(); + + if (mucOptions.nonanonymous() + && !mucOptions.membersOnly() + && !conversation.getBooleanAttribute( + "accept_non_anonymous", false)) { + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.remove(conversation); + } + mucOptions.setError(MucOptions.Error.NON_ANONYMOUS); + updateConversationUi(); + if (onConferenceJoined != null) { + onConferenceJoined.onConferenceJoined(conversation); + } + return; + } - final Jid joinJid = mucOptions.getSelf().getFullJid(); - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString()); - 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) { - x.addChild("password").setContent(mucOptions.getPassword()); - } + final Jid joinJid = mucOptions.getSelf().getFullJid(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": joining conversation " + + joinJid.toString()); + 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) { + x.addChild("password").setContent(mucOptions.getPassword()); + } - if (mucOptions.mamSupport()) { - // Use MAM instead of the limited muc history to get history - x.addChild("history").setAttribute("maxchars", "0"); - } else { - // Fallback to muc history - x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted().getTimestamp())); - } - sendPresencePacket(account, packet); - if (onConferenceJoined != null) { - onConferenceJoined.onConferenceJoined(conversation); - } - if (!joinJid.equals(conversation.getJid())) { - conversation.setContactJid(joinJid); - databaseBackend.updateConversation(conversation); - } + if (mucOptions.mamSupport()) { + // Use MAM instead of the limited muc history to get history + x.addChild("history").setAttribute("maxchars", "0"); + } else { + // Fallback to muc history + x.addChild("history") + .setAttribute( + "since", + PresenceGenerator.getTimestamp( + conversation + .getLastMessageTransmitted() + .getTimestamp())); + } + sendPresencePacket(account, packet); + if (onConferenceJoined != null) { + onConferenceJoined.onConferenceJoined(conversation); + } + if (!joinJid.equals(conversation.getJid())) { + conversation.setContactJid(joinJid); + databaseBackend.updateConversation(conversation); + } - maybeRegisterWithMuc(conversation, null); + maybeRegisterWithMuc(conversation, null); - if (mucOptions.mamSupport()) { - getMessageArchiveService().catchupMUC(conversation); - } - fetchConferenceMembers(conversation); - if (mucOptions.isPrivateAndNonAnonymous()) { - if (followedInvite) { - final Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null) { - if (!bookmark.autojoin()) { - bookmark.setAutojoin(true); - createBookmark(account, bookmark); + if (mucOptions.mamSupport()) { + getMessageArchiveService().catchupMUC(conversation); + } + fetchConferenceMembers(conversation); + if (mucOptions.isPrivateAndNonAnonymous()) { + if (followedInvite) { + final Bookmark bookmark = conversation.getBookmark(); + if (bookmark != null) { + if (!bookmark.autojoin()) { + bookmark.setAutojoin(true); + createBookmark(account, bookmark); + } + } else { + saveConversationAsBookmark(conversation, null); + } } - } else { - saveConversationAsBookmark(conversation, null); + } + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.remove(conversation); + sendUnsentMessages(conversation); } } - } - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); - sendUnsentMessages(conversation); - } - } - @Override - public void onConferenceConfigurationFetched(Conversation conversation) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result"); - return; - } - join(conversation); - } + @Override + public void onConferenceConfigurationFetched(Conversation conversation) { + if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": conversation (" + + conversation.getJid() + + ") got archived before IQ result"); + return; + } + join(conversation); + } - @Override - public void onFetchFailed(final Conversation conversation, final String errorCondition) { - if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result"); - return; - } - if ("remote-server-not-found".equals(errorCondition)) { - synchronized (account.inProgressConferenceJoins) { - account.inProgressConferenceJoins.remove(conversation); + @Override + public void onFetchFailed( + final Conversation conversation, final String errorCondition) { + if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": conversation (" + + conversation.getJid() + + ") got archived before IQ result"); + return; + } + if ("remote-server-not-found".equals(errorCondition)) { + synchronized (account.inProgressConferenceJoins) { + account.inProgressConferenceJoins.remove(conversation); + } + conversation + .getMucOptions() + .setError(MucOptions.Error.SERVER_NOT_FOUND); + updateConversationUi(); + } else { + join(conversation); + fetchConferenceConfiguration(conversation); + } } - conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND); - updateConversationUi(); - } else { - join(conversation); - fetchConferenceConfiguration(conversation); - } - } - }); + }); updateConversationUi(); } else { synchronized (account.pendingConferenceJoins) { @@ -3717,67 +4338,89 @@ public class XmppConnectionService extends Service { final var affiliations = new ArrayList(); affiliations.add("outcast"); if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner")); - final Consumer callback = new Consumer() { + final Consumer callback = + new Consumer() { - private int i = 0; - private boolean success = true; + private int i = 0; + private boolean success = true; - @Override - public void accept(Iq response) { - final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; - 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); - user.setOnline(false); - if (!user.realJidMatchesAccount()) { - boolean isNew = conversation.getMucOptions().updateUser(user); - Contact contact = user.getContact(); - if (omemoEnabled - && isNew - && user.getRealJid() != null - && (contact == null || !contact.mutualPresenceSubscription()) - && axolotlService.hasEmptyDeviceList(user.getRealJid())) { - axolotlService.fetchDeviceIds(user.getRealJid()); + @Override + public void accept(Iq response) { + final boolean omemoEnabled = + conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL; + 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); + user.setOnline(false); + if (!user.realJidMatchesAccount()) { + boolean isNew = + conversation.getMucOptions().updateUser(user); + Contact contact = user.getContact(); + if (omemoEnabled + && isNew + && user.getRealJid() != null + && (contact == null + || !contact.mutualPresenceSubscription()) + && axolotlService.hasEmptyDeviceList( + user.getRealJid())) { + axolotlService.fetchDeviceIds(user.getRealJid()); + } + } } } + } else { + success = false; + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": could not request affiliation " + + affiliations.get(i) + + " in " + + conversation.getJid().asBareJid()); } - } - } else { - success = false; - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not request affiliation " + affiliations.get(i) + " in " + conversation.getJid().asBareJid()); - } - ++i; - if (i >= affiliations.size()) { - List members = conversation.getMucOptions().getMembers(true); - if (success) { - List cryptoTargets = conversation.getAcceptedCryptoTargets(); - boolean changed = false; - for (ListIterator iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) { - Jid jid = iterator.next(); - if (!members.contains(jid) && !members.contains(jid.getDomain())) { - iterator.remove(); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName()); - changed = true; + ++i; + if (i >= affiliations.size()) { + List members = conversation.getMucOptions().getMembers(true); + if (success) { + List cryptoTargets = conversation.getAcceptedCryptoTargets(); + boolean changed = false; + for (ListIterator iterator = cryptoTargets.listIterator(); + iterator.hasNext(); ) { + Jid jid = iterator.next(); + if (!members.contains(jid) + && !members.contains(jid.getDomain())) { + iterator.remove(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": removed " + + jid + + " from crypto targets of " + + conversation.getName()); + changed = true; + } + } + if (changed) { + conversation.setAcceptedCryptoTargets(cryptoTargets); + updateConversation(conversation); + } } - } - if (changed) { - conversation.setAcceptedCryptoTargets(cryptoTargets); - updateConversation(conversation); + getAvatarService().clear(conversation); + updateMucRosterUi(); + updateConversationUi(); } } - getAvatarService().clear(conversation); - updateMucRosterUi(); - updateConversationUi(); - } - } - }; + }; for (String affiliation : affiliations) { - final var x = mIqGenerator.queryAffiliation(conversation, affiliation); - sendIqPacket(account, x, callback); + sendIqPacket( + account, mIqGenerator.queryAffiliation(conversation, affiliation), callback); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName()); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": fetching members for " + conversation.getName()); } public void providePasswordForMuc(final Conversation conversation, final String password) { @@ -3815,47 +4458,72 @@ public class XmppConnectionService extends Service { private void deletePepNode(final Account account, final String node, final Runnable runnable) { 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,account.getJid().asBareJid()+": failed to delete "+ packet); - } - }); + 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, + account.getJid().asBareJid() + ": failed to delete " + packet); + } + }); } private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) { 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,account.getJid().asBareJid()+": no vCard set. nothing to do"); - return; - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - final Iq publication = new Iq(Iq.Type.SET); - publication.setTo(account.getJid().asBareJid()); - publication.addChild(vcard); - 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()); - } - }); - }); + 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, + account.getJid().asBareJid() + ": no vCard set. nothing to do"); + return; + } + Element photo = vcard.findChild("PHOTO"); + if (photo == null) { + photo = vcard.addChild("PHOTO"); + } + photo.clearChildren(); + final Iq publication = new Iq(Iq.Type.SET); + publication.setTo(account.getJid().asBareJid()); + publication.addChild(vcard); + 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()); + } + }); + }); } private boolean hasEnabledAccounts() { @@ -3870,43 +4538,62 @@ public class XmppConnectionService extends Service { return false; } - - public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { - getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded); + public void getAttachments( + final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { + getAttachments( + conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded); } - public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { + public void getAttachments( + final Account account, + final Jid jid, + final int limit, + final OnMediaLoaded onMediaLoaded) { getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded); } - - public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { - new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start(); + public void getAttachments( + final String account, + final Jid jid, + final int limit, + final OnMediaLoaded onMediaLoaded) { + new Thread( + () -> + onMediaLoaded.onMediaLoaded( + fileBackend.convertToAttachments( + databaseBackend.getRelativeFilePaths( + account, jid, limit)))) + .start(); } - public void persistSelfNick(final MucOptions.User self) { + public void persistSelfNick(final MucOptions.User self, final boolean modified) { final Conversation conversation = self.getConversation(); - final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark(); - Jid full = self.getFullJid(); + final Account account = conversation.getAccount(); + final Jid full = self.getFullJid(); if (!full.equals(conversation.getJid())) { - Log.d(Config.LOGTAG, "nick changed. updating"); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full); conversation.setContactJid(full); databaseBackend.updateConversation(conversation); } final String nick = self.getNick(); final Bookmark bookmark = conversation.getBookmark(); - final String bookmarkedNick = bookmark == null ? null : bookmark.getNick(); - if (bookmark != null && (tookProposedNickFromBookmark || Strings.isNullOrEmpty(bookmarkedNick)) && !nick.equals(bookmarkedNick)) { - final Account account = conversation.getAccount(); - final String defaultNick = MucOptions.defaultNick(account); - if (Strings.isNullOrEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) { - return; - } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + nick + "' into bookmark for " + conversation.getJid().asBareJid()); - bookmark.setNick(nick); - createBookmark(bookmark.getAccount(), bookmark); + if (bookmark == null || !modified) { + return; + } + final String defaultNick = MucOptions.defaultNick(account); + if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) { + return; } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": persist nick '" + + full.getResource() + + "' into bookmark for " + + conversation.getJid().asBareJid()); + bookmark.setNick(nick); + createBookmark(bookmark.getAccount(), bookmark); } public void presenceToMuc(final Conversation conversation) { @@ -3920,7 +4607,12 @@ public class XmppConnectionService extends Service { } } - public boolean renameInMuc(final Conversation conversation, final String nick, final UiCallback callback) { + public boolean renameInMuc( + final Conversation conversation, + final String nick, + final UiCallback callback) { + final Account account = conversation.getAccount(); + final Bookmark bookmark = conversation.getBookmark(); final MucOptions options = conversation.getMucOptions(); final Jid joinJid = options.createJoinJid(nick); if (joinJid == null) { @@ -3928,35 +4620,46 @@ public class XmppConnectionService extends Service { } if (options.online()) { maybeRegisterWithMuc(conversation, nick); + options.setOnRenameListener( + new OnRenameListener() { + + @Override + public void onSuccess() { + final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); + packet.setTo(joinJid); + sendPresencePacket(account, packet); + callback.success(conversation); + } - Account account = conversation.getAccount(); - options.setOnRenameListener(new OnRenameListener() { - - @Override - public void onSuccess() { - final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick); - packet.setTo(joinJid); - sendPresencePacket(account, packet); - callback.success(conversation); - } - - @Override - public void onFailure() { - callback.error(R.string.nick_in_use, conversation); - } - }); + @Override + public void onFailure() { + callback.error(R.string.nick_in_use, conversation); + } + }); - final var 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); + if (nick.equals(MucOptions.defaultNick(account)) + && bookmark != null + && bookmark.getNick() != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": removing nick from bookmark for " + + bookmark.getJid()); + bookmark.setNick(null); + createBookmark(account, bookmark); + } } else { conversation.setContactJid(joinJid); databaseBackend.updateConversation(conversation); - if (conversation.getAccount().getStatus() == Account.State.ONLINE) { - Bookmark bookmark = conversation.getBookmark(); + if (account.getStatus() == Account.State.ONLINE) { if (bookmark != null) { bookmark.setNick(nick); - createBookmark(bookmark.getAccount(), bookmark); + createBookmark(account, bookmark); } joinMuc(conversation); } @@ -3964,6 +4667,40 @@ public class XmppConnectionService extends Service { return true; } + public void checkMucRequiresRename() { + synchronized (this.conversations) { + for (final Conversation conversation : this.conversations) { + if (conversation.getMode() == Conversational.MODE_MULTI) { + checkMucRequiresRename(conversation); + } + } + } + } + + private void checkMucRequiresRename(final Conversation conversation) { + final var options = conversation.getMucOptions(); + if (!options.online()) { + return; + } + final var account = conversation.getAccount(); + final String current = options.getActualNick(); + final String proposed = options.getProposedNickPure(); + if (current == null || current.equals(proposed)) { + return; + } + final Jid joinJid = options.createJoinJid(proposed); + Log.d( + Config.LOGTAG, + String.format( + "%s: muc rename required %s (was: %s)", + account.getJid().asBareJid(), joinJid, current)); + final var packet = + mPresenceGenerator.selfPresence( + account, Presence.Status.ONLINE, options.nonanonymous(), proposed); + packet.setTo(joinJid); + sendPresencePacket(account, packet); + } + public void leaveMuc(Conversation conversation) { leaveMuc(conversation, false); } @@ -3977,13 +4714,19 @@ public class XmppConnectionService extends Service { account.pendingConferenceLeaves.remove(conversation); } if (account.getStatus() == Account.State.ONLINE || now) { - sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions())); + sendPresencePacket( + conversation.getAccount(), + mPresenceGenerator.leave(conversation.getMucOptions())); conversation.getMucOptions().setOffline(); Bookmark bookmark = conversation.getBookmark(); if (bookmark != null) { bookmark.setConversation(null); } - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid()); + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": leaving muc " + + conversation.getJid()); } else { synchronized (account.pendingConferenceLeaves) { account.pendingConferenceLeaves.add(conversation); @@ -4010,37 +4753,57 @@ public class XmppConnectionService extends Service { return null; } - - public void createPublicChannel(final Account account, final String name, final Jid address, final UiCallback callback) { - joinMuc(findOrCreateConversation(account, address, true, false, true), conversation -> { - final Bundle configuration = IqGenerator.defaultChannelConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); - } - pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() { - @Override - public void onPushSucceeded() { - saveConversationAsBookmark(conversation, name); - callback.success(conversation); - } - - @Override - public void onPushFailed() { - if (conversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - callback.error(R.string.unable_to_set_channel_configuration, conversation); - } else { - callback.error(R.string.joined_an_existing_channel, conversation); + public void createPublicChannel( + final Account account, + final String name, + final Jid address, + final UiCallback callback) { + joinMuc( + findOrCreateConversation(account, address, true, false, true), + conversation -> { + final Bundle configuration = IqGenerator.defaultChannelConfiguration(); + if (!TextUtils.isEmpty(name)) { + configuration.putString("muc#roomconfig_roomname", name); } - } - }); - }); + pushConferenceConfiguration( + conversation, + configuration, + new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + saveConversationAsBookmark(conversation, name); + callback.success(conversation); + } + + @Override + public void onPushFailed() { + if (conversation + .getMucOptions() + .getSelf() + .getAffiliation() + .ranks(MucOptions.Affiliation.OWNER)) { + callback.error( + R.string.unable_to_set_channel_configuration, + conversation); + } else { + callback.error( + R.string.joined_an_existing_channel, conversation); + } + } + }); + }); } - public boolean createAdhocConference(final Account account, - final String name, - final Iterable jids, - final UiCallback callback) { - Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": creating adhoc conference with " + jids.toString()); + public boolean createAdhocConference( + final Account account, + final String name, + final Iterable jids, + final UiCallback callback) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid().toString() + + ": creating adhoc conference with " + + jids.toString()); if (account.getStatus() == Account.State.ONLINE) { try { String server = findConferenceServer(account); @@ -4051,42 +4814,58 @@ public class XmppConnectionService extends Service { return false; } final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null); - final Conversation conversation = findOrCreateConversation(account, jid, true, false, true); - joinMuc(conversation, new OnConferenceJoined() { - @Override - public void onConferenceJoined(final Conversation conversation) { - final Bundle configuration = IqGenerator.defaultGroupChatConfiguration(); - if (!TextUtils.isEmpty(name)) { - configuration.putString("muc#roomconfig_roomname", name); - } - pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() { + final Conversation conversation = + findOrCreateConversation(account, jid, true, false, true); + joinMuc( + conversation, + new OnConferenceJoined() { @Override - public void onPushSucceeded() { - for (Jid invite : jids) { - invite(conversation, invite); - } - for (String resource : account.getSelfContact().getPresences().toResourceArray()) { - if (resource == null || "".equals(resource)) continue; - Jid other = account.getJid().withResource(resource); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending direct invite to " + other); - directInvite(conversation, other); + public void onConferenceJoined(final Conversation conversation) { + final Bundle configuration = + IqGenerator.defaultGroupChatConfiguration(); + if (!TextUtils.isEmpty(name)) { + configuration.putString("muc#roomconfig_roomname", name); } - saveConversationAsBookmark(conversation, name); - if (callback != null) { - callback.success(conversation); - } - } + pushConferenceConfiguration( + conversation, + configuration, + new OnConfigurationPushed() { + @Override + public void onPushSucceeded() { + for (Jid invite : jids) { + invite(conversation, invite); + } + for (String resource : + account.getSelfContact() + .getPresences() + .toResourceArray()) { + Jid other = + account.getJid().withResource(resource); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": sending direct invite to " + + other); + directInvite(conversation, other); + } + saveConversationAsBookmark(conversation, name); + if (callback != null) { + callback.success(conversation); + } + } - @Override - public void onPushFailed() { - archiveConversation(conversation); - if (callback != null) { - callback.error(R.string.conference_creation_failed, conversation); - } + @Override + public void onPushFailed() { + archiveConversation(conversation); + if (callback != null) { + callback.error( + R.string.conference_creation_failed, + conversation); + } + } + }); } }); - } - }); return true; } catch (IllegalArgumentException e) { if (callback != null) { @@ -4124,75 +4903,121 @@ public class XmppConnectionService extends Service { fetchConferenceConfiguration(conversation, null); } - public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) { + public void fetchConferenceConfiguration( + final Conversation conversation, final OnConferenceConfigurationFetched callback) { final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); final var account = conversation.getAccount(); - sendIqPacket(account, request, response -> { - if (response.getType() == Iq.Type.RESULT) { - final 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(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); - } - } - + 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(response))) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": muc configuration changed for " + + conversation.getJid().asBareJid()); + updateConversation(conversation); + } - if (callback != null) { - callback.onConferenceConfigurationFetched(conversation); - } + if (bookmark != null + && (sameBefore || bookmark.getBookmarkName() == null)) { + if (bookmark.setBookmarkName( + StringUtils.nullOnEmpty(mucOptions.getName()))) { + createBookmark(account, bookmark); + } + } + if (callback != null) { + callback.onConferenceConfigurationFetched(conversation); + } - 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()); - } - } - }); + 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()); + } + } + }); } - public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) { + public void pushNodeConfiguration( + Account account, + final String node, + final Bundle options, + final OnConfigurationPushed callback) { pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback); } - public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) { + 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), 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) { + 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 (callback != null) { - callback.onPushFailed(); - } - } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) { - callback.onPushFailed(); - } - }); + } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) { + callback.onPushFailed(); + } + }); } - public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) { + public void pushConferenceConfiguration( + final Conversation conversation, + final Bundle options, + final OnConfigurationPushed callback) { if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) { conversation.setAttribute("accept_non_anonymous", true); updateConversation(conversation); @@ -4211,69 +5036,95 @@ public class XmppConnectionService extends Service { final Iq request = new Iq(Iq.Type.GET); request.setTo(conversation.getJid().asBareJid()); request.query("http://jabber.org/protocol/muc#owner"); - 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) { - if (packet.getType() == Iq.Type.RESULT) { - callback.onPushSucceeded(); - } else { - Log.d(Config.LOGTAG,"failed: "+packet.toString()); + 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) { + if (packet.getType() == Iq.Type.RESULT) { + callback.onPushSucceeded(); + } else { + Log.d(Config.LOGTAG, "failed: " + packet.toString()); + callback.onPushFailed(); + } + } + }); + } else { + if (callback != null) { callback.onPushFailed(); } } }); - } else { - if (callback != null) { - callback.onPushFailed(); - } - } - }); } public void pushSubjectToConference(final Conversation conference, final String subject) { - final var 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) { - 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 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) { - callback.onAffiliationChangedSuccessful(jid); - } else { - Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation); - } - } else if (callback != null) { - callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation); - } else { - Log.d(Config.LOGTAG, "unable to change affiliation"); - } - }); + public void requestVoice(final Account account, final Jid 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 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) { + callback.onAffiliationChangedSuccessful(jid); + } else { + Log.d( + Config.LOGTAG, + "changed affiliation of " + user + " to " + affiliation); + } + } else if (callback != null) { + callback.onAffiliationChangeFailed( + jid, R.string.could_not_change_affiliation); + } else { + Log.d(Config.LOGTAG, "unable to change affiliation"); + } + }); } - public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) { - final var account =conference.getAccount(); + public void changeRoleInConference( + final Conversation conference, final String nick, MucOptions.Role role) { + 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); - } - }); + 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) { @@ -4290,17 +5141,20 @@ public class XmppConnectionService extends Service { 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, response -> { - if (response.getType() == Iq.Type.RESULT) { - if (callback != null) { - callback.onRoomDestroySucceeded(); - } - } else if (response.getType() == Iq.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(); + } + } + }); } private void disconnect(final Account account, boolean force) { @@ -4377,7 +5231,8 @@ public class XmppConnectionService extends Service { createContact(contact, autoGrant, null); } - public void createContact(final Contact contact, final boolean autoGrant, final String preAuth) { + public void createContact( + final Contact contact, final boolean autoGrant, final String preAuth) { if (autoGrant) { contact.setOption(Contact.Options.PREEMPTIVE_GRANT); contact.setOption(Contact.Options.ASKING); @@ -4395,9 +5250,9 @@ public class XmppConnectionService extends Service { final Account account = contact.getAccount(); if (account.getStatus() == Account.State.ONLINE) { final boolean ask = contact.getOption(Contact.Options.ASKING); - final boolean sendUpdates = contact - .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) - && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); + final boolean sendUpdates = + contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) + && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); final Iq iq = new Iq(Iq.Type.SET); iq.query(Namespace.ROSTER).addChild(contact.asElement()); account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); @@ -4405,207 +5260,321 @@ public class XmppConnectionService extends Service { sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); } if (ask) { - sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); + sendPresencePacket( + account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth)); } } else { syncRoster(contact.getAccount()); } } - public void publishMucAvatar(final Conversation conversation, final Uri image, final OnAvatarPublication callback) { - new Thread(() -> { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - callback.onAvatarPublicationFailed(R.string.error_saving_avatar); - return; - } - avatar.owner = conversation.getJid().asBareJid(); - publishMucAvatar(conversation, avatar, callback); - } else { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); - } - }).start(); - } - - public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) { - new Thread(() -> { - final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; - final int size = Config.AVATAR_SIZE; - final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); - if (avatar != null) { - if (!getFileBackend().save(avatar)) { - Log.d(Config.LOGTAG, "unable to save vcard"); - callback.onAvatarPublicationFailed(R.string.error_saving_avatar); - return; - } - publishAvatar(account, avatar, callback); - } else { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); + public void publishMucAvatar( + final Conversation conversation, final Uri image, final OnAvatarPublication callback) { + new Thread( + () -> { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = + getFileBackend().getPepAvatar(image, size, format); + if (avatar != null) { + if (!getFileBackend().save(avatar)) { + callback.onAvatarPublicationFailed( + R.string.error_saving_avatar); + return; + } + avatar.owner = conversation.getJid().asBareJid(); + publishMucAvatar(conversation, avatar, callback); + } else { + callback.onAvatarPublicationFailed( + R.string.error_publish_avatar_converting); + } + }) + .start(); + } + + public void publishAvatarAsync( + final Account account, + final Uri image, + final boolean open, + final OnAvatarPublication callback) { + new Thread(() -> publishAvatar(account, image, open, callback)).start(); + } + + private void publishAvatar( + final Account account, + final Uri image, + final boolean open, + final OnAvatarPublication callback) { + final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; + final int size = Config.AVATAR_SIZE; + final Avatar avatar = getFileBackend().getPepAvatar(image, size, format); + if (avatar != null) { + if (!getFileBackend().save(avatar)) { + Log.d(Config.LOGTAG, "unable to save vcard"); + callback.onAvatarPublicationFailed(R.string.error_saving_avatar); + return; } - }).start(); - + publishAvatar(account, avatar, open, callback); + } else { + callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting); + } } - private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) { + private void publishMucAvatar( + Conversation conversation, Avatar avatar, OnAvatarPublication callback) { 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"); - } - Element photo = vcard.findChild("PHOTO"); - if (photo == null) { - photo = vcard.addChild("PHOTO"); - } - photo.clearChildren(); - photo.addChild("TYPE").setContent(avatar.type); - photo.addChild("BINVAL").setContent(avatar.image); - final Iq publication = new Iq(Iq.Type.SET); - publication.setTo(conversation.getJid().asBareJid()); - publication.addChild(vcard); - sendIqPacket(account, publication, (publicationResponse) -> { - if (publicationResponse.getType() == Iq.Type.RESULT) { - callback.onAvatarPublicationSucceeded(); + 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"); + } + Element photo = vcard.findChild("PHOTO"); + if (photo == null) { + photo = vcard.addChild("PHOTO"); + } + photo.clearChildren(); + photo.addChild("TYPE").setContent(avatar.type); + photo.addChild("BINVAL").setContent(avatar.image); + final Iq publication = new Iq(Iq.Type.SET); + publication.setTo(conversation.getJid().asBareJid()); + publication.addChild(vcard); + sendIqPacket( + account, + publication, + (publicationResponse) -> { + if (publicationResponse.getType() == Iq.Type.RESULT) { + callback.onAvatarPublicationSucceeded(); + } else { + Log.d( + Config.LOGTAG, + "failed to publish vcard " + + publicationResponse.getErrorCondition()); + callback.onAvatarPublicationFailed( + R.string.error_publish_avatar_server_reject); + } + }); } else { - Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition()); - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); + Log.d(Config.LOGTAG, "failed to request vcard " + response); + callback.onAvatarPublicationFailed( + R.string.error_publish_avatar_no_server_support); } }); - } else { - Log.d(Config.LOGTAG, "failed to request vcard " + response); - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support); - } - }); } - public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) { + public void publishAvatar( + final Account account, + final Avatar avatar, + final boolean open, + final OnAvatarPublication callback) { final Bundle options; if (account.getXmppConnection().getFeatures().pepPublishOptions()) { - options = PublishOptions.openAccess(); + options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess(); } else { options = null; } publishAvatar(account, avatar, options, true, callback); } - 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); + 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); 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); - } + 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); + @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); + } } }); - } 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 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); - } + public void publishAvatarMetadata( + Account account, + final Avatar avatar, + final Bundle options, + final boolean retry, + final OnAvatarPublication callback) { + 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); + } - @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); + @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); + } } }); - } else { - if (callback != null) { - callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject); - } - } - }); } - public void republishAvatarIfNeeded(Account account) { + public void republishAvatarIfNeeded(final Account account) { if (account.getAxolotlService().isPepBroken()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": skipping republication of avatar because pep is broken"); return; } final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null); - this.sendIqPacket(account, packet, new Consumer() { - - private Avatar parseAvatar(Iq packet) { - Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); - if (pubsub != null) { - Element items = pubsub.findChild("items"); - if (items != null) { - return Avatar.parseMetadata(items); + this.sendIqPacket( + account, + packet, + new Consumer() { + + private Avatar parseAvatar(Iq packet) { + Element pubsub = + packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); + if (pubsub != null) { + Element items = pubsub.findChild("items"); + if (items != null) { + return Avatar.parseMetadata(items); + } + } + return null; } - } - return null; - } - private boolean errorIsItemNotFound(Iq packet) { - Element error = packet.findChild("error"); - return packet.getType() == Iq.Type.ERROR - && error != null - && error.hasChild("item-not-found"); - } + private boolean errorIsItemNotFound(Iq packet) { + Element error = packet.findChild("error"); + return packet.getType() == Iq.Type.ERROR + && error != null + && error.hasChild("item-not-found"); + } - @Override - 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()); - if (avatar != null) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing"); - publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null); - } else { - Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar"); + @Override + public void accept(final Iq packet) { + if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) { + final Avatar serverAvatar = parseAvatar(packet); + if (serverAvatar == null && account.getAvatar() != null) { + final Avatar avatar = + fileBackend.getStoredPepAvatar(account.getAvatar()); + if (avatar != null) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": avatar on server was null. republishing"); + // publishing as 'open' - old server (that requires + // republication) likely doesn't support access models anyway + publishAvatar( + account, + fileBackend.getStoredPepAvatar(account.getAvatar()), + true, + null); + } else { + Log.e( + Config.LOGTAG, + account.getJid().asBareJid() + + ": error rereading avatar"); + } + } } } - } - } - }); + }); } public void cancelAvatarFetches(final Account account) { synchronized (mInProgressAvatarFetches) { - for (final Iterator iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) { + for (final Iterator iterator = mInProgressAvatarFetches.iterator(); + iterator.hasNext(); ) { final String KEY = iterator.next(); if (KEY.startsWith(account.getJid().asBareJid() + "_")) { iterator.remove(); @@ -4618,7 +5587,8 @@ public class XmppConnectionService extends Service { fetchAvatar(account, avatar, null); } - public void fetchAvatar(Account account, final Avatar avatar, final UiCallback callback) { + public void fetchAvatar( + Account account, final Avatar avatar, final UiCallback callback) { if (databaseBackend.isBlockedMedia(avatar.cid())) { if (callback != null) callback.error(0, null); return; @@ -4640,154 +5610,200 @@ public class XmppConnectionService extends Service { } else if (avatar.origin == Avatar.Origin.PEP) { mOmittedPepAvatarFetches.add(KEY); } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": already fetching " + avatar.origin + " avatar for " + avatar.owner); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": already fetching " + + avatar.origin + + " avatar for " + + avatar.owner); } } } - private void fetchAvatarPep(final Account account, final Avatar avatar, final UiCallback callback) { + 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(account, avatar)); - } - 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 (account.getJid().asBareJid().equals(avatar.owner)) { - if (account.setAvatar(avatar.getFilename())) { - databaseBackend.updateAccount(account); + sendIqPacket( + account, + packet, + (result) -> { + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); + } + 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 (account.getJid().asBareJid().equals(avatar.owner)) { + if (account.setAvatar(avatar.getFilename())) { + databaseBackend.updateAccount(account); + } + getAvatarService().clear(account); + updateConversationUi(); + updateAccountUi(); + } else { + final Contact contact = + account.getRoster().getContact(avatar.owner); + contact.setAvatar(avatar); + syncRoster(account); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(UpdateRosterReason.AVATAR); + } + if (callback != null) { + callback.success(avatar); + } + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": successfully fetched pep avatar for " + + avatar.owner); + return; } - getAvatarService().clear(account); - updateConversationUi(); - updateAccountUi(); } else { - final Contact contact = account.getRoster().getContact(avatar.owner); - contact.setAvatar(avatar); - syncRoster(account); - getAvatarService().clear(contact); - updateConversationUi(); - updateRosterUi(UpdateRosterReason.AVATAR); + + Log.d(Config.LOGTAG, ERROR + "(parsing error)"); } - if (callback != null) { - callback.success(avatar); + } else { + Element error = result.findChild("error"); + if (error == null) { + Log.d(Config.LOGTAG, ERROR + "(server error)"); + } else { + Log.d(Config.LOGTAG, ERROR + error.toString()); } - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully fetched pep avatar for " + avatar.owner); - return; } - } else { - - Log.d(Config.LOGTAG, ERROR + "(parsing error)"); - } - } else { - Element error = result.findChild("error"); - if (error == null) { - Log.d(Config.LOGTAG, ERROR + "(server error)"); - } else { - Log.d(Config.LOGTAG, ERROR + error.toString()); - } - } - if (callback != null) { - callback.error(0, null); - } - - }); + if (callback != null) { + callback.error(0, null); + } + }); } - private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { + private void fetchAvatarVcard( + final Account account, final Avatar avatar, final UiCallback callback) { 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 { - 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); + 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 { + 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); + } + } + } } } } } - } - } - }); + }); } 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); + 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); + } + getAvatarService().clear(account); + callback.success(avatar); + } else { + fetchAvatarPep(account, avatar, callback); + } + return; } - getAvatarService().clear(account); - callback.success(avatar); - } else { - fetchAvatarPep(account, avatar, callback); } - return; } } - } - } - callback.error(0, null); - }); + callback.error(0, null); + }); } public void notifyAccountAvatarHasChanged(final Account account) { final XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().bookmarksConversion()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar changed. resending presence to online group chats"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": avatar changed. resending presence to online group chats"); for (Conversation conversation : conversations) { if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) { presenceToMuc(conversation); @@ -4843,7 +5859,8 @@ public class XmppConnectionService extends Service { mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation)); } - private void reconnectAccount(final Account account, final boolean force, final boolean interactive) { + private void reconnectAccount( + final Account account, final boolean force, final boolean interactive) { synchronized (account) { final XmppConnection existingConnection = account.getXmppConnection(); final XmppConnection connection; @@ -4886,8 +5903,15 @@ public class XmppConnectionService extends Service { } public void invite(final Conversation conversation, final Jid contact) { - Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid()); - final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid()); + Log.d( + Config.LOGTAG, + conversation.getAccount().getJid().asBareJid() + + ": inviting " + + contact + + " to " + + conversation.getJid().asBareJid()); + final MucOptions.User user = + conversation.getMucOptions().findUserByRealJid(contact.asBareJid()); if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) { changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null); } @@ -4903,21 +5927,29 @@ public class XmppConnectionService extends Service { public void resetSendingToWaiting(Account account) { for (Conversation conversation : getConversations()) { if (conversation.getAccount() == account) { - conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING)); + conversation.findUnsentTextMessages( + message -> markMessage(message, Message.STATUS_WAITING)); } } } - public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { + public Message markMessage( + final Account account, final Jid recipient, final String uuid, final int status) { return markMessage(account, recipient, uuid, status, null); } - public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) { + public Message markMessage( + final Account account, + final Jid recipient, + final String uuid, + final int status, + String errorMessage) { if (uuid == null) { return null; } for (Conversation conversation : getConversations()) { - if (conversation.getJid().asBareJid().equals(recipient) && conversation.getAccount() == account) { + if (conversation.getJid().asBareJid().equals(recipient) + && conversation.getAccount() == account) { final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid); if (message != null) { markMessage(message, status, errorMessage); @@ -4928,7 +5960,11 @@ public class XmppConnectionService extends Service { return null; } - public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId) { + public boolean markMessage( + final Conversation conversation, + final String uuid, + final int status, + final String serverMessageId) { return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null); } @@ -4968,14 +6004,19 @@ public class XmppConnectionService extends Service { markMessage(message, status, null); } - public void markMessage(final Message message, final int status, final String errorMessage) { markMessage(message, status, errorMessage, false); } - public void markMessage(final Message message, final int status, final String errorMessage, final boolean includeBody) { + public void markMessage( + final Message message, + final int status, + final String errorMessage, + final boolean includeBody) { final int oldStatus = message.getStatus(); - if (status == Message.STATUS_SEND_FAILED && (oldStatus == Message.STATUS_SEND_RECEIVED || oldStatus == Message.STATUS_SEND_DISPLAYED)) { + if (status == Message.STATUS_SEND_FAILED + && (oldStatus == Message.STATUS_SEND_RECEIVED + || oldStatus == Message.STATUS_SEND_DISPLAYED)) { return; } if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) { @@ -4995,7 +6036,10 @@ public class XmppConnectionService extends Service { } public long getAutomaticMessageDeletionDate() { - final long timeout = getLongPreference(AppSettings.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion); + final long timeout = + getLongPreference( + AppSettings.AUTOMATIC_MESSAGE_DELETION, + R.integer.automatic_message_deletion); return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000)); } @@ -5033,7 +6077,7 @@ public class XmppConnectionService extends Service { } public boolean showExtendedConnectionOptions() { - return getBooleanPreference("show_connection_options", R.bool.show_connection_options); + return getBooleanPreference(AppSettings.SHOW_CONNECTION_OPTIONS, R.bool.show_connection_options); } public boolean broadcastLastActivity() { @@ -5048,7 +6092,6 @@ public class XmppConnectionService extends Service { return count; } - private List threadSafeList(Set set) { synchronized (LISTENER_LOCK) { return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set); @@ -5071,14 +6114,22 @@ public class XmppConnectionService extends Service { } } - public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) { - for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { + public void notifyJingleRtpConnectionUpdate( + final Account account, + final Jid with, + final String sessionId, + final RtpEndUserState state) { + for (OnJingleRtpConnectionUpdate listener : + threadSafeList(this.onJingleRtpConnectionUpdate)) { listener.onJingleRtpConnectionUpdate(account, with, sessionId, state); } } - public void notifyJingleRtpConnectionUpdate(CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) { + public void notifyJingleRtpConnectionUpdate( + CallIntegration.AudioDevice selectedAudioDevice, + Set availableAudioDevices) { + for (OnJingleRtpConnectionUpdate listener : + threadSafeList(this.onJingleRtpConnectionUpdate)) { listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices); } } @@ -5103,8 +6154,12 @@ public class XmppConnectionService extends Service { public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) { if (mOnCaptchaRequested.size() > 0) { DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics(); - Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity), - (int) (captcha.getHeight() * metrics.scaledDensity), false); + Bitmap scaled = + Bitmap.createScaledBitmap( + captcha, + (int) (captcha.getWidth() * metrics.scaledDensity), + (int) (captcha.getHeight() * metrics.scaledDensity), + false); for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) { listener.onCaptchaRequested(account, id, data, scaled); } @@ -5161,7 +6216,10 @@ public class XmppConnectionService extends Service { public Conversation findUniqueConversationByJid(XmppUri xmppUri) { List findings = new ArrayList<>(); for (Conversation c : getConversations()) { - if (c.getAccount().isEnabled() && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) { + if (c.getAccount().isEnabled() + && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid()) + && ((c.getMode() == Conversational.MODE_MULTI) + == xmppUri.isAction(XmppUri.ACTION_JOIN))) { findings.add(c); } } @@ -5176,17 +6234,19 @@ public class XmppConnectionService extends Service { markRead(conversation, null, true); } - public List markRead(final Conversation conversation, String upToUuid, boolean dismiss) { + public List markRead( + final Conversation conversation, String upToUuid, boolean dismiss) { if (dismiss) { mNotificationService.clear(conversation); } final List readMessages = conversation.markRead(upToUuid); if (readMessages.size() > 0) { - Runnable runnable = () -> { - for (Message message : readMessages) { - databaseBackend.updateMessage(message, false); - } - }; + Runnable runnable = + () -> { + for (Message message : readMessages) { + databaseBackend.updateMessage(message, false); + } + }; mDatabaseWriterExecutor.execute(runnable); updateConversationUi(); updateUnreadCountBadge(); @@ -5259,7 +6319,7 @@ public class XmppConnectionService extends Service { if (!last.isPrivateMessage()) { packet.setTo(packet.getTo().asBareJid()); } - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server assisted "+packet); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet); this.sendMessagePacket(account, packet); } else { publishMds(last); @@ -5299,12 +6359,15 @@ public class XmppConnectionService extends Service { } else { itemId = conversation.getJid().asBareJid(); } - Log.d(Config.LOGTAG,"publishing mds for "+itemId+"/"+stanzaId); + Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId); publishMds(account, itemId, stanzaId, conversation); } private void publishMds( - final Account account, final Jid itemId, final String stanzaId, final Conversation conversation) { + final Account account, + final Jid itemId, + final String stanzaId, + final Conversation conversation) { final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation); pushNodeAndEnforcePublishOptions( account, @@ -5317,18 +6380,42 @@ public class XmppConnectionService extends Service { public boolean sendReactions(final Message message, final Collection reactions) { if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented"); if (message.getConversation() instanceof Conversation conversation) { + final var isPrivateMessage = message.isPrivateMessage(); + final Jid reactTo; + final boolean typeGroupChat; final String reactToId; final Collection combinedReactions; final var newReactions = new HashSet<>(reactions); newReactions.removeAll(message.getAggregatedReactions().ourReactions); - if (conversation.getMode() == Conversational.MODE_MULTI) { - final var self = conversation.getMucOptions().getSelf(); + if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) { + final var mucOptions = conversation.getMucOptions(); + if (!mucOptions.participating()) { + Log.d(Config.LOGTAG, "not participating in MUC"); + return false; + } + final var self = mucOptions.getSelf(); final String occupantId = self.getOccupantId(); + if (Strings.isNullOrEmpty(occupantId)) { + Log.d(Config.LOGTAG, "occupant id not found for reaction in MUC"); + return false; + } + final var existingRaw = + ImmutableSet.copyOf( + Collections2.transform(message.getReactions(), r -> r.reaction)); + final var reactionsAsExistingVariants = + ImmutableSet.copyOf( + Collections2.transform( + reactions, r -> Emoticons.existingVariant(r, existingRaw))); + if (!reactions.equals(reactionsAsExistingVariants)) { + Log.d(Config.LOGTAG, "modified reactions to existing variants"); + } reactToId = message.getServerMsgId(); + reactTo = conversation.getJid().asBareJid(); + typeGroupChat = true; combinedReactions = Reaction.withMine( message.getReactions(), - reactions, + reactionsAsExistingVariants, false, self.getFullJid(), conversation.getAccount().getJid(), @@ -5340,6 +6427,12 @@ public class XmppConnectionService extends Service { } else { reactToId = message.getUuid(); } + typeGroupChat = false; + if (isPrivateMessage) { + reactTo = message.getCounterpart(); + } else { + reactTo = conversation.getJid().asBareJid(); + } combinedReactions = Reaction.withFrom( message.getReactions(), @@ -5348,11 +6441,12 @@ public class XmppConnectionService extends Service { conversation.getAccount().getJid(), null); } - if (Strings.isNullOrEmpty(reactToId)) { + if (reactTo == null || Strings.isNullOrEmpty(reactToId)) { return false; } + final var packet = - mMessageGenerator.reaction(conversation, message, reactToId, reactions); + mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions); final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n"; final var body = quote + String.join(" ", newReactions); @@ -5430,7 +6524,10 @@ public class XmppConnectionService extends Service { } } if (Config.QUICKSY_DOMAIN != null) { - hosts.remove(Config.QUICKSY_DOMAIN.toEscapedString()); //we only want to show this when we type a e164 number + hosts.remove( + Config.QUICKSY_DOMAIN + .toEscapedString()); // we only want to show this when we type a e164 + // number } if (Config.MAGIC_CREATE_DOMAIN != null) { hosts.add(Config.MAGIC_CREATE_DOMAIN); @@ -5456,14 +6553,18 @@ public class XmppConnectionService extends Service { return mucServers; } - public void sendMessagePacket(final Account account, final im.conversations.android.xmpp.model.stanza.Message 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(final Account account, final im.conversations.android.xmpp.model.stanza.Presence packet) { + 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); @@ -5504,8 +6605,10 @@ public class XmppConnectionService extends Service { } 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)); + long since = + Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates + packet.addChild("idle", Namespace.IDLE) + .setAttribute("since", AbstractGenerator.getTimestamp(since)); } sendPresencePacket(account, packet); } @@ -5533,8 +6636,6 @@ public class XmppConnectionService extends Service { } } - - private void sendOfflinePresence(final Account account) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence"); sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); @@ -5572,7 +6673,8 @@ public class XmppConnectionService extends Service { ArrayList contacts = new ArrayList<>(); for (Account account : getAccounts()) { if ((account.isEnabled() || accountJid != null) - && (accountJid == null || accountJid.equals(account.getJid().asBareJid().toString()))) { + && (accountJid == null + || accountJid.equals(account.getJid().asBareJid().toString()))) { Contact contact = account.getRoster().getContactFromContactList(jid); if (contact != null) { contacts.add(contact); @@ -5606,23 +6708,11 @@ public class XmppConnectionService extends Service { } public void resendFailedMessages(final Message message) { - final Collection messages = new ArrayList<>(); - Message current = message; - while (current.getStatus() == Message.STATUS_SEND_FAILED) { - messages.add(current); - if (current.mergeable(current.next())) { - current = current.next(); - } else { - break; - } - } - for (final Message msg : messages) { - msg.setTime(System.currentTimeMillis()); - markMessage(msg, Message.STATUS_WAITING); - this.resendMessage(msg, false); - } - if (message.getConversation() instanceof Conversation) { - ((Conversation) message.getConversation()).sort(); + message.setTime(System.currentTimeMillis()); + markMessage(message, Message.STATUS_WAITING); + this.resendMessage(message, false); + if (message.getConversation() instanceof Conversation c) { + c.sort(); } updateConversationUi(); } @@ -5639,25 +6729,30 @@ public class XmppConnectionService extends Service { reference = null; } conversation.clearMessages(); - conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam conversation.setLastClearHistory(clearDate, reference); - Runnable runnable = () -> { - databaseBackend.deleteMessagesInConversation(conversation); - databaseBackend.updateConversation(conversation); - }; + Runnable runnable = + () -> { + databaseBackend.deleteMessagesInConversation(conversation); + databaseBackend.updateConversation(conversation); + }; mDatabaseWriterExecutor.execute(runnable); } - public boolean sendBlockRequest(final Blockable blockable, final boolean reportSpam, final String serverMsgId) { + 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(account, getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), (response) -> { - if (response.getType() == Iq.Type.RESULT) { - account.getBlocklist().add(jid); - updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); - } - }); + this.sendIqPacket( + account, + getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId), + (response) -> { + if (response.getType() == Iq.Type.RESULT) { + account.getBlocklist().add(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } + }); if (blockable.getBlockedJid().isFullJid()) { return false; } else if (removeBlockedConversations(blockable.getAccount(), jid)) { @@ -5676,15 +6771,24 @@ public class XmppConnectionService extends Service { synchronized (this.conversations) { boolean domainJid = blockedJid.getLocal() == null; for (Conversation conversation : this.conversations) { - boolean jidMatches = (domainJid && blockedJid.getDomain().equals(conversation.getJid().getDomain())) - || blockedJid.equals(conversation.getJid().asBareJid()); + boolean jidMatches = + (domainJid + && blockedJid + .getDomain() + .equals(conversation.getJid().getDomain())) + || blockedJid.equals(conversation.getJid().asBareJid()); if (conversation.getAccount() == account && conversation.getMode() == Conversation.MODE_SINGLE && jidMatches) { this.conversations.remove(conversation); markRead(conversation); conversation.setStatus(Conversation.STATUS_ARCHIVED); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conversation " + conversation.getJid().asBareJid() + " because jid was blocked"); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": archiving conversation " + + conversation.getJid().asBareJid() + + " because jid was blocked"); updateConversation(conversation); removed = true; } @@ -5697,12 +6801,15 @@ public class XmppConnectionService extends Service { if (blockable != null && blockable.getJid() != null) { final var account = blockable.getAccount(); final Jid jid = blockable.getBlockedJid(); - this.sendIqPacket(account, getIqGenerator().generateSetUnblockRequest(jid), response -> { - if (response.getType() == Iq.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); + } + }); } } @@ -5715,11 +6822,18 @@ public class XmppConnectionService extends Service { request = mIqGenerator.publishNick(displayName); } mAvatarService.clear(account); - sendIqPacket(account, request, (packet) -> { - if (packet.getType() == Iq.Type.ERROR) { - Log.d(Config.LOGTAG, account.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); + } + }); } public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair key) { @@ -5783,27 +6897,55 @@ public class XmppConnectionService extends Service { if (node != null && ver != null) { 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, (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response); - if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) { - databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult(account.getRoster(), presence == null ? null : presence.getHash(), presence == null ? null : presence.getVer(), jid.getResource(), discoveryResult); - if (discoveryResult.hasIdentity("gateway", "pstn")) { - final Contact contact = account.getRoster().getContact(jid); - contact.registerAsPhoneAccount(this); - mQuickConversationsService.considerSyncBackground(false); + + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": making disco request for " + + key.second + + " to " + + jid); + sendIqPacket( + account, + request, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { + final ServiceDiscoveryResult discoveryResult = + new ServiceDiscoveryResult(response); + if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) { + databaseBackend.insertDiscoveryResult(discoveryResult); + injectServiceDiscoveryResult( + account.getRoster(), + presence == null ? null : presence.getHash(), + presence == null ? null : presence.getVer(), + jid.getResource(), + discoveryResult); + if (discoveryResult.hasIdentity("gateway", "pstn")) { + final Contact contact = account.getRoster().getContact(jid); + contact.registerAsPhoneAccount(this); + mQuickConversationsService.considerSyncBackground(false); + } + updateConversationUi(true); + if (cb != null) cb.run(); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": mismatch in caps for contact " + + jid + + " " + + presence.getVer() + + " vs " + + discoveryResult.getVer()); + } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": unable to fetch caps from " + + jid); } - updateConversationUi(true); - if (cb != null) cb.run(); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer()); - } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid); - } - }); + }); } } @@ -5812,7 +6954,8 @@ public class XmppConnectionService extends Service { sendIqPacket(account, request, callback); } - private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) { + private void injectServiceDiscoveryResult( + Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) { boolean rosterNeedsSync = false; for (final Contact contact : roster.getContacts()) { boolean serviceDiscoverySet = false; @@ -5848,15 +6991,17 @@ public class XmppConnectionService extends Service { final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); final Iq request = new Iq(Iq.Type.GET); request.addChild("prefs", version.namespace); - sendIqPacket(account, request, (packet) -> { - final Element prefs = packet.findChild("prefs", version.namespace); - if (packet.getType() == Iq.Type.RESULT && prefs != null) { - account.setMamPrefs(prefs); - if (callback != null) callback.onPreferencesFetched(prefs); - } else { - if (callback != null) callback.onPreferencesFetchFailed(); - } - }); + 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(); + } + }); } public PushManagementService getPushManagementService() { @@ -5906,11 +7051,13 @@ public class XmppConnectionService extends Service { for (XmppUri.Fingerprint fp : fingerprints) { if (fp.type == XmppUri.FingerprintType.OMEMO) { String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); - FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); + FingerprintStatus fingerprintStatus = + axolotlService.getFingerprintTrust(fingerprint); if (fingerprintStatus != null) { if (!fingerprintStatus.isVerified()) { performedVerification = true; - axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); + axolotlService.setFingerprintTrust( + fingerprint, fingerprintStatus.toVerified()); } } else { axolotlService.preVerifyFingerprint(contact, fingerprint); @@ -5927,10 +7074,12 @@ public class XmppConnectionService extends Service { if (fp.type == XmppUri.FingerprintType.OMEMO) { String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", ""); Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint); - FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint); + FingerprintStatus fingerprintStatus = + axolotlService.getFingerprintTrust(fingerprint); if (fingerprintStatus != null) { if (!fingerprintStatus.isVerified()) { - axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); + axolotlService.setFingerprintTrust( + fingerprint, fingerprintStatus.toVerified()); verifiedSomething = true; } } else { @@ -6013,9 +7162,15 @@ public class XmppConnectionService extends Service { } public interface OnJingleRtpConnectionUpdate { - void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state); + void onJingleRtpConnectionUpdate( + final Account account, + final Jid with, + final String sessionId, + final RtpEndUserState state); - void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set availableAudioDevices); + void onAudioDeviceChanged( + CallIntegration.AudioDevice selectedAudioDevice, + Set availableAudioDevices); } public interface OnAccountUpdate { @@ -6080,9 +7235,9 @@ public class XmppConnectionService extends Service { public void onReceive(final Context context, final Intent intent) { final String action = intent == null ? null : intent.getAction(); if (allowedActions.contains(action)) { - onStartCommand(intent,0,0); + onStartCommand(intent, 0, 0); } else { - Log.e(Config.LOGTAG,"restricting broadcast of event "+action); + Log.e(Config.LOGTAG, "restricting broadcast of event " + action); } } } @@ -6092,7 +7247,8 @@ public class XmppConnectionService extends Service { public final Set media; public final boolean reconnecting; - public OngoingCall(AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { + public OngoingCall( + AbstractJingleConnection.Id id, Set media, final boolean reconnecting) { this.id = id; this.media = media; this.reconnecting = reconnecting; @@ -6103,7 +7259,9 @@ public class XmppConnectionService extends Service { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; OngoingCall that = (OngoingCall) o; - return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media); + return reconnecting == that.reconnecting + && Objects.equal(id, that.id) + && Objects.equal(media, that.media); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/AddReactionActivity.java b/src/main/java/eu/siacs/conversations/ui/AddReactionActivity.java new file mode 100644 index 0000000000000000000000000000000000000000..709202f659dbb772ba872dc888399dac543bf88e --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/AddReactionActivity.java @@ -0,0 +1,68 @@ +package eu.siacs.conversations.ui; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.databinding.DataBindingUtil; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.ActivityAddReactionBinding; + +public class AddReactionActivity extends XmppActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final ActivityAddReactionBinding binding = + DataBindingUtil.setContentView(this, R.layout.activity_add_reaction); + Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); + + setSupportActionBar(binding.toolbar); + binding.toolbar.setNavigationIcon(R.drawable.ic_clear_24dp); + binding.toolbar.setNavigationOnClickListener(v -> finish()); + setTitle(R.string.add_reaction_title); + binding.emojiPicker.setOnEmojiPickedListener( + emojiViewItem -> addReaction(emojiViewItem.getEmoji())); + } + + private void addReaction(final String emoji) { + final var intent = getIntent(); + final var conversation = intent == null ? null : intent.getStringExtra("conversation"); + final var message = intent == null ? null : intent.getStringExtra("message"); + if (Strings.isNullOrEmpty(conversation) || Strings.isNullOrEmpty(message)) { + Toast.makeText(this, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); + return; + } + final var c = xmppConnectionService.findConversationByUuid(conversation); + final var m = c == null ? null : c.findMessageWithUuid(message); + if (m == null) { + Toast.makeText(this, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); + return; + } + final var aggregated = m.getAggregatedReactions(); + if (aggregated.ourReactions.contains(emoji)) { + if (!xmppConnectionService.sendReactions(m, aggregated.ourReactions)) { + Toast.makeText(this, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); + return; + } + } else { + final ImmutableSet.Builder reactionBuilder = new ImmutableSet.Builder<>(); + reactionBuilder.addAll(aggregated.ourReactions); + reactionBuilder.add(emoji); + if (!xmppConnectionService.sendReactions(m, reactionBuilder.build())) { + Toast.makeText(this, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show(); + } + } + finish(); + } + + @Override + protected void refreshUiReal() {} + + @Override + protected void onBackendConnected() {} +} diff --git a/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java b/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java index d8f73a1dcfe57ac74464aa8c12af0e7dd180c296..f659eb4e6d252e255e70c1c1523ebdc4b9e97e76 100644 --- a/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java +++ b/src/main/java/eu/siacs/conversations/ui/BindingAdapters.java @@ -9,12 +9,10 @@ import com.cheogram.android.EmojiSearch; import com.google.android.material.chip.Chip; import com.google.android.material.chip.ChipGroup; import com.google.android.material.color.MaterialColors; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.utils.UIHelper; @@ -23,33 +21,35 @@ import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.function.Function; public class BindingAdapters { public static void setReactionsOnReceived( final ChipGroup chipGroup, - final Conversation conversation, final Reaction.Aggregated reactions, final Consumer> onModifiedReactions, + final Function>, Boolean> onDetailsClicked, final Consumer onCustomReaction, final Consumer onCustomReactionRemove, final Runnable addReaction) { - setReactions(chipGroup, conversation, reactions, true, onModifiedReactions, onCustomReaction, onCustomReactionRemove, addReaction); + setReactions(chipGroup, reactions, true, onModifiedReactions, onDetailsClicked, onCustomReaction, onCustomReactionRemove, addReaction); } public static void setReactionsOnSent( final ChipGroup chipGroup, final Reaction.Aggregated reactions, - final Consumer> onModifiedReactions) { - setReactions(chipGroup, null, reactions, false, onModifiedReactions, null, null, null); + final Consumer> onModifiedReactions, + final Function>, Boolean> onDetailsClicked) { + setReactions(chipGroup, reactions, false, onModifiedReactions, onDetailsClicked, null, null, null); } private static void setReactions( final ChipGroup chipGroup, - final Conversation conversation, final Reaction.Aggregated aggregated, final boolean onReceived, final Consumer> onModifiedReactions, + final Function>, Boolean> onDetailsClicked, final Consumer onCustomReaction, final Consumer onCustomReactionRemove, final Runnable addReaction) { @@ -113,14 +113,7 @@ public class BindingAdapters { } } }); - chip.setOnLongClickListener(v -> { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); - builder.setTitle(emoji.toString()); - builder.setMessage(reaction.getValue().stream().map(r -> UIHelper.getDisplayName(conversation, r)).collect(Collectors.joining("\n"))); - builder.setPositiveButton(context.getResources().getString(R.string.ok), null); - builder.create().show(); - return true; - }); + chip.setOnLongClickListener(v -> onDetailsClicked.apply(reaction)); chipGroup.addView(chip); } if (addReaction != null) { diff --git a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java index d5f6b4d9471a3f3dc58d07b849ee7a0585a13bd4..b5a7e6d4ceadfaaabc958e00c80aed7a92825889 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChannelDiscoveryActivity.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui; -import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -17,11 +16,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; - import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Strings; @@ -35,7 +32,6 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityChannelDiscoveryBinding; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Room; import eu.siacs.conversations.services.ChannelDiscoveryService; @@ -46,7 +42,11 @@ import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.xmpp.Jid; -public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.OnActionExpandListener, TextView.OnEditorActionListener, ChannelDiscoveryService.OnChannelSearchResultsFound, ChannelSearchResultAdapter.OnChannelSearchResultSelected { +public class ChannelDiscoveryActivity extends XmppActivity + implements MenuItem.OnActionExpandListener, + TextView.OnEditorActionListener, + ChannelDiscoveryService.OnChannelSearchResultsFound, + ChannelSearchResultAdapter.OnChannelSearchResultSelected { private static final String CHANNEL_DISCOVERY_OPT_IN = "channel_discovery_opt_in"; @@ -63,9 +63,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private boolean optedIn = false; @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override protected void onBackendConnected() { @@ -101,7 +99,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O this.adapter.setOnChannelSearchResultSelectedListener(this); this.optedIn = getPreferences().getBoolean(CHANNEL_DISCOVERY_OPT_IN, false); - final String search = savedInstanceState == null ? null : savedInstanceState.getString("search"); + final String search = + savedInstanceState == null ? null : savedInstanceState.getString("search"); if (search != null) { mInitialSearchValue.push(search); } @@ -111,14 +110,17 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private ChannelDiscoveryService.Method getMethod(final Context c) { if (this.mucServices != null) return ChannelDiscoveryService.Method.LOCAL_SERVER; - if ( Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { + if (Strings.isNullOrEmpty(Config.CHANNEL_DISCOVERY)) { return ChannelDiscoveryService.Method.LOCAL_SERVER; } if (QuickConversationsService.isQuicksy()) { return ChannelDiscoveryService.Method.JABBER_NETWORK; } final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(c); - final String m = p.getString("channel_discovery_method", c.getString(R.string.default_channel_discovery)); + final String m = + p.getString( + "channel_discovery_method", + c.getString(R.string.default_channel_discovery)); try { return ChannelDiscoveryService.Method.valueOf(m); } catch (IllegalArgumentException e) { @@ -139,7 +141,8 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O mMenuSearchView.expandActionView(); mSearchEditText.append(initialSearchValue); mSearchEditText.requestFocus(); - if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) && xmppConnectionService != null) { + if ((optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) + && xmppConnectionService != null) { xmppConnectionService.discoverChannels(initialSearchValue, this.method, this.mucServices, this); } } @@ -150,18 +153,22 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O @Override public boolean onMenuItemActionExpand(@NonNull MenuItem item) { - mSearchEditText.post(() -> { - mSearchEditText.requestFocus(); - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); - }); + mSearchEditText.post( + () -> { + mSearchEditText.requestFocus(); + final InputMethodManager imm = + (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT); + }); return true; } @Override public boolean onMenuItemActionCollapse(@NonNull MenuItem item) { - final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); + final InputMethodManager imm = + (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow( + mSearchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); mSearchEditText.setText(""); toggleLoadingScreen(); if (optedIn || method == ChannelDiscoveryService.Method.LOCAL_SERVER) { @@ -173,7 +180,9 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private void toggleLoadingScreen() { adapter.submitList(Collections.emptyList()); binding.progressBar.setVisibility(View.VISIBLE); - binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface)); + binding.list.setBackgroundColor( + MaterialColors.getColor( + binding.list, com.google.android.material.R.attr.colorSurface)); } @Override @@ -188,13 +197,14 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O builder.setPositiveButton(R.string.confirm, (dialog, which) -> optIn()); builder.setOnCancelListener(dialog -> finish()); final androidx.appcompat.app.AlertDialog dialog = builder.create(); - dialog.setOnShowListener(d -> { - final TextView textView = dialog.findViewById(android.R.id.message); - if (textView == null) { - return; - } - textView.setMovementMethod(LinkMovementMethod.getInstance()); - }); + dialog.setOnShowListener( + d -> { + final TextView textView = dialog.findViewById(android.R.id.message); + if (textView == null) { + return; + } + textView.setMovementMethod(LinkMovementMethod.getInstance()); + }); dialog.setCanceledOnTouchOutside(false); dialog.show(); holdLoading(); @@ -204,13 +214,17 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O private void holdLoading() { adapter.submitList(Collections.emptyList()); binding.progressBar.setVisibility(View.GONE); - binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface)); + binding.list.setBackgroundColor( + MaterialColors.getColor( + binding.list, com.google.android.material.R.attr.colorSurface)); } @Override public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 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); } @@ -235,16 +249,20 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O @Override public void onChannelSearchResultsFound(final List results) { - runOnUiThread(() -> { - adapter.submitList(results); - binding.progressBar.setVisibility(View.GONE); - if (results.isEmpty()) { - binding.list.setBackground(ContextCompat.getDrawable(this,R.drawable.background_no_results)); - } else { - binding.list.setBackgroundColor(MaterialColors.getColor(binding.list, com.google.android.material.R.attr.colorSurface)); - } - }); - + runOnUiThread( + () -> { + adapter.submitList(results); + binding.progressBar.setVisibility(View.GONE); + if (results.isEmpty()) { + binding.list.setBackground( + ContextCompat.getDrawable(this, R.drawable.background_no_results)); + } else { + binding.list.setBackgroundColor( + MaterialColors.getColor( + binding.list, + com.google.android.material.R.attr.colorSurface)); + } + }); } @Override @@ -258,12 +276,16 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O final AtomicReference account = new AtomicReference<>(accounts.get(0)); final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.choose_account); - builder.setSingleChoiceItems(accounts.toArray(new CharSequence[0]), 0, (dialog, which) -> account.set(accounts.get(which))); - builder.setPositiveButton(R.string.join, (dialog, which) -> joinChannelSearchResult(account.get(), result)); + builder.setSingleChoiceItems( + accounts.toArray(new CharSequence[0]), + 0, + (dialog, which) -> account.set(accounts.get(which))); + builder.setPositiveButton( + R.string.join, + (dialog, which) -> joinChannelSearchResult(account.get(), result)); builder.setNegativeButton(R.string.cancel, null); builder.create().show(); } - } @Override @@ -294,17 +316,7 @@ public class ChannelDiscoveryActivity extends XmppActivity implements MenuItem.O final Conversation conversation = xmppConnectionService.findOrCreateConversation( account, result.getRoom(), true, true, true); - final var existingBookmark = conversation.getBookmark(); - if (existingBookmark == null) { - final var bookmark = new Bookmark(account, conversation.getJid().asBareJid()); - bookmark.setAutojoin(true); - xmppConnectionService.createBookmark(account, bookmark); - } else { - if (!existingBookmark.autojoin()) { - existingBookmark.setAutojoin(true); - xmppConnectionService.createBookmark(account, existingBookmark); - } - } + xmppConnectionService.ensureBookmarkIsAutoJoin(conversation); switchToConversation(conversation); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 863d7e8da7087f61084001d3ba31dd4b0df27e75..8843c3cb2670ed8125dad6cb39a01c50ac21035a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -1,5 +1,8 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.entities.Bookmark.printableValue; +import static eu.siacs.conversations.utils.StringUtils.changed; + import android.app.Activity; import android.app.PendingIntent; import android.content.Context; @@ -7,6 +10,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.Editable; @@ -31,6 +35,7 @@ import androidx.databinding.DataBindingUtil; import com.cheogram.android.Util; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.color.MaterialColors; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; @@ -77,14 +82,17 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.utils.XEP0392Helper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; -import me.drakeet.support.toast.ToastCompat; - -import static eu.siacs.conversations.entities.Bookmark.printableValue; -import static eu.siacs.conversations.utils.StringUtils.changed; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import me.drakeet.support.toast.ToastCompat; -public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnConfigurationPushed, XmppConnectionService.OnRoomDestroy, TextWatcher, OnMediaLoaded { +public class ConferenceDetailsActivity extends XmppActivity + implements OnConversationUpdate, + OnMucRosterUpdate, + XmppConnectionService.OnAffiliationChanged, + XmppConnectionService.OnConfigurationPushed, + XmppConnectionService.OnRoomDestroy, + TextWatcher, + OnMediaLoaded { public static final String ACTION_VIEW_MUC = "view_muc"; private Conversation mConversation; @@ -96,26 +104,25 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers private boolean mAdvancedMode = false; private boolean showDynamicTags = true; - private final UiCallback renameCallback = new UiCallback() { - @Override - public void success(Conversation object) { - displayToast(getString(R.string.your_nick_has_been_changed)); - runOnUiThread(() -> { - updateView(); - }); - - } - - @Override - public void error(final int errorCode, Conversation object) { - displayToast(getString(errorCode)); - } + private final UiCallback renameCallback = + new UiCallback() { + @Override + public void success(Conversation object) { + displayToast(getString(R.string.your_nick_has_been_changed)); + runOnUiThread( + () -> { + updateView(); + }); + } - @Override - public void userInputRequired(PendingIntent pi, Conversation object) { + @Override + public void error(final int errorCode, Conversation object) { + displayToast(getString(errorCode)); + } - } - }; + @Override + public void userInputRequired(PendingIntent pi, Conversation object) {} + }; public static void open(final Activity activity, final Conversation conversation) { Intent intent = new Intent(activity, ConferenceDetailsActivity.class); @@ -124,39 +131,49 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers activity.startActivity(intent); } - private final OnClickListener mNotifyStatusClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this); - builder.setTitle(R.string.pref_notification_settings); - String[] choices = { - getString(R.string.notify_on_all_messages), - getString(R.string.notify_only_when_highlighted), + private final OnClickListener mNotifyStatusClickListener = + new OnClickListener() { + @Override + public void onClick(View v) { + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(ConferenceDetailsActivity.this); + builder.setTitle(R.string.pref_notification_settings); + String[] choices = { + getString(R.string.notify_on_all_messages), + getString(R.string.notify_only_when_highlighted), getString(R.string.notify_only_when_highlighted_or_replied), - getString(R.string.notify_never) - }; - final AtomicInteger choice; - if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0) == Long.MAX_VALUE) { - choice = new AtomicInteger(3); - } else { - choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : (mConversation.notifyReplies() ? 2 : 1)); - } - builder.setSingleChoiceItems(choices, choice.get(), (dialog, which) -> choice.set(which)); - builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - if (choice.get() == 3) { - mConversation.setMutedTill(Long.MAX_VALUE); - } else { - mConversation.setMutedTill(0); - mConversation.setAttribute(Conversation.ATTRIBUTE_ALWAYS_NOTIFY, String.valueOf(choice.get() == 0)); - mConversation.setAttribute(Conversation.ATTRIBUTE_NOTIFY_REPLIES, String.valueOf(choice.get() == 2)); + getString(R.string.notify_never) + }; + final AtomicInteger choice; + if (mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0) + == Long.MAX_VALUE) { + choice = new AtomicInteger(3); + } else { + choice = new AtomicInteger(mConversation.alwaysNotify() ? 0 : (mConversation.notifyReplies() ? 2 : 1)); + } + builder.setSingleChoiceItems( + choices, choice.get(), (dialog, which) -> choice.set(which)); + builder.setNegativeButton(R.string.cancel, null); + builder.setPositiveButton( + R.string.ok, + (dialog, which) -> { + if (choice.get() == 3) { + mConversation.setMutedTill(Long.MAX_VALUE); + } else { + mConversation.setMutedTill(0); + mConversation.setAttribute( + Conversation.ATTRIBUTE_ALWAYS_NOTIFY, + String.valueOf(choice.get() == 0)); + mConversation.setAttribute( + Conversation.ATTRIBUTE_NOTIFY_REPLIES, + String.valueOf(choice.get() == 2)); + } + xmppConnectionService.updateConversation(mConversation); + updateView(); + }); + builder.create().show(); } - xmppConnectionService.updateConversation(mConversation); - updateView(); - }); - builder.create().show(); - } - }; + }; private final OnClickListener mChangeConferenceSettings = new OnClickListener() { @@ -217,32 +234,49 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.changeConferenceButton.setOnClickListener(this.mChangeConferenceSettings); setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar()); - this.binding.editNickButton.setOnClickListener(v -> quickEdit(mConversation.getMucOptions().getActualNick(), - R.string.nickname, - value -> { - if (xmppConnectionService.renameInMuc(mConversation, value, renameCallback)) { - return null; - } else { - return getString(R.string.invalid_muc_nick); - } - })); + this.binding.editNickButton.setOnClickListener( + v -> + quickEdit( + mConversation.getMucOptions().getActualNick(), + R.string.nickname, + value -> { + if (xmppConnectionService.renameInMuc( + mConversation, value, renameCallback)) { + return null; + } else { + return getString(R.string.invalid_muc_nick); + } + })); this.mAdvancedMode = getPreferences().getBoolean("advanced_muc_mode", false); this.binding.mucInfoMore.setVisibility(this.mAdvancedMode ? View.VISIBLE : View.GONE); this.binding.notificationStatusButton.setOnClickListener(this.mNotifyStatusClickListener); - this.binding.yourPhoto.setOnClickListener(v -> { - final MucOptions mucOptions = mConversation.getMucOptions(); - if (!mucOptions.hasVCards()) { - Toast.makeText(this, R.string.host_does_not_support_group_chat_avatars, Toast.LENGTH_SHORT).show(); - return; - } - if (!mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) { - Toast.makeText(this, R.string.only_the_owner_can_change_group_chat_avatar, Toast.LENGTH_SHORT).show(); - return; - } - final Intent intent = new Intent(this, PublishGroupChatProfilePictureActivity.class); - intent.putExtra("uuid", mConversation.getUuid()); - startActivity(intent); - }); + this.binding.yourPhoto.setOnClickListener( + v -> { + final MucOptions mucOptions = mConversation.getMucOptions(); + if (!mucOptions.hasVCards()) { + Toast.makeText( + this, + R.string.host_does_not_support_group_chat_avatars, + Toast.LENGTH_SHORT) + .show(); + return; + } + if (!mucOptions + .getSelf() + .getAffiliation() + .ranks(MucOptions.Affiliation.OWNER)) { + Toast.makeText( + this, + R.string.only_the_owner_can_change_group_chat_avatar, + Toast.LENGTH_SHORT) + .show(); + return; + } + final Intent intent = + new Intent(this, PublishGroupChatProfilePictureActivity.class); + intent.putExtra("uuid", mConversation.getUuid()); + startActivity(intent); + }); this.binding.yourPhoto.setOnLongClickListener(v -> { PopupMenu popupMenu = new PopupMenu(this, v); popupMenu.inflate(R.menu.conference_photo); @@ -267,11 +301,13 @@ 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.setContentDescription( + getString(R.string.edit_name_and_topic)); this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked); this.binding.mucEditTitle.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(this); - //this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject)); + //this.binding.mucEditSubject.addTextChangedListener( + // new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject)); this.binding.editTags.addTextChangedListener(this); this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); this.mUserPreviewAdapter = new UserPreviewAdapter(); @@ -284,11 +320,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers switchToConversation(mConversation, null, false, null, false, true, null, thread.getThreadId()); }); this.binding.invite.setOnClickListener(v -> inviteToConversation(mConversation)); - this.binding.showUsers.setOnClickListener(v -> { - Intent intent = new Intent(this, MucUsersActivity.class); - intent.putExtra("uuid", mConversation.getUuid()); - startActivity(intent); - }); + this.binding.showUsers.setOnClickListener( + v -> { + Intent intent = new Intent(this, MucUsersActivity.class); + intent.putExtra("uuid", mConversation.getUuid()); + startActivity(intent); + }); this.binding.relatedMucs.setOnClickListener(v -> { final Intent intent = new Intent(this, ChannelDiscoveryActivity.class); intent.putExtra("services", new String[]{ mConversation.getJid().getDomain().toEscapedString(), mConversation.getAccount().getJid().toEscapedString() }); @@ -299,7 +336,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override public void onStart() { super.onStart(); - binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); + binding.mediaWrapper.setVisibility( + Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); } @Override @@ -327,23 +365,43 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.mAdvancedMode = !menuItem.isChecked(); menuItem.setChecked(this.mAdvancedMode); getPreferences().edit().putBoolean("advanced_muc_mode", mAdvancedMode).apply(); - final boolean online = mConversation != null && mConversation.getMucOptions().online(); - this.binding.mucInfoMore.setVisibility(this.mAdvancedMode && online ? View.VISIBLE : View.GONE); + final boolean online = + mConversation != null && mConversation.getMucOptions().online(); + this.binding.mucInfoMore.setVisibility( + this.mAdvancedMode && online ? View.VISIBLE : View.GONE); invalidateOptionsMenu(); updateView(); break; + case R.id.action_custom_notifications: + if (mConversation != null) { + configureCustomNotifications(mConversation); + } + break; } return super.onOptionsItemSelected(menuItem); } + private void configureCustomNotifications(final Conversation conversation) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R + || conversation.getMode() != Conversation.MODE_MULTI) { + return; + } + final var shortcut = + xmppConnectionService + .getShortcutService() + .getShortcutInfo(conversation.getMucOptions()); + configureCustomNotification(shortcut); + } + @Override - public boolean onContextItemSelected(MenuItem item) { + public boolean onContextItemSelected(@NonNull final MenuItem item) { final User user = mUserPreviewAdapter.getSelectedUser(); if (user == null) { Toast.makeText(this, R.string.unable_to_perform_this_action, Toast.LENGTH_SHORT).show(); return true; } - if (!MucDetailsContextMenuHelper.onContextItemSelected(item, mUserPreviewAdapter.getSelectedUser(), this)) { + if (!MucDetailsContextMenuHelper.onContextItemSelected( + item, mUserPreviewAdapter.getSelectedUser(), this)) { return super.onContextItemSelected(item); } return true; @@ -358,7 +416,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers 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); + final boolean owner = + mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER); if (owner || printableValue(name)) { this.binding.mucEditTitle.setVisibility(View.VISIBLE); if (name != null) { @@ -410,8 +469,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.editTags.setVisibility(View.GONE); } } else { - String subject = this.binding.mucEditSubject.isEnabled() ? this.binding.mucEditSubject.getEditableText().toString().trim() : null; - String name = this.binding.mucEditTitle.isEnabled() ? this.binding.mucEditTitle.getEditableText().toString().trim() : null; + String subject = + this.binding.mucEditSubject.isEnabled() + ? this.binding.mucEditSubject.getEditableText().toString().trim() + : null; + String name = + this.binding.mucEditTitle.isEnabled() + ? this.binding.mucEditTitle.getEditableText().toString().trim() + : null; onMucInfoUpdated(subject, name); final Bookmark bookmark = mConversation.getBookmark(); @@ -430,7 +495,8 @@ 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)); + this.binding.editMucNameButton.setContentDescription( + getString(R.string.edit_name_and_topic)); } private void onMucInfoUpdated(String subject, String name) { @@ -438,7 +504,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (mucOptions.canChangeSubject() && changed(mucOptions.getSubject(), subject)) { xmppConnectionService.pushSubjectToConference(mConversation, subject); } - if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) && changed(mucOptions.getName(), name)) { + if (mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER) + && changed(mucOptions.getName(), name)) { Bundle options = new Bundle(); options.putString("muc#roomconfig_persistentroom", "1"); options.putString("muc#roomconfig_roomname", StringUtils.nullOnEmpty(name)); @@ -446,12 +513,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } } - @Override protected String getShareableUri(boolean http) { if (mConversation != null) { if (http) { - return "https://conversations.im/j/" + XmppUri.lameUrlEncode(mConversation.getJid().asBareJid().toEscapedString()); + return "https://conversations.im/j/" + + XmppUri.lameUrlEncode( + mConversation.getJid().asBareJid().toEscapedString()); } else { return "xmpp:" + Uri.encode(mConversation.getJid().asBareJid().toEscapedString(), "@/+") + "?join"; } @@ -470,7 +538,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers return true; } menuItemSaveBookmark.setVisible(mConversation.getBookmark() == null); - menuItemDestroyRoom.setVisible(mConversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)); + menuItemDestroyRoom.setVisible( + mConversation + .getMucOptions() + .getSelf() + .getAffiliation() + .ranks(MucOptions.Affiliation.OWNER)); return true; } @@ -483,32 +556,40 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final MenuItem destroy = menu.findItem(R.id.action_destroy_room); destroy.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel); AccountUtils.showHideMenuItems(menu); + final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications); + customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R); return super.onCreateOptionsMenu(menu); } @Override - public void onMediaLoaded(List attachments) { - runOnUiThread(() -> { - int limit = GridManager.getCurrentColumnCount(binding.media); - mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); - binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); - }); - + public void onMediaLoaded(final List attachments) { + runOnUiThread( + () -> { + final int limit = GridManager.getCurrentColumnCount(binding.media); + mMediaAdapter.setAttachments( + attachments.subList(0, Math.min(limit, attachments.size()))); + binding.mediaWrapper.setVisibility( + attachments.isEmpty() ? View.GONE : View.VISIBLE); + }); } - protected void saveAsBookmark() { - xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName()); + xmppConnectionService.saveConversationAsBookmark( + mConversation, mConversation.getMucOptions().getName()); } protected void destroyRoom() { final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(groupChat ? R.string.destroy_room : R.string.destroy_channel); - builder.setMessage(groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog); - builder.setPositiveButton(R.string.ok, (dialog, which) -> { - xmppConnectionService.destroyRoom(mConversation, ConferenceDetailsActivity.this); - }); + builder.setMessage( + groupChat ? R.string.destroy_room_dialog : R.string.destroy_channel_dialog); + builder.setPositiveButton( + R.string.ok, + (dialog, which) -> { + xmppConnectionService.destroyRoom( + mConversation, ConferenceDetailsActivity.this); + }); builder.setNegativeButton(R.string.cancel, null); final AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); @@ -530,7 +611,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (Compatibility.hasStoragePermission(this)) { final int limit = GridManager.getCurrentColumnCount(this.binding.media); xmppConnectionService.getAttachments(this.mConversation, limit, this); - this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, mConversation)); + this.binding.showMedia.setOnClickListener( + (v) -> MediaBrowserActivity.launch(this, mConversation)); } binding.storeInCache.setChecked(mConversation.storeInCache()); @@ -561,20 +643,25 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final MucOptions mucOptions = mConversation.getMucOptions(); final User self = mucOptions.getSelf(); final String account = mConversation.getAccount().getJid().asBareJid().toEscapedString(); - setTitle(mucOptions.isPrivateAndNonAnonymous() ? R.string.action_muc_details : R.string.channel_details); + setTitle( + mucOptions.isPrivateAndNonAnonymous() + ? R.string.action_muc_details + : R.string.channel_details); final Bookmark bookmark = mConversation.getBookmark(); final XmppConnection connection = mConversation.getAccount().getXmppConnection(); this.binding.editMucNameButton.setVisibility((self.getAffiliation().ranks(MucOptions.Affiliation.OWNER) || mucOptions.canChangeSubject() || (bookmark != null && connection != null && connection.getFeatures().bookmarks2())) ? View.VISIBLE : View.GONE); this.binding.detailsAccount.setText(getString(R.string.using_account, account)); this.binding.truejid.setVisibility(View.GONE); if (mConversation.isPrivateAndNonAnonymous()) { - this.binding.jid.setText(getString(R.string.hosted_on, mConversation.getJid().getDomain())); + this.binding.jid.setText( + getString(R.string.hosted_on, mConversation.getJid().getDomain())); this.binding.truejid.setText(mConversation.getJid().asBareJid().toEscapedString()); if (mAdvancedMode) this.binding.truejid.setVisibility(View.VISIBLE); } else { this.binding.jid.setText(mConversation.getJid().asBareJid().toEscapedString()); } - AvatarWorkerTask.loadAvatar(mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar( + mConversation, binding.yourPhoto, R.dimen.avatar_on_details_screen_size); String roomName = mucOptions.getName(); String subject = mucOptions.getSubject(); final boolean hasTitle; @@ -595,7 +682,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); MyLinkify.addLinks(spannable, false); this.binding.mucSubject.setText(spannable); - this.binding.mucSubject.setTextAppearance( subject.length() > (hasTitle ? 128 : 196) ? com.google.android.material.R.style.TextAppearance_Material3_BodyMedium : com.google.android.material.R.style.TextAppearance_Material3_BodyLarge); + this.binding.mucSubject.setTextAppearance( + subject.length() > (hasTitle ? 128 : 196) + ? com.google.android.material.R.style + .TextAppearance_Material3_BodyMedium + : com.google.android.material.R.style + .TextAppearance_Material3_BodyLarge); this.binding.mucSubject.setAutoLinkMask(0); this.binding.mucSubject.setVisibility(View.VISIBLE); this.binding.mucSubject.setMovementMethod(LinkMovementMethod.getInstance()); @@ -613,7 +705,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucConferenceType.setText(MucConfiguration.describe(this, mucOptions)); } else if (!mucOptions.isPrivateAndNonAnonymous() && mucOptions.nonanonymous()) { this.binding.mucSettings.setVisibility(View.VISIBLE); - this.binding.mucConferenceType.setText(R.string.group_chat_will_make_your_jabber_id_public); + this.binding.mucConferenceType.setText( + R.string.group_chat_will_make_your_jabber_id_public); } else { this.binding.mucSettings.setVisibility(View.GONE); } @@ -636,43 +729,55 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers final long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL, 0); if (mutedTill == Long.MAX_VALUE) { this.binding.notificationStatusText.setText(R.string.notify_never); - this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_off_24dp); + this.binding.notificationStatusButton.setImageResource( + R.drawable.ic_notifications_off_24dp); } else if (System.currentTimeMillis() < mutedTill) { this.binding.notificationStatusText.setText(R.string.notify_paused); - this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_paused_24dp); + this.binding.notificationStatusButton.setImageResource( + R.drawable.ic_notifications_paused_24dp); } else if (mConversation.alwaysNotify()) { this.binding.notificationStatusText.setText(R.string.notify_on_all_messages); - this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_24dp); + this.binding.notificationStatusButton.setImageResource( + R.drawable.ic_notifications_24dp); } else if (mConversation.notifyReplies()) { this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted_or_replied); this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp); } else { this.binding.notificationStatusText.setText(R.string.notify_only_when_highlighted); - this.binding.notificationStatusButton.setImageResource(R.drawable.ic_notifications_none_24dp); + this.binding.notificationStatusButton.setImageResource( + R.drawable.ic_notifications_none_24dp); } final List users = mucOptions.getUsers(); - Collections.sort(users, (a, b) -> { - if (b.getAffiliation().outranks(a.getAffiliation())) { - return 1; - } else if (a.getAffiliation().outranks(b.getAffiliation())) { - return -1; - } else { - if (a.getAvatar() != null && b.getAvatar() == null) { - return -1; - } else if (a.getAvatar() == null && b.getAvatar() != null) { - return 1; - } else { - return a.getComparableName().compareToIgnoreCase(b.getComparableName()); - } - } - }); - this.mUserPreviewAdapter.submitList(MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users))); + Collections.sort( + users, + (a, b) -> { + if (b.getAffiliation().outranks(a.getAffiliation())) { + return 1; + } else if (a.getAffiliation().outranks(b.getAffiliation())) { + return -1; + } else { + if (a.getAvatar() != null && b.getAvatar() == null) { + return -1; + } else if (a.getAvatar() == null && b.getAvatar() != null) { + return 1; + } else { + return a.getComparableName().compareToIgnoreCase(b.getComparableName()); + } + } + }); + this.mUserPreviewAdapter.submitList( + MucOptions.sub(users, GridManager.getCurrentColumnCount(binding.users))); this.binding.invite.setVisibility(mucOptions.canInvite() ? View.VISIBLE : View.GONE); this.binding.showUsers.setVisibility(mucOptions.getUsers(true, mucOptions.getSelf().getAffiliation().ranks(MucOptions.Affiliation.ADMIN)).size() > 0 ? View.VISIBLE : View.GONE); - this.binding.showUsers.setText(getResources().getQuantityString(R.plurals.view_users, users.size(), users.size())); - this.binding.usersWrapper.setVisibility(users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE); + this.binding.showUsers.setText( + getResources().getQuantityString(R.plurals.view_users, users.size(), users.size())); + this.binding.usersWrapper.setVisibility( + users.size() > 0 || mucOptions.canInvite() ? View.VISIBLE : View.GONE); if (users.size() == 0) { - this.binding.noUsersHints.setText(mucOptions.isPrivateAndNonAnonymous() ? R.string.no_users_hint_group_chat : R.string.no_users_hint_channel); + this.binding.noUsersHints.setText( + mucOptions.isPrivateAndNonAnonymous() + ? R.string.no_users_hint_group_chat + : R.string.no_users_hint_channel); this.binding.noUsersHints.setVisibility(View.VISIBLE); } else { this.binding.noUsersHints.setVisibility(View.GONE); @@ -718,7 +823,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers public static String getStatus(Context context, User user, final boolean advanced) { if (advanced) { - return String.format("%s (%s)", context.getString(user.getAffiliation().getResId()), context.getString(user.getRole().getResId())); + return String.format( + "%s (%s)", + context.getString(user.getAffiliation().getResId()), + context.getString(user.getRole().getResId())); } else { return context.getString(user.getAffiliation().getResId()); } @@ -746,7 +854,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override public void onRoomDestroyFailed() { final boolean groupChat = mConversation != null && mConversation.isPrivateAndNonAnonymous(); - displayToast(getString(groupChat ? R.string.could_not_destroy_room : R.string.could_not_destroy_channel)); + displayToast( + getString( + groupChat + ? R.string.could_not_destroy_room + : R.string.could_not_destroy_channel)); } @Override @@ -760,23 +872,20 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } private void displayToast(final String msg) { - runOnUiThread(() -> { - if (isFinishing()) { - return; - } - ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show(); - }); + runOnUiThread( + () -> { + if (isFinishing()) { + return; + } + ToastCompat.makeText(this, msg, Toast.LENGTH_SHORT).show(); + }); } @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } + public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override public void afterTextChanged(Editable s) { @@ -785,8 +894,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } final MucOptions mucOptions = mConversation.getMucOptions(); if (this.binding.mucEditor.getVisibility() == View.VISIBLE) { - boolean subjectChanged = changed(binding.mucEditSubject.getEditableText().toString(), mucOptions.getSubject()); - boolean nameChanged = changed(binding.mucEditTitle.getEditableText().toString(), mucOptions.getName()); + boolean subjectChanged = + changed( + binding.mucEditSubject.getEditableText().toString(), + mucOptions.getSubject()); + boolean nameChanged = + changed( + binding.mucEditTitle.getEditableText().toString(), + mucOptions.getName()); final Bookmark bookmark = mConversation.getBookmark(); if (subjectChanged || nameChanged || (bookmark != null && mConversation.getAccount().getXmppConnection().getFeatures().bookmarks2())) { this.binding.editMucNameButton.setImageResource(R.drawable.ic_save_24dp); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index f60f325e793e41c077d66b18db56aef827732762..8f66f34038330b3c199128a2bef2609bc7ea7e73 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -33,7 +33,6 @@ import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; @@ -98,8 +97,17 @@ import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.openintents.openpgp.util.OpenPgpUtils; -public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded { +public class ContactDetailsActivity extends OmemoActivity + implements OnAccountUpdate, + OnRosterUpdate, + OnUpdateBlocklist, + OnKeyStatusUpdated, + OnMediaLoaded { public static final String ACTION_VIEW_CONTACT = "view_contact"; private final int REQUEST_SYNC_CONTACTS = 0x28cf; ActivityContactDetailsBinding binding; @@ -108,40 +116,55 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp protected MenuItem save = null; private Contact contact; - private final DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - xmppConnectionService.deleteContactOnServer(contact); - } - }; - private final OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() { + private final DialogInterface.OnClickListener removeFromRoster = + new DialogInterface.OnClickListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - xmppConnectionService.stopPresenceUpdatesTo(contact); - } else { - contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + @Override + public void onClick(DialogInterface dialog, int which) { + xmppConnectionService.deleteContactOnServer(contact); } - } else { - contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); - xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact)); - } - } - }; - private final OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().requestPresenceUpdatesFrom(contact)); - } else { - xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesFrom(contact)); - } - } - }; + }; + private final OnCheckedChangeListener mOnSendCheckedChange = + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + xmppConnectionService.stopPresenceUpdatesTo(contact); + } else { + contact.setOption(Contact.Options.PREEMPTIVE_GRANT); + } + } else { + contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); + xmppConnectionService.sendPresencePacket( + contact.getAccount(), + xmppConnectionService + .getPresenceGenerator() + .stopPresenceUpdatesTo(contact)); + } + } + }; + private final OnCheckedChangeListener mOnReceiveCheckedChange = + new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + xmppConnectionService.sendPresencePacket( + contact.getAccount(), + xmppConnectionService + .getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); + } else { + xmppConnectionService.sendPresencePacket( + contact.getAccount(), + xmppConnectionService + .getPresenceGenerator() + .stopPresenceUpdatesFrom(contact)); + } + } + }; private Jid accountJid; private Jid contactJid; private boolean showDynamicTags = false; @@ -152,14 +175,18 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp private void checkContactPermissionAndShowAddDialog() { if (hasContactsPermission()) { showAddToPhoneBookDialog(); - } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); + } else if (QuickConversationsService.isContactListIntegration(this) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + requestPermissions( + new String[] {Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS); } } private boolean hasContactsPermission() { - if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED; + if (QuickConversationsService.isContactListIntegration(this) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return checkSelfPermission(Manifest.permission.READ_CONTACTS) + == PackageManager.PERMISSION_GRANTED; } else { return true; } @@ -167,9 +194,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp private void showAddToPhoneBookDialog() { final Jid jid = contact.getJid(); - final boolean quicksyContact = AbstractQuickConversationsService.isQuicksy() - && Config.QUICKSY_DOMAIN.equals(jid.getDomain()) - && jid.getLocal() != null; + final boolean quicksyContact = + AbstractQuickConversationsService.isQuicksy() + && Config.QUICKSY_DOMAIN.equals(jid.getDomain()) + && jid.getLocal() != null; final String value; if (quicksyContact) { value = PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, jid); @@ -180,24 +208,33 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp builder.setTitle(getString(R.string.action_add_phone_book)); builder.setMessage(getString(R.string.add_phone_book_text, value)); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add), (dialog, which) -> { - final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); - intent.setType(Contacts.CONTENT_ITEM_TYPE); - if (quicksyContact) { - intent.putExtra(Intents.Insert.PHONE, value); - } else { - intent.putExtra(Intents.Insert.IM_HANDLE, value); - intent.putExtra(Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER); - //TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a value of 'XMPP' - // however we don’t have such a field and thus have to use the legacy PROTOCOL_JABBER - } - intent.putExtra("finishActivityOnSaveCompleted", true); - try { - startActivityForResult(intent, 0); - } catch (ActivityNotFoundException e) { - Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show(); - } - }); + builder.setPositiveButton( + getString(R.string.add), + (dialog, which) -> { + final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT); + intent.setType(Contacts.CONTENT_ITEM_TYPE); + if (quicksyContact) { + intent.putExtra(Intents.Insert.PHONE, value); + } else { + intent.putExtra(Intents.Insert.IM_HANDLE, value); + intent.putExtra( + Intents.Insert.IM_PROTOCOL, CommonDataKinds.Im.PROTOCOL_JABBER); + // TODO for modern use we want PROTOCOL_CUSTOM and an extra field with a + // value of 'XMPP' + // however we don’t have such a field and thus have to use the legacy + // PROTOCOL_JABBER + } + intent.putExtra("finishActivityOnSaveCompleted", true); + try { + startActivityForResult(intent, 0); + } catch (ActivityNotFoundException e) { + Toast.makeText( + ContactDetailsActivity.this, + R.string.no_application_found_to_view_contact, + Toast.LENGTH_SHORT) + .show(); + } + }); builder.create().show(); } @@ -224,7 +261,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @Override protected String getShareableUri(boolean http) { if (http) { - return "https://conversations.im/i/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString()); + return "https://conversations.im/i/" + + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString()); } else { return "xmpp:" + Uri.encode(contact.getJid().asBareJid().toEscapedString(), "@/+"); } @@ -233,7 +271,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false); + showInactiveOmemo = + savedInstanceState != null + && savedInstanceState.getBoolean("show_inactive_omemo", false); if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { try { this.accountJid = Jid.ofEscaped(getIntent().getExtras().getString(EXTRA_ACCOUNT)); @@ -250,10 +290,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp setSupportActionBar(binding.toolbar); configureActionBar(getSupportActionBar()); - binding.showInactiveDevices.setOnClickListener(v -> { - showInactiveOmemo = !showInactiveOmemo; - populateView(); - }); + binding.showInactiveDevices.setOnClickListener( + v -> { + showInactiveOmemo = !showInactiveOmemo; + populateView(); + }); binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact)); mMediaAdapter = new MediaAdapter(this, R.dimen.media_size); @@ -273,12 +314,15 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false); this.showLastSeen = preferences.getBoolean("last_activity", false); - binding.mediaWrapper.setVisibility(Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); + binding.mediaWrapper.setVisibility( + Compatibility.hasStoragePermission(this) ? View.VISIBLE : View.GONE); mMediaAdapter.setAttachments(Collections.emptyList()); } @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + // TODO check for Camera / Scan permission super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -322,15 +366,20 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setNegativeButton(getString(R.string.cancel), null); builder.setTitle(getString(R.string.action_delete_contact)) - .setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString())) - .setPositiveButton(getString(R.string.delete), - removeFromRoster).create().show(); + .setMessage( + JidDialog.style( + this, + R.string.remove_contact_text, + contact.getJid().toEscapedString())) + .setPositiveButton(getString(R.string.delete), removeFromRoster) + .create() + .show(); break; case R.id.action_save: saveEdits(); break; case R.id.action_edit_contact: - Uri systemAccount = contact.getSystemAccount(); + final Uri systemAccount = contact.getSystemAccount(); if (systemAccount == null) { menuItem.expandActionView(); EditText text = menuItem.getActionView().findViewById(R.id.search_field); @@ -382,31 +431,43 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp try { startActivity(intent); } catch (ActivityNotFoundException e) { - Toast.makeText(ContactDetailsActivity.this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show(); + Toast.makeText( + ContactDetailsActivity.this, + R.string.no_application_found_to_view_contact, + Toast.LENGTH_SHORT) + .show(); } - } break; - case R.id.action_block: + case R.id.action_block, R.id.action_unblock: BlockContactDialog.show(this, contact); break; - case R.id.action_unblock: - BlockContactDialog.show(this, contact); + case R.id.action_custom_notifications: + configureCustomNotifications(contact); break; } return super.onOptionsItemSelected(menuItem); } + private void configureCustomNotifications(final Contact contact) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return; + } + final var shortcut = xmppConnectionService.getShortcutService().getShortcutInfo(contact); + configureCustomNotification(shortcut); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.contact_details, menu); AccountUtils.showHideMenuItems(menu); - edit = menu.findItem(R.id.action_edit_contact); - save = menu.findItem(R.id.action_save); - MenuItem block = menu.findItem(R.id.action_block); - MenuItem unblock = menu.findItem(R.id.action_unblock); - MenuItem edit = menu.findItem(R.id.action_edit_contact); - MenuItem delete = menu.findItem(R.id.action_delete_contact); + final MenuItem block = menu.findItem(R.id.action_block); + final MenuItem unblock = menu.findItem(R.id.action_unblock); + final MenuItem edit = menu.findItem(R.id.action_edit_contact); + final MenuItem save = menu.findItem(R.id.action_save); + final MenuItem delete = menu.findItem(R.id.action_delete_contact); + final MenuItem customNotifications = menu.findItem(R.id.action_custom_notifications); + customNotifications.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R); if (contact == null) { return true; } @@ -463,7 +524,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.statusMessage.setVisibility(View.VISIBLE); final Spannable span = new SpannableString(message); if (Emoticons.isOnlyEmoji(message)) { - span.setSpan(new RelativeSizeSpan(2.0f), 0, message.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan( + new RelativeSizeSpan(2.0f), + 0, + message.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } binding.statusMessage.setText(span); } else { @@ -487,14 +552,16 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.detailsSendPresence.setText(R.string.send_presence_updates); } else { binding.detailsSendPresence.setText(R.string.preemptively_grant); - binding.detailsSendPresence.setChecked(contact.getOption(Contact.Options.PREEMPTIVE_GRANT)); + binding.detailsSendPresence.setChecked( + contact.getOption(Contact.Options.PREEMPTIVE_GRANT)); } if (contact.getOption(Contact.Options.TO)) { binding.detailsReceivePresence.setText(R.string.receive_presence_updates); binding.detailsReceivePresence.setChecked(true); } else { binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates); - binding.detailsReceivePresence.setChecked(contact.getOption(Contact.Options.ASKING)); + binding.detailsReceivePresence.setChecked( + contact.getOption(Contact.Options.ASKING)); } if (contact.getAccount().isOnlineAndConnected()) { binding.detailsReceivePresence.setEnabled(true); @@ -520,7 +587,11 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp && contact.getLastseen() > 0 && contact.getPresences().allOrNonSupport(Namespace.IDLE)) { binding.detailsLastseen.setVisibility(View.VISIBLE); - binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen())); + binding.detailsLastseen.setText( + UIHelper.lastseen( + getApplicationContext(), + contact.isActive(), + contact.getLastseen())); } else { binding.detailsLastseen.setVisibility(View.GONE); } @@ -529,7 +600,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid())); final String account = contact.getAccount().getJid().asBareJid().toEscapedString(); binding.detailsAccount.setText(getString(R.string.using_account, account)); - AvatarWorkerTask.loadAvatar(contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); + AvatarWorkerTask.loadAvatar( + contact, binding.detailsContactBadge, R.dimen.avatar_on_details_screen_size); binding.detailsContactBadge.setOnClickListener(this::onBadgeClick); binding.detailsContactKeys.removeAllViews(); @@ -537,7 +609,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final LayoutInflater inflater = getLayoutInflater(); final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); if (Config.supportOmemo() && axolotlService != null) { - final Collection sessions = axolotlService.findSessionsForContact(contact); + final Collection sessions = + axolotlService.findSessionsForContact(contact); boolean anyActive = false; for (XmppAxolotlSession session : sessions) { anyActive = session.getTrust().isActive(); @@ -567,9 +640,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp showUnverifiedWarning = true; } } - binding.unverifiedWarning.setVisibility(showUnverifiedWarning ? View.VISIBLE : View.GONE); + binding.unverifiedWarning.setVisibility( + showUnverifiedWarning ? View.VISIBLE : View.GONE); if (showsInactive || skippedInactive) { - binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices); + binding.showInactiveDevices.setText( + showsInactive + ? R.string.hide_inactive_devices + : R.string.show_inactive_devices); binding.showInactiveDevices.setVisibility(View.VISIBLE); } else { binding.showInactiveDevices.setVisibility(View.GONE); @@ -578,7 +655,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.showInactiveDevices.setVisibility(View.GONE); } final boolean isCameraFeatureAvailable = isCameraFeatureAvailable(); - binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE); + binding.scanButton.setVisibility( + hasKeys && isCameraFeatureAvailable ? View.VISIBLE : View.GONE); if (hasKeys) { binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this)); } @@ -589,7 +667,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp TextView keyType = view.findViewById(R.id.key_type); keyType.setText(R.string.openpgp_key_id); if ("pgp".equals(messageFingerprint)) { - keyType.setTextColor(MaterialColors.getColor(keyType, com.google.android.material.R.attr.colorPrimaryVariant)); + keyType.setTextColor( + MaterialColors.getColor( + keyType, com.google.android.material.R.attr.colorPrimaryVariant)); } key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId())); final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId()); @@ -601,7 +681,8 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE); final List tagList = contact.getTags(this); - final boolean hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE; + final boolean hasMetaTags = + contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE; if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) { binding.tags.setVisibility(View.GONE); } else { @@ -610,9 +691,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); for (final ListItem.Tag tag : tagList) { final String name = tag.getName(); - final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); + final TextView tv = + (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); tv.setText(name); - tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(this,XEP0392Helper.rgbFromNick(name)))); + tv.setBackgroundTintList( + ColorStateList.valueOf( + MaterialColors.harmonizeWithPrimary( + this, XEP0392Helper.rgbFromNick(name)))); final int id = ViewCompat.generateViewId(); tv.setId(id); viewIdBuilder.add(id); @@ -620,11 +705,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } if (contact.isBlocked()) { final TextView tv = - (TextView) - inflater.inflate( - R.layout.item_tag, binding.tags, false); + (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); tv.setText(R.string.blocked); - tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(), ContextCompat.getColor(tv.getContext(),R.color.gray_800)))); + tv.setBackgroundTintList( + ColorStateList.valueOf( + MaterialColors.harmonizeWithPrimary( + tv.getContext(), + ContextCompat.getColor( + tv.getContext(), R.color.gray_800)))); final int id = ViewCompat.generateViewId(); tv.setId(id); viewIdBuilder.add(id); @@ -633,9 +721,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp final Presence.Status status = contact.getShownStatus(); if (status != Presence.Status.OFFLINE) { final TextView tv = - (TextView) - inflater.inflate( - R.layout.item_tag, binding.tags, false); + (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); UIHelper.setStatus(tv, status); final int id = ViewCompat.generateViewId(); tv.setId(id); @@ -707,8 +793,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp if (Compatibility.hasStoragePermission(this)) { final int limit = GridManager.getCurrentColumnCount(this.binding.media); - xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this); - this.binding.showMedia.setOnClickListener((v) -> MediaBrowserActivity.launch(this, contact)); + xmppConnectionService.getAttachments( + account, contact.getJid().asBareJid(), limit, this); + this.binding.showMedia.setOnClickListener( + (v) -> MediaBrowserActivity.launch(this, contact)); } final VcardAdapter items = new VcardAdapter(); @@ -763,7 +851,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @Override protected void processFingerprintVerification(XmppUri uri) { - if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) { + if (contact != null + && contact.getJid().asBareJid().equals(uri.getJid()) + && uri.hasFingerprints()) { if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) { Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show(); } @@ -774,12 +864,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp @Override public void onMediaLoaded(List attachments) { - runOnUiThread(() -> { - int limit = GridManager.getCurrentColumnCount(binding.media); - mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit, attachments.size()))); - binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); - }); - + runOnUiThread( + () -> { + int limit = GridManager.getCurrentColumnCount(binding.media); + mMediaAdapter.setAttachments( + attachments.subList(0, Math.min(limit, attachments.size()))); + binding.mediaWrapper.setVisibility( + attachments.size() > 0 ? View.VISIBLE : View.GONE); + }); } class VcardAdapter extends ArrayAdapter { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 3b378ae04ec21ae03049db950d180b3b2f5482af..fbc95500023f2d6e1bb9c99adb103d3cffb95405 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -200,23 +200,12 @@ 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.jingle.RtpEndUserState; import im.conversations.android.xmpp.model.stanza.Iq; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; -import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; - public class ConversationFragment extends XmppFragment implements EditMessage.KeyboardListener, MessageAdapter.OnContactPictureLongClicked, @@ -407,7 +396,10 @@ public class ConversationFragment extends XmppFragment } catch (IllegalStateException e) { Log.d( Config.LOGTAG, - "caught illegal state exception while updating status messages"); + "caught illegal state" + + " exception while" + + " updating status" + + " messages"); } messageListAdapter .notifyDataSetChanged(); @@ -784,14 +776,6 @@ public class ConversationFragment extends XmppFragment for (int i = 0; i < messages.size(); ++i) { if (uuid.equals(messages.get(i).getUuid())) { return i; - } else { - Message next = messages.get(i); - while (next != null && next.wasMergedIntoPrevious(activity == null ? null : activity.xmppConnectionService)) { - if (uuid.equals(next.getUuid())) { - return i; - } - next = next.next(); - } } } return -1; @@ -1425,13 +1409,12 @@ public class ConversationFragment extends XmppFragment menuCall.setVisible(false); } else { menuOngoingCall.setVisible(false); - final RtpCapability.Capability rtpCapability = - RtpCapability.check(conversation.getContact()); + // use RtpCapability.check(conversation.getContact()); to check if contact + // actually has support final boolean cameraAvailable = activity != null && activity.isCameraFeatureAvailable(); - menuCall.setVisible(rtpCapability != RtpCapability.Capability.NONE); - menuVideoCall.setVisible( - rtpCapability == RtpCapability.Capability.VIDEO && cameraAvailable); + menuCall.setVisible(true); + menuVideoCall.setVisible(cameraAvailable); } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); @@ -1846,13 +1829,9 @@ public class ConversationFragment extends XmppFragment } } - private void populateContextMenu(ContextMenu menu) { + private void populateContextMenu(final ContextMenu menu) { final Message m = this.selectedMessage; final Transferable t = m.getTransferable(); - Message relevantForCorrection = m; - while (relevantForCorrection.mergeable(relevantForCorrection.next())) { - relevantForCorrection = relevantForCorrection.next(); - } if (m.getType() != Message.TYPE_STATUS && m.getType() != Message.TYPE_RTP_SESSION) { if (m.getEncryption() == Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE @@ -1889,6 +1868,7 @@ public class ConversationFragment extends XmppFragment MenuItem shareWith = menu.findItem(R.id.share_with); MenuItem sendAgain = menu.findItem(R.id.send_again); MenuItem copyUrl = menu.findItem(R.id.copy_url); + MenuItem copyLink = menu.findItem(R.id.copy_link); MenuItem saveAsSticker = menu.findItem(R.id.save_as_sticker); MenuItem downloadFile = menu.findItem(R.id.download_file); MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); @@ -1902,7 +1882,8 @@ public class ConversationFragment extends XmppFragment && m.getErrorMessage() != null && !Message.ERROR_MESSAGE_CANCELLED.equals(m.getErrorMessage()); final Conversational conversational = m.getConversation(); - if (m.getStatus() == Message.STATUS_RECEIVED && conversational instanceof Conversation c) { + if (m.getStatus() == Message.STATUS_RECEIVED + && conversational instanceof Conversation c) { final XmppConnection connection = c.getAccount().getXmppConnection(); if (c.isWithStranger() && m.getServerMsgId() != null @@ -1912,8 +1893,16 @@ public class ConversationFragment extends XmppFragment reportAndBlock.setVisible(true); } } - if (!encrypted && !m.isPrivateMessage()) { - addReaction.setVisible(!showError && !m.isDeleted()); + if (conversational instanceof Conversation c) { + addReaction.setVisible( + !showError + && !m.isDeleted() + && !m.isPrivateMessage() + && (c.getMode() == Conversational.MODE_SINGLE + || (c.getMucOptions().occupantId() + && c.getMucOptions().participating()))); + } else { + addReaction.setVisible(false); } if (!m.isFileOrImage() && !encrypted @@ -1922,20 +1911,29 @@ public class ConversationFragment extends XmppFragment && !unInitiatedButKnownSize && t == null) { copyMessage.setVisible(true); + quoteMessage.setVisible(!showError && !MessageUtils.prepareQuote(m).isEmpty()); + final String scheme = + ShareUtil.getLinkScheme(new SpannableStringBuilder(m.getBody())); + if ("xmpp".equals(scheme)) { + copyLink.setTitle(R.string.copy_jabber_id); + copyLink.setVisible(true); + } else if (scheme != null) { + copyLink.setVisible(true); + } } quoteMessage.setVisible(!encrypted && !showError); if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) { retryDecryption.setVisible(true); } if (!showError - && relevantForCorrection.getType() == Message.TYPE_TEXT - && relevantForCorrection.isEditable() + && m.getType() == Message.TYPE_TEXT + && m.isEditable() && !m.isGeoUri() && m.getConversation() instanceof Conversation) { correctMessage.setVisible(true); - if (!relevantForCorrection.getBody().equals("") && !relevantForCorrection.getBody().equals(" ")) retractMessage.setVisible(true); + if (!m.getBody().equals("") && !m.getBody().equals(" ")) retractMessage.setVisible(true); } - if (relevantForCorrection.getStatus() == Message.STATUS_WAITING) { + if (m.getStatus() == Message.STATUS_WAITING) { correctMessage.setVisible(true); retractMessage.setVisible(true); } @@ -2017,10 +2015,7 @@ public class ConversationFragment extends XmppFragment .setTitle(R.string.retract_message) .setMessage("Do you really want to retract this message?") .setPositiveButton(R.string.yes, (dialog, whichButton) -> { - Message message = selectedMessage; - while (message.mergeable(message.next())) { - message = message.next(); - } + final var message = selectedMessage; if (message.getStatus() == Message.STATUS_WAITING || message.getStatus() == Message.STATUS_OFFERED) { activity.xmppConnectionService.deleteMessage(message); return; @@ -2054,11 +2049,7 @@ public class ConversationFragment extends XmppFragment return true; case R.id.moderate_message: activity.quickEdit("Spam", (reason) -> { - Message message = selectedMessage; - do { - activity.xmppConnectionService.moderateMessage(conversation.getAccount(), message, reason); - message = message.mergeable(message.next()) ? message.next() : null; - } while (message != null); + activity.xmppConnectionService.moderateMessage(conversation.getAccount(), selectedMessage, reason); return null; }, R.string.moderate_reason, false, false, true, true); return true; @@ -2350,9 +2341,9 @@ public class ConversationFragment extends XmppFragment private void addShortcut() { ShortcutInfoCompat info; if (conversation.getMode() == Conversation.MODE_MULTI) { - info = activity.xmppConnectionService.getShortcutService().getShortcutInfoCompat(conversation.getMucOptions()); + info = activity.xmppConnectionService.getShortcutService().getShortcutInfo(conversation.getMucOptions()); } else { - info = activity.xmppConnectionService.getShortcutService().getShortcutInfoCompat(conversation.getContact()); + info = activity.xmppConnectionService.getShortcutService().getShortcutInfo(conversation.getContact()); } ShortcutManagerCompat.requestPinShortcut(activity, info, null); } @@ -2436,7 +2427,11 @@ public class ConversationFragment extends XmppFragment } private void triggerRtpSession(final Account account, final Jid with, final String action) { - CallIntegrationConnectionService.placeCall(activity.xmppConnectionService, account,with,RtpSessionActivity.actionToMedia(action)); + CallIntegrationConnectionService.placeCall( + activity.xmppConnectionService, + account, + with, + RtpSessionActivity.actionToMedia(action)); } private void handleAttachmentSelection(MenuItem item) { @@ -2512,10 +2507,14 @@ public class ConversationFragment extends XmppFragment } public void attachFile(final int attachmentChoice) { - attachFile(attachmentChoice, true); + attachFile(attachmentChoice, true, false); } public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed) { + attachFile(attachmentChoice, updateRecentlyUsed, false); + } + + public void attachFile(final int attachmentChoice, final boolean updateRecentlyUsed, final boolean fromPermissions) { if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) { if (!hasPermissions( attachmentChoice, @@ -2524,7 +2523,8 @@ public class ConversationFragment extends XmppFragment return; } } else if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO - || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO) { + || attachmentChoice == ATTACHMENT_CHOICE_RECORD_VIDEO + || (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE && !fromPermissions)) { if (!hasPermissions( attachmentChoice, Manifest.permission.WRITE_EXTERNAL_STORAGE, @@ -2615,7 +2615,7 @@ public class ConversationFragment extends XmppFragment final PermissionUtils.PermissionResult permissionResult = PermissionUtils.removeBluetoothConnect(permissions, grantResults); if (grantResults.length > 0) { - if (allGranted(permissionResult.grantResults)) { + if (allGranted(permissionResult.grantResults) || requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { switch (requestCode) { case REQUEST_START_DOWNLOAD: if (this.mPendingDownloadableMessage != null) { @@ -2637,7 +2637,7 @@ public class ConversationFragment extends XmppFragment triggerRtpSession(RtpSessionActivity.ACTION_MAKE_VIDEO_CALL); break; default: - attachFile(requestCode); + attachFile(requestCode, true, true); break; } } else { @@ -2720,7 +2720,8 @@ public class ConversationFragment extends XmppFragment @SuppressLint("InflateParams") protected void clearHistoryDialog(final Conversation conversation) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(R.string.clear_conversation_history); final View dialogView = requireActivity().getLayoutInflater().inflate(R.layout.dialog_clear_history, null); @@ -2744,7 +2745,8 @@ public class ConversationFragment extends XmppFragment } protected void muteConversationDialog(final Conversation conversation) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(R.string.disable_notifications); final int[] durations = activity.getResources().getIntArray(R.array.mute_options_durations); final CharSequence[] labels = new CharSequence[durations.length]; @@ -2776,7 +2778,9 @@ public class ConversationFragment extends XmppFragment private boolean hasPermissions(int requestCode, List permissions) { final List missingPermissions = new ArrayList<>(); for (String permission : permissions) { - if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU || Config.ONLY_INTERNAL_STORAGE) && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + || Config.ONLY_INTERNAL_STORAGE) + && permission.equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { continue; } if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { @@ -2786,9 +2790,7 @@ public class ConversationFragment extends XmppFragment if (missingPermissions.size() == 0) { return true; } else { - requestPermissions( - missingPermissions.toArray(new String[0]), - requestCode); + requestPermissions(missingPermissions.toArray(new String[0]), requestCode); return false; } } @@ -2826,7 +2828,9 @@ public class ConversationFragment extends XmppFragment intent.setType("*/*"); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"image/*", "video/*"}); intent = Intent.createChooser(intent, getString(R.string.perform_action_with)); - intent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { takePhotoIntent, takeVideoIntent }); + if (activity.checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + intent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { takePhotoIntent, takeVideoIntent }); + } break; case ATTACHMENT_CHOICE_RECORD_VIDEO: intent = takeVideoIntent; @@ -2932,9 +2936,6 @@ public class ConversationFragment extends XmppFragment } } if (message != null) { - while (message.next() != null && message.next().wasMergedIntoPrevious(activity == null ? null : activity.xmppConnectionService)) { - message = message.next(); - } return message; } } @@ -2968,7 +2969,15 @@ public class ConversationFragment extends XmppFragment } private void addReaction(final Message message) { - activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message, reactions)); + activity.addReaction( + message, + reactions -> { + if (activity.xmppConnectionService.sendReactions(message, reactions)) { + return; + } + Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG) + .show(); + }); } private void reportMessage(final Message message) { @@ -2976,7 +2985,8 @@ public class ConversationFragment extends XmppFragment } private void showErrorMessage(final Message message) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setTitle(R.string.error_message); final String errorMessage = message.getErrorMessage(); final String[] errorMessageParts = @@ -3047,7 +3057,8 @@ public class ConversationFragment extends XmppFragment } private void deleteFile(final Message message) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_file_dialog); builder.setMessage(R.string.delete_file_dialog_msg); @@ -3157,10 +3168,7 @@ public class ConversationFragment extends XmppFragment updateEditablity(); } - private void correctMessage(Message message) { - while (message.mergeable(message.next())) { - message = message.next(); - } + private void correctMessage(final Message message) { setThread(message.getThread()); conversation.setUserSelectedThread(true); this.conversation.setCorrectingMessage(message); @@ -3281,7 +3289,8 @@ public class ConversationFragment extends XmppFragment final String uuid = pendingConversationsUuid.pop(); Log.d( Config.LOGTAG, - "ConversationFragment.onStart() - activity was bound but no conversation loaded. uuid=" + "ConversationFragment.onStart() - activity was bound but no conversation" + + " loaded. uuid=" + uuid); if (uuid != null) { findAndReInitByUuidOrArchive(uuid); @@ -3824,7 +3833,10 @@ public class ConversationFragment extends XmppFragment R.string.enable, this.mEnableAccountListener); } else if (account.getStatus() == Account.State.LOGGED_OUT) { - showSnackbar(R.string.this_account_is_logged_out,R.string.log_in,this.mEnableAccountListener); + showSnackbar( + R.string.this_account_is_logged_out, + R.string.log_in, + this.mEnableAccountListener); } else if (conversation.isBlocked()) { showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener); } else if (account.getStatus() == Account.State.CONNECTING) { @@ -3890,7 +3902,8 @@ public class ConversationFragment extends XmppFragment showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); break; case TECHNICAL_PROBLEMS: - showSnackbar(R.string.conference_technical_problems, R.string.try_again, joinMuc); + showSnackbar( + R.string.conference_technical_problems, R.string.try_again, joinMuc); break; case UNKNOWN: showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc); @@ -4168,7 +4181,7 @@ public class ConversationFragment extends XmppFragment if (!ReadByMarker.contains(marker, addedMarkers)) { addedMarkers.add( marker); // may be put outside this condition. set should do - // dedup anyway + // dedup anyway MucOptions.User user = mucOptions.findUser(marker); if (user != null && !users.contains(user)) { shownMarkers.add(user); @@ -4427,8 +4440,10 @@ public class ConversationFragment extends XmppFragment }); } - public void showNoPGPKeyDialog(final boolean plural, final DialogInterface.OnClickListener listener) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity()); + public void showNoPGPKeyDialog( + final boolean plural, final DialogInterface.OnClickListener listener) { + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(requireActivity()); if (plural) { builder.setTitle(getString(R.string.no_pgp_keys)); builder.setMessage(getText(R.string.contacts_have_no_pgp_keys)); @@ -4597,7 +4612,13 @@ public class ConversationFragment extends XmppFragment try { getActivity() .startIntentSenderForResult( - pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); + pendingIntent.getIntentSender(), + requestCode, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); } catch (final SendIntentException ignored) { } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java index 301f8c8b70d2fa2ba7d54357cd210fe204a36d40..17e033485a90fcd3998c9216591a51511788daa8 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java @@ -53,7 +53,6 @@ import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; - import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; @@ -117,8 +116,22 @@ import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import org.openintents.openpgp.util.OpenPgpApi; -public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged { +public class ConversationsActivity extends XmppActivity + implements OnConversationSelected, + OnConversationArchived, + OnConversationsListItemUpdated, + OnConversationRead, + XmppConnectionService.OnAccountUpdate, + XmppConnectionService.OnConversationUpdate, + XmppConnectionService.OnRosterUpdate, + OnUpdateBlocklist, + XmppConnectionService.OnShowErrorToast, + XmppConnectionService.OnAffiliationChanged { public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW"; public static final String EXTRA_CONVERSATION = "conversationUuid"; @@ -134,11 +147,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final String EXTRA_NODE = "node"; public static final String EXTRA_JID = "jid"; - private static final List VIEW_AND_SHARE_ACTIONS = Arrays.asList( - ACTION_VIEW_CONVERSATION, - Intent.ACTION_SEND, - Intent.ACTION_SEND_MULTIPLE - ); + private static final List VIEW_AND_SHARE_ACTIONS = + Arrays.asList( + ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE); public static final int REQUEST_OPEN_MESSAGE = 0x9876; public static final int REQUEST_PLAY_PAUSE = 0x5432; @@ -162,9 +173,11 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public static final long DRAWER_START_CHAT_PUBLIC = 14; public static final long DRAWER_START_CHAT_DISCOVER = 15; - //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment - private static final @IdRes - int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment}; + // secondary fragment (when holding the conversation, must be initialized before refreshing the + // overview fragment + private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = { + R.id.secondary_fragment, R.id.main_fragment + }; private final PendingItem pendingViewIntent = new PendingItem<>(); private final PendingItem postponedActivityResult = new PendingItem<>(); private ActivityConversationsBinding binding; @@ -181,7 +194,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private static boolean isViewOrShareIntent(Intent i) { Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction())); - return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION); + return i != null + && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) + && i.hasExtra(EXTRA_CONVERSATION); } private static Intent createLauncherIntent(Context context) { @@ -428,7 +443,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } invalidateActionBarTitle(); - if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) { + if (binding.secondaryFragment != null + && ConversationFragment.getConversation(this) == null) { Conversation conversation = ConversationsOverviewFragment.getSuggestion(this); if (conversation != null) { openConversation(conversation, null); @@ -696,7 +712,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio return performRedirectIfNecessary(null, noAnimation); } - private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) { + private boolean performRedirectIfNecessary( + final Conversation ignore, final boolean noAnimation) { if (xmppConnectionService == null) { return false; } @@ -707,12 +724,13 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio if (noAnimation) { intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); } - runOnUiThread(() -> { - startActivity(intent); - if (noAnimation) { - overridePendingTransition(0, 0); - } - }); + runOnUiThread( + () -> { + startActivity(intent); + if (noAnimation) { + overridePendingTransition(0, 0); + } + }); } return mRedirectInProcess.get(); } @@ -738,7 +756,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private String getBatteryOptimizationPreferenceKey() { - @SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); + @SuppressLint("HardwareIds") + String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); return "show_battery_optimization" + (device == null ? "" : device); } @@ -747,20 +766,31 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private boolean openBatteryOptimizationDialogIfNeeded() { - if (isOptimizingBattery() && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) { + if (isOptimizingBattery() + && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.battery_optimizations_enabled); - builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name))); - builder.setPositiveButton(R.string.next, (dialog, which) -> { - final Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - final Uri uri = Uri.parse("package:" + getPackageName()); - intent.setData(uri); - try { - startActivityForResult(intent, REQUEST_BATTERY_OP); - } catch (final ActivityNotFoundException e) { - Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show(); - } - }); + builder.setMessage( + getString( + R.string.battery_optimizations_enabled_dialog, + getString(R.string.app_name))); + builder.setPositiveButton( + R.string.next, + (dialog, which) -> { + final Intent intent = + new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + final Uri uri = Uri.parse("package:" + getPackageName()); + intent.setData(uri); + try { + startActivityForResult(intent, REQUEST_BATTERY_OP); + } catch (final ActivityNotFoundException e) { + Toast.makeText( + this, + R.string.device_does_not_support_battery_op, + Toast.LENGTH_SHORT) + .show(); + } + }); builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain()); final AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); @@ -771,8 +801,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private boolean 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); + 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); return true; } return false; @@ -878,9 +912,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } } - private boolean processViewIntent(Intent intent) { + private boolean processViewIntent(final Intent intent) { final String uuid = intent.getStringExtra(EXTRA_CONVERSATION); - final Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null; + final Conversation conversation = + uuid != null ? xmppConnectionService.findConversationByUuidReliable(uuid) : null; if (conversation == null) { Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid); return false; @@ -890,7 +925,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } @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); UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (grantResults.length > 0) { @@ -1057,7 +1093,9 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio public void onConversationSelected(Conversation conversation) { clearPendingViewIntent(); if (ConversationFragment.getConversation(this) == conversation) { - Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open"); + Log.d( + Config.LOGTAG, + "ignore onConversationSelected() because conversation is already open"); return; } openConversation(conversation, null); @@ -1070,13 +1108,12 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio } private void displayToast(final String msg) { - runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show()); + runOnUiThread( + () -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show()); } @Override - public void onAffiliationChangedSuccessful(Jid jid) { - - } + public void onAffiliationChangedSuccessful(Jid jid) {} @Override public void onAffiliationChangeFailed(Jid jid, int resId) { @@ -1086,7 +1123,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio private void openConversation(Conversation conversation, Bundle extras) { final FragmentManager fragmentManager = getFragmentManager(); executePendingTransactions(fragmentManager); - ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment); + ConversationFragment conversationFragment = + (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment); final boolean mainNeedsRefresh; if (conversationFragment == null) { mainNeedsRefresh = false; @@ -1102,7 +1140,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio fragmentTransaction.commit(); } catch (IllegalStateException e) { Log.w(Config.LOGTAG, "sate loss while opening conversation", e); - //allowing state loss is probably fine since view intents et all are already stored and a click can probably be 'ignored' + // allowing state loss is probably fine since view intents et all are already + // stored and a click can probably be 'ignored' return; } } @@ -1120,14 +1159,15 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio try { fragmentManager.executePendingTransactions(); } catch (final Exception e) { - Log.e(Config.LOGTAG,"unable to execute pending fragment transactions"); + Log.e(Config.LOGTAG, "unable to execute pending fragment transactions"); } } public boolean onXmppUriClicked(Uri uri) { XmppUri xmppUri = new XmppUri(uri); if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) { - final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri); + final Conversation conversation = + xmppConnectionService.findUniqueConversationByJid(xmppUri); if (conversation != null) { if (xmppUri.getParameter("password") != null) { xmppConnectionService.providePasswordForMuc(conversation, xmppUri.getParameter("password")); @@ -1293,7 +1333,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio final FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment); - final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); + final Fragment secondaryFragment = + fragmentManager.findFragmentById(R.id.secondary_fragment); if (mainFragment != null) { if (binding.secondaryFragment != null) { if (mainFragment instanceof ConversationFragment) { @@ -1348,7 +1389,8 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio return; } } - final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); + final Fragment secondaryFragment = + fragmentManager.findFragmentById(R.id.secondary_fragment); if (secondaryFragment instanceof ConversationFragment conversationFragment) { final Conversation conversation = conversationFragment.getConversation(); if (conversation != null) { @@ -1392,15 +1434,21 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio try { fragmentManager.popBackStack(); } catch (final IllegalStateException e) { - Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e); - //this usually means activity is no longer active; meaning on the next open we will run through this again + Log.w( + Config.LOGTAG, + "state loss while popping back state after archiving conversation", + e); + // this usually means activity is no longer active; meaning on the next open we will + // run through this again } return; } - final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment); + final Fragment secondaryFragment = + fragmentManager.findFragmentById(R.id.secondary_fragment); if (secondaryFragment instanceof ConversationFragment) { if (((ConversationFragment) secondaryFragment).getConversation() == conversation) { - Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation); + Conversation suggestion = + ConversationsOverviewFragment.getSuggestion(this, conversation); if (suggestion != null) { openConversation(suggestion, null); } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 770dfae7e971513a5bb13b9718095a36e158e0c9..25149a7979d56d77192d05fa48564154275d9aa8 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -586,6 +586,7 @@ public class EditAccountActivity extends OmemoActivity @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { + // TODO check for Camera / Scan permission super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) { updateAccountInformation(mAccount == null); @@ -764,7 +765,7 @@ public class EditAccountActivity extends OmemoActivity this.binding.hostname.addTextChangedListener(mTextWatcher); this.binding.hostname.setOnFocusChangeListener(mEditTextFocusListener); this.binding.clearDevices.setOnClickListener(v -> showWipePepDialog()); - this.binding.port.setText(String.valueOf(Resolver.DEFAULT_PORT_XMPP)); + this.binding.port.setText(String.valueOf(Resolver.XMPP_PORT_STARTTLS)); this.binding.port.addTextChangedListener(mTextWatcher); this.binding.saveButton.setOnClickListener(this.mSaveButtonClickListener); this.binding.cancelButton.setOnClickListener(this.mCancelButtonClickListener); @@ -1110,11 +1111,7 @@ public class EditAccountActivity extends OmemoActivity } private void deleteAccount() { - this.deleteAccount( - mAccount, - () -> { - finish(); - }); + this.deleteAccount(mAccount, () -> finish()); } private boolean inNeedOfSaslAccept() { @@ -1519,7 +1516,7 @@ public class EditAccountActivity extends OmemoActivity 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 + // be fed with reactivated devices this.binding.otherDeviceKeysCard.setVisibility(View.VISIBLE); Set otherDevices = mAccount.getAxolotlService().getOwnDeviceIds(); if (otherDevices == null || otherDevices.isEmpty()) { @@ -1548,12 +1545,17 @@ public class EditAccountActivity extends OmemoActivity } } else { final TextInputLayout errorLayout; - if (this.mAccount.errorStatus()) { - if (this.mAccount.getStatus() == Account.State.UNAUTHORIZED - || this.mAccount.getStatus() == Account.State.DOWNGRADE_ATTACK) { + final var status = this.mAccount.getStatus(); + if (status.isError() + || Arrays.asList( + Account.State.NO_INTERNET, + Account.State.MISSING_INTERNET_PERMISSION) + .contains(status)) { + if (status == Account.State.UNAUTHORIZED + || status == Account.State.DOWNGRADE_ATTACK) { errorLayout = this.binding.accountPasswordLayout; } else if (mShowOptions - && this.mAccount.getStatus() == Account.State.SERVER_NOT_FOUND + && status == Account.State.SERVER_NOT_FOUND && this.binding.hostname.getText().length() > 0) { errorLayout = this.binding.hostnameLayout; } else { diff --git a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java index e6587203316ff49b9ff4609976e75fec302d7a19..d0713219355b835c5095e3333073d5baeda28800 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java @@ -29,6 +29,8 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE; + import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.AnimatedImageDrawable; @@ -40,10 +42,9 @@ import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Toast; - import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; - +import com.canhub.cropper.CropImage; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; @@ -52,20 +53,15 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.ui.util.PendingItem; -import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE; - -import com.canhub.cropper.CropImage; - -public class PublishGroupChatProfilePictureActivity extends XmppActivity implements OnAvatarPublication { +public class PublishGroupChatProfilePictureActivity extends XmppActivity + implements OnAvatarPublication { private final PendingItem pendingConversationUuid = new PendingItem<>(); private ActivityPublishProfilePictureBinding binding; private Conversation conversation; private Uri uri; @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override protected void onBackendConnected() { @@ -97,17 +93,20 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - this.binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture); + this.binding = + DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture); + this.binding.contactOnly.setVisibility(View.GONE); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(this.binding.toolbar); configureActionBar(getSupportActionBar()); this.binding.cancelButton.setOnClickListener((v) -> this.finish()); this.binding.secondaryHint.setVisibility(View.GONE); - this.binding.accountImage.setOnClickListener((v) -> PublishProfilePictureActivity.chooseAvatar(this)); - Intent intent = getIntent(); - String uuid = intent == null ? null : intent.getStringExtra("uuid"); + this.binding.accountImage.setOnClickListener( + (v) -> PublishProfilePictureActivity.chooseAvatar(this)); + final var intent = getIntent(); + final var uuid = intent == null ? null : intent.getStringExtra("uuid"); if (uuid != null) { pendingConversationUuid.push(uuid); } @@ -115,25 +114,24 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme this.binding.publishButton.setOnClickListener(this::publish); } - - private void publish(View view) { + private void publish(final View view) { binding.publishButton.setText(R.string.publishing); binding.publishButton.setEnabled(false); xmppConnectionService.publishMucAvatar(conversation, uri, this); } @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { + public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { final CropImage.ActivityResult result = CropImage.getActivityResult(data); if (resultCode == RESULT_OK) { - this.uri = result.getUri(); + this.uri = result == null ? null : result.getUri(); if (xmppConnectionServiceBound) { reloadAvatar(); } } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { - Exception error = result.getError(); + final var error = result == null ? null : result.getError(); if (error != null) { Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show(); } @@ -162,18 +160,21 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity impleme @Override public void onAvatarPublicationSucceeded() { - runOnUiThread(() -> { - Toast.makeText(this, R.string.avatar_has_been_published, Toast.LENGTH_SHORT).show(); - finish(); - }); + runOnUiThread( + () -> { + Toast.makeText(this, R.string.avatar_has_been_published, Toast.LENGTH_SHORT) + .show(); + finish(); + }); } @Override public void onAvatarPublicationFailed(@StringRes int res) { - runOnUiThread(() -> { - Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); - this.binding.publishButton.setText(R.string.publish); - this.binding.publishButton.setEnabled(true); - }); + runOnUiThread( + () -> { + Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); + this.binding.publishButton.setText(R.string.publish); + this.binding.publishButton.setEnabled(true); + }); } } diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 11568dc03d60c6fae1576251e012ca89c7f145b7..7677e1e7510f5546302af6c96791015ab75fe206 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -14,19 +14,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnLongClickListener; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; - import com.canhub.cropper.CropImage; - -import java.util.concurrent.atomic.AtomicBoolean; - +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; @@ -35,83 +28,89 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.utils.PhoneHelper; +import java.util.concurrent.atomic.AtomicBoolean; -public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication { +public class PublishProfilePictureActivity extends XmppActivity + implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication { public static final int REQUEST_CHOOSE_PICTURE = 0x1337; - private ImageView avatar; - private TextView hintOrWarning; - private TextView secondaryHint; - private Button cancelButton; - private Button publishButton; + private ActivityPublishProfilePictureBinding binding; private Uri avatarUri; private Uri defaultUri; private Account account; private boolean support = false; private boolean publishing = false; private final AtomicBoolean handledExternalUri = new AtomicBoolean(false); - private final OnLongClickListener backToDefaultListener = new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - avatarUri = defaultUri; - loadImageIntoPreview(defaultUri); - return true; - } - }; + private final OnLongClickListener backToDefaultListener = + new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + avatarUri = defaultUri; + loadImageIntoPreview(defaultUri); + return true; + } + }; private boolean mInitialAccountSetup; @Override public void onAvatarPublicationSucceeded() { - runOnUiThread(() -> { - if (mInitialAccountSetup) { - Intent intent = new Intent(getApplicationContext(), StartConversationActivity.class); - StartConversationActivity.addInviteUri(intent, getIntent()); - intent.putExtra("init", true); - intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); - startActivity(intent); - } - Toast.makeText(PublishProfilePictureActivity.this, - R.string.avatar_has_been_published, - Toast.LENGTH_SHORT).show(); - finish(); - }); + runOnUiThread( + () -> { + if (mInitialAccountSetup) { + Intent intent = + new Intent( + getApplicationContext(), StartConversationActivity.class); + StartConversationActivity.addInviteUri(intent, getIntent()); + intent.putExtra("init", true); + intent.putExtra( + EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString()); + startActivity(intent); + } + Toast.makeText( + PublishProfilePictureActivity.this, + R.string.avatar_has_been_published, + Toast.LENGTH_SHORT) + .show(); + finish(); + }); } @Override - public void onAvatarPublicationFailed(int res) { - runOnUiThread(() -> { - hintOrWarning.setText(res); - hintOrWarning.setVisibility(View.VISIBLE); - publishing = false; - togglePublishButton(true, R.string.publish); - }); + public void onAvatarPublicationFailed(final int res) { + runOnUiThread( + () -> { + this.binding.hintOrWarning.setText(res); + this.binding.hintOrWarning.setVisibility(View.VISIBLE); + this.publishing = false; + togglePublishButton(true, R.string.publish); + }); } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - ActivityPublishProfilePictureBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture); + this.binding = + DataBindingUtil.setContentView(this, R.layout.activity_publish_profile_picture); setSupportActionBar(binding.toolbar); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); - this.avatar = findViewById(R.id.account_image); - this.cancelButton = findViewById(R.id.cancel_button); - this.publishButton = findViewById(R.id.publish_button); - this.hintOrWarning = findViewById(R.id.hint_or_warning); - this.secondaryHint = findViewById(R.id.secondary_hint); - this.publishButton.setOnClickListener(v -> { - if (avatarUri != null) { - publishing = true; - togglePublishButton(false, R.string.publishing); - xmppConnectionService.publishAvatar(account, avatarUri, this); - } - }); - this.cancelButton.setOnClickListener( + this.binding.publishButton.setOnClickListener( + v -> { + final boolean open = !this.binding.contactOnly.isChecked(); + final var uri = this.avatarUri; + if (uri == null) { + return; + } + publishing = true; + togglePublishButton(false, R.string.publishing); + xmppConnectionService.publishAvatarAsync(account, uri, open, this); + }); + this.binding.cancelButton.setOnClickListener( v -> { if (mInitialAccountSetup) { final Intent intent = @@ -127,11 +126,12 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC } finish(); }); - this.avatar.setOnClickListener(v -> chooseAvatar(this)); + this.binding.accountImage.setOnClickListener(v -> chooseAvatar(this)); this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); if (savedInstanceState != null) { this.avatarUri = savedInstanceState.getParcelable("uri"); - this.handledExternalUri.set(savedInstanceState.getBoolean("handle_external_uri",false)); + this.handledExternalUri.set( + savedInstanceState.getBoolean("handle_external_uri", false)); } } @@ -144,8 +144,8 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC @Override public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == R.id.action_delete_avatar) { - if (xmppConnectionService != null && account != null) { - xmppConnectionService.deleteAvatar(account); + if (account != null) { + deleteAvatar(account); } return true; } else { @@ -153,6 +153,22 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC } } + private void deleteAvatar(final Account account) { + new MaterialAlertDialogBuilder(this) + .setTitle(R.string.delete_avatar) + .setMessage(R.string.delete_avatar_message) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton( + R.string.confirm, + (d, v) -> { + if (xmppConnectionService != null) { + xmppConnectionService.deleteAvatar(account); + } + }) + .create() + .show(); + } + @Override public void onSaveInstanceState(@NonNull Bundle outState) { if (this.avatarUri != null) { @@ -162,7 +178,6 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC super.onSaveInstanceState(outState); } - @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); @@ -181,7 +196,7 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC } } else if (requestCode == REQUEST_CHOOSE_PICTURE) { if (resultCode == RESULT_OK) { - cropUri(data.getData()); + cropUri(this, data.getData()); } } } @@ -191,9 +206,9 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("image/*"); activity.startActivityForResult( - Intent.createChooser(intent, activity.getString(R.string.attach_choose_picture)), - REQUEST_CHOOSE_PICTURE - ); + Intent.createChooser( + intent, activity.getString(R.string.attach_choose_picture)), + REQUEST_CHOOSE_PICTURE); } else { CropImage.activity() .setOutputCompressFormat(Bitmap.CompressFormat.PNG) @@ -212,7 +227,9 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC } private void reloadAvatar() { - this.support = this.account.getXmppConnection() != null && this.account.getXmppConnection().getFeatures().pep(); + this.support = + this.account.getXmppConnection() != null + && this.account.getXmppConnection().getFeatures().pep(); if (this.avatarUri == null) { if (this.account.getAvatar() != null || this.defaultUri == null) { loadImageIntoPreview(null); @@ -233,80 +250,93 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC final Uri uri = intent != null ? intent.getData() : null; - if (uri != null && handledExternalUri.compareAndSet(false,true)) { - cropUri(uri); + if (uri != null && handledExternalUri.compareAndSet(false, true)) { + cropUri(this, uri); return; } if (this.mInitialAccountSetup) { - this.cancelButton.setText(R.string.skip); + this.binding.cancelButton.setText(R.string.skip); } - configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get()); + configureActionBar( + getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get()); } - public void cropUri(final Uri uri) { + public void cropUri(final Activity activity, final Uri uri) { if (Build.VERSION.SDK_INT >= 28) { loadImageIntoPreview(uri); - if (this.avatar.getDrawable() instanceof AnimatedImageDrawable || this.avatar.getDrawable() instanceof FileBackend.SVGDrawable) { + if (binding.accountImage.getDrawable() instanceof AnimatedImageDrawable || binding.accountImage.getDrawable() instanceof FileBackend.SVGDrawable) { this.avatarUri = uri; return; } } - CropImage.activity(uri).setOutputCompressFormat(Bitmap.CompressFormat.PNG) + CropImage.activity(uri) + .setOutputCompressFormat(Bitmap.CompressFormat.PNG) .setAspectRatio(1, 1) .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE) .start(this); } - protected void loadImageIntoPreview(Uri uri) { + protected void loadImageIntoPreview(final Uri uri) { Drawable bm = null; if (uri == null) { - bm = avatarService().get(account, (int) getResources().getDimension(R.dimen.publish_avatar_size)); + bm = + avatarService() + .get( + account, + (int) getResources().getDimension(R.dimen.publish_avatar_size)); } else { try { - bm = xmppConnectionService.getFileBackend().cropCenterSquareDrawable(uri, (int) getResources().getDimension(R.dimen.publish_avatar_size)); - } catch (Exception e) { + bm = + xmppConnectionService + .getFileBackend() + .cropCenterSquareDrawable( + uri, + (int) + getResources() + .getDimension(R.dimen.publish_avatar_size)); + } catch (final Exception e) { Log.d(Config.LOGTAG, "unable to load bitmap into image view", e); } } if (bm == null) { togglePublishButton(false, R.string.publish); - this.hintOrWarning.setVisibility(View.VISIBLE); - this.hintOrWarning.setText(R.string.error_publish_avatar_converting); + this.binding.hintOrWarning.setVisibility(View.VISIBLE); + this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting); return; } - this.avatar.setImageDrawable(bm); + this.binding.accountImage.setImageDrawable(bm); if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) { ((AnimatedImageDrawable) bm).start(); } if (support) { togglePublishButton(uri != null, R.string.publish); - this.hintOrWarning.setVisibility(View.INVISIBLE); + this.binding.hintOrWarning.setVisibility(View.INVISIBLE); } else { togglePublishButton(false, R.string.publish); - this.hintOrWarning.setVisibility(View.VISIBLE); + this.binding.hintOrWarning.setVisibility(View.VISIBLE); if (account.getStatus() == Account.State.ONLINE) { - this.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support); + this.binding.hintOrWarning.setText(R.string.error_publish_avatar_no_server_support); } else { - this.hintOrWarning.setText(R.string.error_publish_avatar_offline); + this.binding.hintOrWarning.setText(R.string.error_publish_avatar_offline); } } if (this.defaultUri == null || this.defaultUri.equals(uri)) { - this.secondaryHint.setVisibility(View.INVISIBLE); - this.avatar.setOnLongClickListener(null); + this.binding.secondaryHint.setVisibility(View.INVISIBLE); + this.binding.accountImage.setOnLongClickListener(null); } else if (this.defaultUri != null) { - this.secondaryHint.setVisibility(View.VISIBLE); - this.avatar.setOnLongClickListener(this.backToDefaultListener); + this.binding.secondaryHint.setVisibility(View.VISIBLE); + this.binding.accountImage.setOnLongClickListener(this.backToDefaultListener); } } protected void togglePublishButton(boolean enabled, @StringRes int res) { final boolean status = enabled && !publishing; - this.publishButton.setText(publishing ? R.string.publishing : res); - this.publishButton.setEnabled(status); + this.binding.publishButton.setText(publishing ? R.string.publishing : res); + this.binding.publishButton.setEnabled(status); } public void refreshUiReal() { @@ -319,5 +349,4 @@ public class PublishProfilePictureActivity extends XmppActivity implements XmppC public void onAccountUpdate() { refreshUi(); } - } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 0ef3625fdba7d98174a9308d7d5423416e632390..80b4537c16edb89fdcf1a201414695347934f8e8 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -32,6 +32,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; @@ -100,7 +101,8 @@ public class RtpSessionActivity extends XmppActivity public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; - private static final int CALL_DURATION_UPDATE_INTERVAL = 333; + private static final int CALL_DURATION_UPDATE_INTERVAL = 250; + private static final int BUTTON_VISIBILITY_TIMEOUT = 10_000; public static final List END_CARD = Arrays.asList( @@ -155,6 +157,8 @@ public class RtpSessionActivity extends XmppActivity mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); } }; + private boolean buttonsHiddenAfterTimeout = false; + private final Runnable mVisibilityToggleExecutor = this::updateButtonInVideoCallVisibility; public static Set actionToMedia(final String action) { if (ACTION_MAKE_VIDEO_CALL.equals(action)) { @@ -191,6 +195,8 @@ public class RtpSessionActivity extends XmppActivity | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session); + this.binding.remoteVideo.setOnClickListener(this::onVideoScreenClick); + this.binding.localVideo.setOnClickListener(this::onVideoScreenClick); setSupportActionBar(binding.toolbar); binding.dialpad.setClickConsumer(tag -> { @@ -207,6 +213,10 @@ public class RtpSessionActivity extends XmppActivity Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); } + private void onVideoScreenClick(final View view) { + resetVisibilityExecutorShowButtons(); + } + @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.activity_rtp_session, menu); @@ -573,15 +583,17 @@ public class RtpSessionActivity extends XmppActivity final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE); final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState); + final var contact = account.getRoster().getContact(with); if (state != null) { Log.d(Config.LOGTAG, "restored last state from intent extra"); updateButtonConfiguration(state); updateVerifiedShield(false); updateStateDisplay(state); updateIncomingCallScreen(state); + updateSupportWarning(state, contact); invalidateOptionsMenu(); } - setWith(account.getRoster().getContact(with), state); + setWith(state, contact); if (xmppConnectionService .getJingleConnectionManager() .fireJingleRtpConnectionStateUpdates()) { @@ -604,10 +616,10 @@ public class RtpSessionActivity extends XmppActivity } private void setWith(final RtpEndUserState state) { - setWith(getWith(), state); + setWith(state, getWith()); } - private void setWith(final Contact contact, final RtpEndUserState state) { + private void setWith(final RtpEndUserState state, final Contact contact) { binding.with.setText(contact.getDisplayName()); if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL) .contains(state)) { @@ -655,12 +667,20 @@ public class RtpSessionActivity extends XmppActivity public void onStart() { super.onStart(); mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL); + mHandler.postDelayed(mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT); this.binding.remoteVideo.setOnAspectRatioChanged(this); } + @Override + public void onResume() { + super.onResume(); + resetVisibilityExecutorShowButtons(); + } + @Override public void onStop() { mHandler.removeCallbacks(mTickExecutor); + mHandler.removeCallbacks(mVisibilityToggleExecutor); binding.remoteVideo.release(); binding.remoteVideo.setOnAspectRatioChanged(null); binding.localVideo.release(); @@ -782,6 +802,17 @@ public class RtpSessionActivity extends XmppActivity } } + private boolean isInConnectedVideoCall() { + final JingleRtpConnection rtpConnection; + try { + rtpConnection = requireRtpConnection(); + } catch (final IllegalStateException e) { + return false; + } + return rtpConnection.getMedia().contains(Media.VIDEO) + && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED; + } + private boolean initializeActivityWithRunningRtpSession( final Account account, Jid with, String sessionId) { final WeakReference reference = @@ -844,7 +875,9 @@ public class RtpSessionActivity extends XmppActivity updateCallDuration(); updateVerifiedShield(false); invalidateOptionsMenu(); - setWith(account.getRoster().getContact(with), state); + final var contact = account.getRoster().getContact(with); + setWith(state, contact); + updateSupportWarning(state, contact); } private void reInitializeActivityWithRunningRtpSession( @@ -885,7 +918,7 @@ public class RtpSessionActivity extends XmppActivity final ContentAddition contentAddition) { switch (state) { case INCOMING_CALL -> { - Preconditions.checkArgument(media.size() > 0, "Media must not be empty"); + Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty"); if (media.contains(Media.VIDEO)) { setTitle(R.string.rtp_state_incoming_video_call); } else { @@ -934,7 +967,7 @@ public class RtpSessionActivity extends XmppActivity private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) { if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) { - final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call); + final boolean show = getResources().getBoolean(R.bool.is_portrait_mode); if (show) { binding.contactPhoto.setVisibility(View.VISIBLE); if (contact == null) { @@ -959,6 +992,18 @@ public class RtpSessionActivity extends XmppActivity } } + private void updateSupportWarning(final RtpEndUserState state, final Contact contact) { + if (state == RtpEndUserState.CONNECTIVITY_ERROR + && getResources().getBoolean(R.bool.is_portrait_mode)) { + binding.supportWarning.setVisibility( + RtpCapability.check(contact) == RtpCapability.Capability.NONE + ? View.VISIBLE + : View.GONE); + } else { + binding.supportWarning.setVisibility(View.GONE); + } + } + private Set getMedia() { return requireRtpConnection().getMedia(); } @@ -976,7 +1021,9 @@ public class RtpSessionActivity extends XmppActivity final RtpEndUserState state, final Set media, final ContentAddition contentAddition) { - if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) { + if (state == RtpEndUserState.ENDING_CALL + || isPictureInPicture() + || this.buttonsHiddenAfterTimeout) { this.binding.rejectCall.setVisibility(View.INVISIBLE); this.binding.endCall.setVisibility(View.INVISIBLE); this.binding.acceptCall.setVisibility(View.INVISIBLE); @@ -1033,12 +1080,17 @@ public class RtpSessionActivity extends XmppActivity this.binding.endCall.setContentDescription(getString(R.string.hang_up)); this.binding.endCall.setOnClickListener(this::endCall); this.binding.endCall.setImageResource(R.drawable.ic_call_end_24dp); - this.binding.endCall.setVisibility(View.VISIBLE); + setVisibleAndShow(this.binding.endCall); this.binding.acceptCall.setVisibility(View.INVISIBLE); } updateInCallButtonConfiguration(state, media); } + private static void setVisibleAndShow(final FloatingActionButton button) { + button.show(); + button.setVisibility(View.VISIBLE); + } + private boolean isPictureInPicture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { return isInPictureInPictureMode(); @@ -1055,7 +1107,8 @@ public class RtpSessionActivity extends XmppActivity @SuppressLint("RestrictedApi") private void updateInCallButtonConfiguration( final RtpEndUserState state, final Set media) { - if (STATES_CONSIDERED_CONNECTED.contains(state) && !isPictureInPicture()) { + final var showButtons = !isPictureInPicture() && !buttonsHiddenAfterTimeout; + if (STATES_CONSIDERED_CONNECTED.contains(state) && showButtons) { Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty"); if (media.contains(Media.VIDEO)) { final JingleRtpConnection rtpConnection = requireRtpConnection(); @@ -1075,7 +1128,7 @@ public class RtpSessionActivity extends XmppActivity this.binding.inCallActionLeft.setVisibility(View.GONE); } } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state) - && !isPictureInPicture() + && showButtons && Media.audioOnly(media)) { final CallIntegration callIntegration; try { @@ -1140,17 +1193,17 @@ public class RtpSessionActivity extends XmppActivity this.binding.inCallActionRight.setClickable(false); } } - this.binding.inCallActionRight.setVisibility(View.VISIBLE); + setVisibleAndShow(this.binding.inCallActionRight); } @SuppressLint("RestrictedApi") private void updateInCallButtonConfigurationVideo( final boolean videoEnabled, final boolean isCameraSwitchable) { - this.binding.inCallActionRight.setVisibility(View.VISIBLE); + setVisibleAndShow(this.binding.inCallActionRight); if (isCameraSwitchable) { this.binding.inCallActionFarRight.setImageResource( R.drawable.ic_flip_camera_android_24dp); - this.binding.inCallActionFarRight.setVisibility(View.VISIBLE); + setVisibleAndShow(this.binding.inCallActionFarRight); this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera); this.binding.inCallActionFarRight.setContentDescription( getString(R.string.flip_camera)); @@ -1171,6 +1224,7 @@ public class RtpSessionActivity extends XmppActivity } private void switchCamera(final View view) { + resetVisibilityToggleExecutor(); Futures.addCallback( requireRtpConnection().switchCamera(), new FutureCallback<>() { @@ -1196,6 +1250,7 @@ public class RtpSessionActivity extends XmppActivity } private void enableVideo(final View view) { + resetVisibilityToggleExecutor(); try { requireRtpConnection().setVideoEnabled(true); } catch (final IllegalStateException e) { @@ -1206,6 +1261,7 @@ public class RtpSessionActivity extends XmppActivity } private void disableVideo(final View view) { + resetVisibilityToggleExecutor(); final JingleRtpConnection rtpConnection = requireRtpConnection(); final ContentAddition pending = rtpConnection.getPendingContentAddition(); if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) { @@ -1230,7 +1286,7 @@ public class RtpSessionActivity extends XmppActivity this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_24dp); this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone); } - this.binding.inCallActionLeft.setVisibility(View.VISIBLE); + setVisibleAndShow(this.binding.inCallActionLeft); } private void updateCallDuration() { @@ -1249,6 +1305,47 @@ public class RtpSessionActivity extends XmppActivity } } + private void resetVisibilityToggleExecutor() { + mHandler.removeCallbacks(this.mVisibilityToggleExecutor); + mHandler.postDelayed(this.mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT); + } + + private void updateButtonInVideoCallVisibility() { + if (isInConnectedVideoCall()) { + if (isPictureInPicture()) { + return; + } + Log.d(Config.LOGTAG, "hiding in-call buttons after timeout was reached"); + hideInCallButtons(); + } + } + + private void hideInCallButtons() { + binding.inCallActionLeft.hide(); + binding.endCall.hide(); + binding.inCallActionRight.hide(); + binding.inCallActionFarRight.hide(); + } + + private void showInCallButtons() { + this.buttonsHiddenAfterTimeout = false; + final JingleRtpConnection rtpConnection; + try { + rtpConnection = requireRtpConnection(); + } catch (final IllegalStateException e) { + return; + } + updateButtonConfiguration( + rtpConnection.getEndUserState(), + rtpConnection.getMedia(), + rtpConnection.getPendingContentAddition()); + } + + private void resetVisibilityExecutorShowButtons() { + resetVisibilityToggleExecutor(); + showInCallButtons(); + } + private void updateVideoViews(final RtpEndUserState state) { if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) { binding.localVideo.setVisibility(View.GONE); @@ -1343,17 +1440,23 @@ public class RtpSessionActivity extends XmppActivity return connection.getRemoteVideoTrack(); } - private void disableMicrophone(View view) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - if (rtpConnection.setMicrophoneEnabled(false)) { - updateInCallButtonConfiguration(); - } + private void disableMicrophone(final View view) { + setMicrophoneEnabled(false); } - private void enableMicrophone(View view) { - final JingleRtpConnection rtpConnection = requireRtpConnection(); - if (rtpConnection.setMicrophoneEnabled(true)) { - updateInCallButtonConfiguration(); + private void enableMicrophone(final View view) { + setMicrophoneEnabled(true); + } + + private void setMicrophoneEnabled(final boolean enabled) { + resetVisibilityExecutorShowButtons(); + try { + final JingleRtpConnection rtpConnection = requireRtpConnection(); + if (rtpConnection.setMicrophoneEnabled(enabled)) { + updateInCallButtonConfiguration(); + } + } catch (final IllegalStateException e) { + Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show(); } } @@ -1362,7 +1465,7 @@ public class RtpSessionActivity extends XmppActivity requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE); acquireProximityWakeLock(); } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show(); } } @@ -1371,7 +1474,7 @@ public class RtpSessionActivity extends XmppActivity requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE); releaseProximityWakeLock(); } catch (final IllegalStateException e) { - Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show(); } } @@ -1461,6 +1564,7 @@ public class RtpSessionActivity extends XmppActivity () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); } if (with.isBareJid()) { + // TODO check for ENDED updateRtpSessionProposalState(account, with, state); return; } @@ -1484,6 +1588,7 @@ public class RtpSessionActivity extends XmppActivity finish(); return; } + resetVisibilityToggleExecutor(); runOnUiThread( () -> { updateStateDisplay(state, media, contentAddition); @@ -1492,6 +1597,7 @@ public class RtpSessionActivity extends XmppActivity updateButtonConfiguration(state, media, contentAddition); updateVideoViews(state); updateIncomingCallScreen(state, contact); + updateSupportWarning(state, contact); invalidateOptionsMenu(); }); if (END_CARD.contains(state)) { @@ -1569,6 +1675,7 @@ public class RtpSessionActivity extends XmppActivity updateStateDisplay(state); updateButtonConfiguration(state, media, null); updateIncomingCallScreen(state); + updateSupportWarning(state, account.getRoster().getContact(with)); invalidateOptionsMenu(); }); resetIntent(account, with, state, media); diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 44f43e722fbf51f6b8af3f1935f40b4a84b05f6e..ddc6e253ccbb71717786c2d91f77f2d4c249e98f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -8,11 +8,11 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; - import androidx.annotation.NonNull; +import androidx.core.content.pm.ShortcutManagerCompat; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.LinearLayoutManager; - +import com.google.common.collect.Iterables; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityShareWithBinding; @@ -21,7 +21,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.xmpp.Jid; - import java.util.ArrayList; import java.util.List; @@ -112,7 +111,34 @@ public class ShareWithActivity extends XmppActivity new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)); binding.chooseConversationList.setAdapter(mAdapter); mAdapter.setConversationClickListener((view, conversation) -> share(conversation)); + final var intent = getIntent(); + final var shortcutId = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID); this.share = new Share(); + if (shortcutId != null) { + final var conversation = shortcutIdToConversation(shortcutId); + if (conversation != null) { + // we have everything we need. Jump into chat + populateShare(intent); + share(conversation); + } + } + } + + private String shortcutIdToConversation(final String shortcutId) { + final var shortcut = + Iterables.tryFind( + ShortcutManagerCompat.getDynamicShortcuts(this), + si -> si.getId().equals(shortcutId)); + if (shortcut.isPresent()) { + final var extras = shortcut.get().getExtras(); + if (extras == null) { + return null; + } else { + return extras.getString(ConversationsActivity.EXTRA_CONVERSATION); + } + } else { + return null; + } } @Override @@ -137,10 +163,18 @@ public class ShareWithActivity extends XmppActivity @Override public void onStart() { super.onStart(); - Intent intent = getIntent(); + final Intent intent = getIntent(); if (intent == null) { return; } + populateShare(intent); + if (xmppConnectionServiceBound) { + xmppConnectionService.populateWithOrderedConversations( + mConversations, this.share.uris.isEmpty(), false); + } + } + + private void populateShare(final Intent intent) { final String type = intent.getType(); final String action = intent.getAction(); final Uri data = intent.getData(); @@ -217,8 +251,12 @@ public class ShareWithActivity extends XmppActivity mPendingConversation = conversation; return; } + share(conversation.getUuid()); + } + + private void share(final String conversation) { final Intent intent = new Intent(this, ConversationsActivity.class); - intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); + intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation); if (!share.uris.isEmpty()) { intent.setAction(Intent.ACTION_SEND_MULTIPLE); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, share.uris); @@ -233,7 +271,7 @@ public class ShareWithActivity extends XmppActivity } try { startActivity(intent); - } catch (SecurityException e) { + } catch (final SecurityException e) { Toast.makeText( this, R.string.sharing_application_not_grant_permission, diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 77a8c4406bdd770a1c881089c5afc93d9d637ffb..b2329093c26418588c9b7936c925fd360e95757a 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -1084,7 +1084,7 @@ public class StartConversationActivity extends XmppActivity super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (grantResults.length > 0) if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); + UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) { if (QuickConversationsService.isQuicksy()) { setRefreshing(true); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 22708826d27ceee38eeb033f66366e46269d6f17..00e7decb39e2347dfcb92ad3f629437f6cd923e8 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -4,6 +4,7 @@ import android.telephony.TelephonyManager; import android.Manifest; import android.annotation.SuppressLint; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.ClipData; @@ -35,6 +36,7 @@ import android.os.IBinder; import android.os.PowerManager; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.provider.Settings; import android.text.Html; import android.text.InputType; import android.util.DisplayMetrics; @@ -48,14 +50,15 @@ import android.widget.Button; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.Toast; - import androidx.annotation.BoolRes; import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; import androidx.databinding.DataBindingUtil; - import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Strings; @@ -86,6 +89,7 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Reaction; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.BarcodeProvider; +import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; @@ -95,17 +99,14 @@ import eu.siacs.conversations.ui.util.SettingsUtils; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; -import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.SignupUtils; import eu.siacs.conversations.utils.ThemeHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; - import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.RejectedExecutionException; @@ -131,52 +132,60 @@ public abstract class XmppActivity extends ActionBarActivity { protected boolean mUsingEnterKey = false; protected boolean mUseTor = false; protected Toast mToast; - public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show(); + public Runnable onOpenPGPKeyPublished = + () -> + Toast.makeText( + XmppActivity.this, + R.string.openpgp_has_been_published, + Toast.LENGTH_SHORT) + .show(); protected ConferenceInvite mPendingConferenceInvite = null; protected PriorityQueue>> activityCallbacks = Build.VERSION.SDK_INT >= 24 ? new PriorityQueue<>((x, y) -> y.first.compareTo(x.first)) : new PriorityQueue<>(); - protected ServiceConnection mConnection = new ServiceConnection() { + protected ServiceConnection mConnection = + new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - XmppConnectionBinder binder = (XmppConnectionBinder) service; - xmppConnectionService = binder.getService(); - xmppConnectionServiceBound = true; - registerListeners(); - onBackendConnected(); - } + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + XmppConnectionBinder binder = (XmppConnectionBinder) service; + xmppConnectionService = binder.getService(); + xmppConnectionServiceBound = true; + registerListeners(); + onBackendConnected(); + } - @Override - public void onServiceDisconnected(ComponentName arg0) { - xmppConnectionServiceBound = false; - } - }; + @Override + public void onServiceDisconnected(ComponentName arg0) { + xmppConnectionServiceBound = false; + } + }; private DisplayMetrics metrics; private long mLastUiRefresh = 0; private final Handler mRefreshUiHandler = new Handler(); - private final Runnable mRefreshUiRunnable = () -> { - mLastUiRefresh = SystemClock.elapsedRealtime(); - refreshUiReal(); - }; - private final UiCallback adhocCallback = new UiCallback() { - @Override - public void success(final Conversation conversation) { - runOnUiThread(() -> { - switchToConversation(conversation); - hideToast(); - }); - } - - @Override - public void error(final int errorCode, Conversation object) { - runOnUiThread(() -> replaceToast(getString(errorCode))); - } + private final Runnable mRefreshUiRunnable = + () -> { + mLastUiRefresh = SystemClock.elapsedRealtime(); + refreshUiReal(); + }; + private final UiCallback adhocCallback = + new UiCallback() { + @Override + public void success(final Conversation conversation) { + runOnUiThread( + () -> { + switchToConversation(conversation); + hideToast(); + }); + } - @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) {} + }; public static boolean cancelPotentialWork(Message message, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); @@ -233,7 +242,7 @@ public abstract class XmppActivity extends ActionBarActivity { } } - abstract protected void refreshUiReal(); + protected abstract void refreshUiReal(); @Override public void onStart() { @@ -272,6 +281,31 @@ public abstract class XmppActivity extends ActionBarActivity { } } + @RequiresApi(api = Build.VERSION_CODES.R) + protected void configureCustomNotification(final ShortcutInfoCompat shortcut) { + final var notificationManager = getSystemService(NotificationManager.class); + final var channel = + notificationManager.getNotificationChannel( + NotificationService.MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId()); + if (channel != null && channel.getConversationId() != null) { + ShortcutManagerCompat.pushDynamicShortcut(this, shortcut); + openNotificationSettings(shortcut); + } else { + NotificationService.createConversationChannel(this, shortcut); + ShortcutManagerCompat.pushDynamicShortcut(this, shortcut); + openNotificationSettings(shortcut); + } + } + + @RequiresApi(api = Build.VERSION_CODES.R) + protected void openNotificationSettings(final ShortcutInfoCompat shortcut) { + final var intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName()); + intent.putExtra( + Settings.EXTRA_CHANNEL_ID, NotificationService.MESSAGES_NOTIFICATION_CHANNEL); + intent.putExtra(Settings.EXTRA_CONVERSATION_ID, shortcut.getId()); + startActivity(intent); + } public boolean hasPgp() { return xmppConnectionService.getPgpEngine() != null; @@ -281,16 +315,20 @@ public abstract class XmppActivity extends ActionBarActivity { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(getString(R.string.openkeychain_required)); builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name)))); + builder.setMessage( + Html.fromHtml( + getString( + R.string.openkeychain_required_long, + getString(R.string.app_name)))); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton(getString(R.string.restart), + builder.setNeutralButton( + getString(R.string.restart), (dialog, which) -> { if (xmppConnectionServiceBound) { unbindService(mConnection); xmppConnectionServiceBound = false; } - stopService(new Intent(XmppActivity.this, - XmppConnectionService.class)); + stopService(new Intent(XmppActivity.this, XmppConnectionService.class)); finish(); }); builder.setPositiveButton( @@ -350,6 +388,14 @@ public abstract class XmppActivity extends ActionBarActivity { dialog.dismiss(); }); } + viewBinding.more.setOnClickListener( + v -> { + dialog.dismiss(); + final var intent = new Intent(this, AddReactionActivity.class); + intent.putExtra("conversation", message.getConversation().getUuid()); + intent.putExtra("message", message.getUuid()); + startActivity(intent); + }); dialog.show(); } @@ -360,58 +406,89 @@ public abstract class XmppActivity extends ActionBarActivity { protected void deleteAccount(final Account account, final Runnable postDelete) { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null); - final CheckBox deleteFromServer = - dialogView.findViewById(R.id.delete_from_server); + final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server); builder.setView(dialogView); builder.setTitle(R.string.mgmt_account_delete); - builder.setPositiveButton(getString(R.string.delete),null); + builder.setPositiveButton(getString(R.string.delete), null); builder.setNegativeButton(getString(R.string.cancel), null); final AlertDialog dialog = builder.create(); - dialog.setOnShowListener(dialogInterface->{ - final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); - button.setOnClickListener(v -> { - final boolean unregister = deleteFromServer.isChecked(); - if (unregister) { - if (account.isOnlineAndConnected()) { - deleteFromServer.setEnabled(false); - button.setText(R.string.please_wait); - button.setEnabled(false); - xmppConnectionService.unregisterAccount(account, result -> { - runOnUiThread(()->{ - if (result) { - dialog.dismiss(); - if (postDelete != null) { - postDelete.run(); + dialog.setOnShowListener( + dialogInterface -> { + final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + button.setOnClickListener( + v -> { + final boolean unregister = deleteFromServer.isChecked(); + if (unregister) { + if (account.isOnlineAndConnected()) { + deleteFromServer.setEnabled(false); + button.setText(R.string.please_wait); + button.setEnabled(false); + xmppConnectionService.unregisterAccount( + account, + result -> { + runOnUiThread( + () -> { + if (result) { + dialog.dismiss(); + if (postDelete != null) { + postDelete.run(); + } + if (xmppConnectionService + .getAccounts() + .size() + == 0 + && Config + .MAGIC_CREATE_DOMAIN + != null) { + final Intent intent = + SignupUtils + .getSignUpIntent( + this); + intent.setFlags( + Intent + .FLAG_ACTIVITY_NEW_TASK + | Intent + .FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + } else { + deleteFromServer.setEnabled( + true); + button.setText(R.string.delete); + button.setEnabled(true); + Toast.makeText( + this, + R.string + .could_not_delete_account_from_server, + Toast + .LENGTH_LONG) + .show(); + } + }); + }); + } else { + Toast.makeText( + this, + R.string.not_connected_try_again, + Toast.LENGTH_LONG) + .show(); } - if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { + } else { + xmppConnectionService.deleteAccount(account); + dialog.dismiss(); + if (xmppConnectionService.getAccounts().size() == 0 + && Config.MAGIC_CREATE_DOMAIN != null) { final Intent intent = SignupUtils.getSignUpIntent(this); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); + } else if (postDelete != null) { + postDelete.run(); } - } else { - deleteFromServer.setEnabled(true); - button.setText(R.string.delete); - button.setEnabled(true); - Toast.makeText(this,R.string.could_not_delete_account_from_server,Toast.LENGTH_LONG).show(); } }); - }); - } else { - Toast.makeText(this,R.string.not_connected_try_again,Toast.LENGTH_LONG).show(); - } - } else { - xmppConnectionService.deleteAccount(account); - dialog.dismiss(); - if (xmppConnectionService.getAccounts().size() == 0 && Config.MAGIC_CREATE_DOMAIN != null) { - final Intent intent = SignupUtils.getSignUpIntent(this); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - } else if (postDelete != null) { - postDelete.run(); - } - } - }); - }); + }); dialog.show(); } @@ -419,61 +496,75 @@ public abstract class XmppActivity extends ActionBarActivity { protected void registerListeners() { if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); + this.xmppConnectionService.setOnConversationListChangedListener( + (XmppConnectionService.OnConversationUpdate) this); } if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); + this.xmppConnectionService.setOnAccountListChangedListener( + (XmppConnectionService.OnAccountUpdate) this); } if (this instanceof XmppConnectionService.OnCaptchaRequested) { - this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); + this.xmppConnectionService.setOnCaptchaRequestedListener( + (XmppConnectionService.OnCaptchaRequested) this); } if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); + this.xmppConnectionService.setOnRosterUpdateListener( + (XmppConnectionService.OnRosterUpdate) this); } if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); + this.xmppConnectionService.setOnMucRosterUpdateListener( + (XmppConnectionService.OnMucRosterUpdate) this); } if (this instanceof OnUpdateBlocklist) { this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); } if (this instanceof XmppConnectionService.OnShowErrorToast) { - this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); + this.xmppConnectionService.setOnShowErrorToastListener( + (XmppConnectionService.OnShowErrorToast) this); } if (this instanceof OnKeyStatusUpdated) { this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this); } if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { - this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); + this.xmppConnectionService.setOnRtpConnectionUpdateListener( + (XmppConnectionService.OnJingleRtpConnectionUpdate) this); } } protected void unregisterListeners() { if (this instanceof XmppConnectionService.OnConversationUpdate) { - this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this); + this.xmppConnectionService.removeOnConversationListChangedListener( + (XmppConnectionService.OnConversationUpdate) this); } if (this instanceof XmppConnectionService.OnAccountUpdate) { - this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this); + this.xmppConnectionService.removeOnAccountListChangedListener( + (XmppConnectionService.OnAccountUpdate) this); } if (this instanceof XmppConnectionService.OnCaptchaRequested) { - this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this); + this.xmppConnectionService.removeOnCaptchaRequestedListener( + (XmppConnectionService.OnCaptchaRequested) this); } if (this instanceof XmppConnectionService.OnRosterUpdate) { - this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this); + this.xmppConnectionService.removeOnRosterUpdateListener( + (XmppConnectionService.OnRosterUpdate) this); } if (this instanceof XmppConnectionService.OnMucRosterUpdate) { - this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); + this.xmppConnectionService.removeOnMucRosterUpdateListener( + (XmppConnectionService.OnMucRosterUpdate) this); } if (this instanceof OnUpdateBlocklist) { this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this); } if (this instanceof XmppConnectionService.OnShowErrorToast) { - this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); + this.xmppConnectionService.removeOnShowErrorToastListener( + (XmppConnectionService.OnShowErrorToast) this); } if (this instanceof OnKeyStatusUpdated) { this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this); } if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) { - this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this); + this.xmppConnectionService.removeRtpConnectionUpdateListener( + (XmppConnectionService.OnJingleRtpConnectionUpdate) this); } } @@ -481,7 +572,9 @@ public abstract class XmppActivity extends ActionBarActivity { public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: - startActivity(new Intent(this, eu.siacs.conversations.ui.activity.SettingsActivity.class)); + startActivity( + new Intent( + this, eu.siacs.conversations.ui.activity.SettingsActivity.class)); break; case R.id.action_privacy_policy: openPrivacyPolicy(); @@ -516,7 +609,8 @@ public abstract class XmppActivity extends ActionBarActivity { } } - public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { + public void selectPresence( + final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { final Contact contact = conversation.getContact(); if (contact.showInRoster() || contact.isSelf()) { final Presences presences = contact.getPresences(); @@ -537,7 +631,8 @@ public abstract class XmppActivity extends ActionBarActivity { } } else if (presences.size() == 1) { final String presence = presences.toResourceArray()[0]; - conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence)); + conversation.setNextCounterpart( + PresenceSelector.getNextCounterpart(contact, presence)); listener.onPresenceSelected(); } else { PresenceSelector.showPresenceSelectionDialog(this, conversation, listener); @@ -552,7 +647,8 @@ public abstract class XmppActivity extends ActionBarActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); metrics = getResources().getDisplayMetrics(); - this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + this.isCameraFeatureAvailable = + getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); this.mCustomColors = ThemeHelper.applyCustomColors(this); } @@ -563,14 +659,16 @@ public abstract class XmppActivity extends ActionBarActivity { protected boolean isOptimizingBattery() { final PowerManager pm = getSystemService(PowerManager.class); return !pm.isIgnoringBatteryOptimizations(getPackageName()); -} + } protected boolean isAffectedByDataSaver() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + final ConnectivityManager cm = + (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); return cm != null && cm.isActiveNetworkMetered() - && Compatibility.getRestrictBackgroundStatus(cm) == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + && Compatibility.getRestrictBackgroundStatus(cm) + == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; } else { return false; } @@ -647,9 +745,16 @@ public abstract class XmppActivity extends ActionBarActivity { switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null); } - public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit, String thread) { + public void switchToConversation( + Conversation conversation, + String text, + boolean asQuote, + String nick, + boolean pm, + boolean doNotAppend, + String postInit, + String thread) { if (conversation == null) return; - Intent intent = new Intent(this, ConversationsActivity.class); intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION); intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid()); @@ -699,7 +804,10 @@ public abstract class XmppActivity extends ActionBarActivity { intent.putExtra("jid", account.getJid().asBareJid().toEscapedString()); intent.putExtra("init", init); if (init) { - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); + intent.setFlags( + Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NO_ANIMATION); } if (fingerprint != null) { intent.putExtra("fingerprint", fingerprint); @@ -723,85 +831,113 @@ public abstract class XmppActivity extends ActionBarActivity { } protected void inviteToConversation(Conversation conversation) { - startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION); + startActivityForResult( + ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION); } - protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) { + protected void announcePgp( + final Account account, + final Conversation conversation, + Intent intent, + final Runnable onSuccess) { if (account.getPgpId() == 0) { choosePgpSignId(account); } else { final String status = Strings.nullToEmpty(account.getPresenceStatusMessage()); - xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback() { - - @Override - public void userInputRequired(final PendingIntent pi, final String signature) { - try { - startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0,Compatibility.pgpStartIntentSenderOptions()); - } catch (final SendIntentException ignored) { - } - } + xmppConnectionService + .getPgpEngine() + .generateSignature( + intent, + account, + status, + new UiCallback() { + + @Override + public void userInputRequired( + final PendingIntent pi, final String signature) { + try { + startIntentSenderForResult( + pi.getIntentSender(), + REQUEST_ANNOUNCE_PGP, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); + } catch (final SendIntentException ignored) { + } + } - @Override - public void success(String signature) { - account.setPgpSignature(signature); - xmppConnectionService.databaseBackend.updateAccount(account); - xmppConnectionService.sendPresence(account); - if (conversation != null) { - conversation.setNextEncryption(Message.ENCRYPTION_PGP); - xmppConnectionService.updateConversation(conversation); - refreshUi(); - } - if (onSuccess != null) { - runOnUiThread(onSuccess); - } - } + @Override + public void success(String signature) { + account.setPgpSignature(signature); + xmppConnectionService.databaseBackend.updateAccount(account); + xmppConnectionService.sendPresence(account); + if (conversation != null) { + conversation.setNextEncryption(Message.ENCRYPTION_PGP); + xmppConnectionService.updateConversation(conversation); + refreshUi(); + } + if (onSuccess != null) { + runOnUiThread(onSuccess); + } + } - @Override - public void error(int error, String signature) { - if (error == 0) { - account.setPgpSignId(0); - account.unsetPgpSignature(); - xmppConnectionService.databaseBackend.updateAccount(account); - choosePgpSignId(account); - } else { - displayErrorDialog(error); - } - } - }); + @Override + public void error(int error, String signature) { + if (error == 0) { + account.setPgpSignId(0); + account.unsetPgpSignature(); + xmppConnectionService.databaseBackend.updateAccount( + account); + choosePgpSignId(account); + } else { + displayErrorDialog(error); + } + } + }); } } protected void choosePgpSignId(final Account account) { - xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<>() { - @Override - public void success(final Account a) { - } - - @Override - public void error(int errorCode, Account object) { - - } - - @Override - public void userInputRequired(PendingIntent pi, Account object) { - try { - startIntentSenderForResult(pi.getIntentSender(), - REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0, Compatibility.pgpStartIntentSenderOptions()); - } catch (final SendIntentException ignored) { - } - } - }); + xmppConnectionService + .getPgpEngine() + .chooseKey( + account, + new UiCallback<>() { + @Override + public void success(final Account a) {} + + @Override + public void error(int errorCode, Account object) {} + + @Override + public void userInputRequired(PendingIntent pi, Account object) { + try { + startIntentSenderForResult( + pi.getIntentSender(), + REQUEST_CHOOSE_PGP_ID, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); + } catch (final SendIntentException ignored) { + } + } + }); } protected void displayErrorDialog(final int errorCode) { - runOnUiThread(() -> { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(XmppActivity.this); - builder.setTitle(getString(R.string.error)); - builder.setMessage(errorCode); - builder.setNeutralButton(R.string.accept, null); - builder.create().show(); - }); - + runOnUiThread( + () -> { + final MaterialAlertDialogBuilder builder = + new MaterialAlertDialogBuilder(XmppActivity.this); + builder.setTitle(getString(R.string.error)); + builder.setMessage(errorCode); + builder.setNeutralButton(R.string.accept, null); + builder.create().show(); + }); } protected void showAddToRosterDialog(final Contact contact) { @@ -821,13 +957,15 @@ public abstract class XmppActivity extends ActionBarActivity { builder.setTitle(contact.getJid().toString()); builder.setMessage(R.string.request_presence_updates); builder.setNegativeButton(R.string.cancel, null); - builder.setPositiveButton(R.string.request_now, + builder.setPositiveButton( + R.string.request_now, (dialog, which) -> { if (xmppConnectionServiceBound) { - xmppConnectionService.sendPresencePacket(contact - .getAccount(), xmppConnectionService - .getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); + xmppConnectionService.sendPresencePacket( + contact.getAccount(), + xmppConnectionService + .getPresenceGenerator() + .requestPresenceUpdatesFrom(contact)); } }); builder.create().show(); @@ -837,7 +975,11 @@ public abstract class XmppActivity extends ActionBarActivity { quickEdit(previousValue, callback, hint, false, false); } - protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) { + protected void quickEdit( + String previousValue, + @StringRes int hint, + OnValueEdited callback, + boolean permitEmpty) { quickEdit(previousValue, callback, hint, false, permitEmpty); } @@ -862,9 +1004,12 @@ public abstract class XmppActivity extends ActionBarActivity { boolean alwaysCallback, boolean startSelected) { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - final DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false); + final DialogQuickeditBinding binding = + DataBindingUtil.inflate( + getLayoutInflater(), R.layout.dialog_quickedit, null, false); if (password) { - binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + binding.inputEditText.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } builder.setPositiveButton(R.string.accept, null); if (hint != 0) { @@ -882,33 +1027,39 @@ public abstract class XmppActivity extends ActionBarActivity { if (startSelected) { binding.inputEditText.selectAll(); } - View.OnClickListener clickListener = v -> { - String value = binding.inputEditText.getText().toString(); - if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) { - String error = callback.onValueEdited(value); - if (error != null) { - binding.inputLayout.setError(error); - return; - } - } - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - dialog.dismiss(); - }; + View.OnClickListener clickListener = + v -> { + String value = binding.inputEditText.getText().toString(); + if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) { + String error = callback.onValueEdited(value); + if (error != null) { + binding.inputLayout.setError(error); + return; + } + } + SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + dialog.dismiss(); + }; dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener); - dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> { - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - dialog.dismiss(); - })); + dialog.getButton(DialogInterface.BUTTON_NEGATIVE) + .setOnClickListener( + (v -> { + SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + dialog.dismiss(); + })); dialog.setCanceledOnTouchOutside(false); - dialog.setOnDismissListener(dialog1 -> { - SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); - }); + dialog.setOnDismissListener( + dialog1 -> { + SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText); + }); } protected boolean hasStoragePermission(int requestCode) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode); + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode); return false; } else { return true; @@ -966,7 +1117,8 @@ public abstract class XmppActivity extends ActionBarActivity { } protected boolean manuallyChangePresence() { - return getBooleanPreference(AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); + return getBooleanPreference( + AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence); } protected String getShareableUri() { @@ -996,16 +1148,21 @@ public abstract class XmppActivity extends ActionBarActivity { PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine(); try { startIntentSenderForResult( - pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0, - 0, 0, Compatibility.pgpStartIntentSenderOptions()); + pgp.getIntentForKey(keyId).getIntentSender(), + 0, + null, + 0, + 0, + 0, + Compatibility.pgpStartIntentSenderOptions()); } catch (final Throwable e) { - Log.d(Config.LOGTAG,"could not launch OpenKeyChain", e); + Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e); Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show(); } } @Override - protected void onResume(){ + protected void onResume() { super.onResume(); SettingsUtils.applyScreenshotSetting(this); } @@ -1058,11 +1215,27 @@ public abstract class XmppActivity extends ActionBarActivity { final int black; final int white; if (Activities.isNightMode(this)) { - black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured"); - white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured"); + black = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceContainerHighest, + "No surface color configured"); + white = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceInverse, + "No inverse surface color configured"); } else { - black = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceInverse,"No inverse surface color configured"); - white = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurfaceContainerHighest,"No surface color configured"); + black = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceInverse, + "No inverse surface color configured"); + white = + MaterialColors.getColor( + this, + com.google.android.material.R.attr.colorSurfaceContainerHighest, + "No surface color configured"); } final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white); final ImageView view = new ImageView(this); @@ -1089,7 +1262,10 @@ public abstract class XmppActivity extends ActionBarActivity { public void loadBitmap(Message message, ImageView imageView) { Drawable bm; try { - bm = xmppConnectionService.getFileBackend().getThumbnail(message, getResources(), (int) (metrics.density * 288), true); + bm = + xmppConnectionService + .getFileBackend() + .getThumbnail(message, getResources(), (int) (metrics.density * 288), true); } catch (IOException e) { bm = null; } @@ -1148,7 +1324,8 @@ public abstract class XmppActivity extends ActionBarActivity { return false; } else { jids.add(conversation.getJid().asBareJid()); - return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback); + return service.createAdhocConference( + conversation.getAccount(), null, jids, activity.adhocCallback); } } } 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 b9c4f553f33a6e0fa532385085b2c4d8e73ed108..62c33522037c5406fd3638089c2bb7b6d9162a8d 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -189,15 +189,22 @@ public class ConversationAdapter if (status == Message.STATUS_RECEIVED) { if (conversation.getMode() == Conversation.MODE_MULTI) { viewHolder.binding.senderName.setVisibility(View.VISIBLE); - final String dname = UIHelper.getMessageDisplayName(message); - final String[] words = dname.split("\\s+"); - viewHolder.binding.senderName.setText((words.length > 0 ? words[0] : dname) + ':'); + final var displayName = UIHelper.getMessageDisplayName(message); + final var displayNameParts = displayName.split("\\s+"); + // Skip when nickname only consists of blank chars + if (displayNameParts.length == 0) { + viewHolder.binding.senderName.setText(String.format("%s:", displayName)); + } else { + viewHolder.binding.senderName.setText( + String.format("%s:", displayNameParts[0])); + } } else { viewHolder.binding.senderName.setVisibility(View.GONE); } } else if (message.getType() != Message.TYPE_STATUS) { viewHolder.binding.senderName.setVisibility(View.VISIBLE); - viewHolder.binding.senderName.setText(activity.getString(R.string.me) + ':'); + viewHolder.binding.senderName.setText( + String.format("%s:", activity.getString(R.string.me))); } else { viewHolder.binding.senderName.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 25bc9fa630ce61023612606f52f0a0c32e427277..36299d486175c467c4969810f2268af0c2b32f70 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -35,7 +35,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListAdapter; @@ -43,12 +42,12 @@ 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.constraintlayout.widget.ConstraintLayout; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; @@ -77,6 +76,7 @@ import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Joiner; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -106,6 +106,11 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.databinding.LinkDescriptionBinding; import eu.siacs.conversations.databinding.DialogAddReactionBinding; +import eu.siacs.conversations.databinding.ItemMessageDateBubbleBinding; +import eu.siacs.conversations.databinding.ItemMessageEndBinding; +import eu.siacs.conversations.databinding.ItemMessageRtpSessionBinding; +import eu.siacs.conversations.databinding.ItemMessageStartBinding; +import eu.siacs.conversations.databinding.ItemMessageStatusBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -153,15 +158,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; -import java.util.function.Consumer; 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 END = 0; + private static final int START = 1; private static final int STATUS = 2; private static final int DATE_SEPARATOR = 3; private static final int RTP_SESSION = 4; @@ -175,8 +179,7 @@ public class MessageAdapter extends ArrayAdapter { private OnContactPictureClicked mOnMessageBoxSwipedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private OnInlineImageLongClicked mOnInlineImageLongClickedListener; - private boolean mUseGreenBackground = false; - private BubbleDesign bubbleDesign = new BubbleDesign(false, false); + private BubbleDesign bubbleDesign = new BubbleDesign(false, false, false, true); private final boolean mForceNames; private final Map lastWebxdcUpdate = new HashMap<>(); private String selectionUuid = null; @@ -256,7 +259,7 @@ public class MessageAdapter extends ArrayAdapter { return 5; } - private int getItemViewType(Message message) { + private static int getItemViewType(final Message message, final boolean alignStart) { if (message.getType() == Message.TYPE_STATUS) { if (DATE_SEPARATOR_BODY.equals(message.getBody())) { return DATE_SEPARATOR; @@ -265,32 +268,32 @@ public class MessageAdapter extends ArrayAdapter { } } else if (message.getType() == Message.TYPE_RTP_SESSION) { return RTP_SESSION; - } else if (message.getStatus() <= Message.STATUS_RECEIVED) { - return RECEIVED; + } else if (message.getStatus() <= Message.STATUS_RECEIVED || alignStart) { + return START; } else { - return SENT; + return END; } } @Override - public int getItemViewType(int position) { - return this.getItemViewType(getItem(position)); + public int getItemViewType(final int position) { + return getItemViewType(getItem(position), bubbleDesign.alignStart); } private void displayStatus( - final ViewHolder viewHolder, + final BubbleMessageItemViewHolder viewHolder, final Message message, - final int type, final BubbleColor bubbleColor) { - final int mergedStatus = message.getMergedStatus(); + final int status = message.getStatus(); 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; + && message.getStatus() <= Message.STATUS_RECEIVED; + final boolean sent = status != Message.STATUS_RECEIVED; + final boolean showUserNickname = + message.getConversation().getMode() == Conversation.MODE_MULTI + && viewHolder instanceof StartBubbleMessageItemViewHolder; final String fileSize; if (message.isFileOrImage() || transferable != null @@ -310,106 +313,101 @@ public class MessageAdapter extends ArrayAdapter { fileSize = null; error = message.getStatus() == Message.STATUS_SEND_FAILED; } - if (type == SENT && viewHolder.indicatorReceived != null) { + + if (sent) { final @DrawableRes Integer receivedIndicator = - getMessageStatusAsDrawable(message, mergedStatus); + getMessageStatusAsDrawable(message, status); if (receivedIndicator == null) { - viewHolder.indicatorReceived.setVisibility(View.INVISIBLE); + viewHolder.indicatorReceived().setVisibility(View.INVISIBLE); } else { - viewHolder.indicatorReceived.setImageResource(receivedIndicator); - if (mergedStatus == Message.STATUS_SEND_FAILED) { - setImageTintError(viewHolder.indicatorReceived); + viewHolder.indicatorReceived().setImageResource(receivedIndicator); + if (status == Message.STATUS_SEND_FAILED) { + setImageTintError(viewHolder.indicatorReceived()); } else { - setImageTint(viewHolder.indicatorReceived, bubbleColor); + setImageTint(viewHolder.indicatorReceived(), bubbleColor); } - viewHolder.indicatorReceived.setVisibility(View.VISIBLE); + viewHolder.indicatorReceived().setVisibility(View.VISIBLE); } + } else { + viewHolder.indicatorReceived().setVisibility(View.GONE); } - final var additionalStatusInfo = getAdditionalStatusInfo(message, mergedStatus); - - if (error && type == SENT) { - viewHolder.time.setTextColor( - MaterialColors.getColor( - viewHolder.time, com.google.android.material.R.attr.colorError)); + final var additionalStatusInfo = getAdditionalStatusInfo(message, status); + + if (error && sent) { + viewHolder + .time() + .setTextColor( + MaterialColors.getColor( + viewHolder.time(), + com.google.android.material.R.attr.colorError)); } else { - setTextColor(viewHolder.time, bubbleColor); + setTextColor(viewHolder.time(), bubbleColor); } - setTextColor(viewHolder.subject, bubbleColor); + setTextColor(viewHolder.subject(), bubbleColor); if (message.getEncryption() == Message.ENCRYPTION_NONE) { - viewHolder.indicator.setVisibility(View.GONE); + viewHolder.indicatorSecurity().setVisibility(View.GONE); } else { boolean verified = false; if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { - final FingerprintStatus status = + final FingerprintStatus fingerprintStatus = message.getConversation() .getAccount() .getAxolotlService() .getFingerprintTrust(message.getFingerprint()); - if (status != null && status.isVerified()) { + if (fingerprintStatus != null && fingerprintStatus.isVerified()) { verified = true; } } if (verified) { - viewHolder.indicator.setImageResource(R.drawable.ic_verified_user_24dp); + viewHolder.indicatorSecurity().setImageResource(R.drawable.ic_verified_user_24dp); } else { - viewHolder.indicator.setImageResource(R.drawable.ic_lock_24dp); + viewHolder.indicatorSecurity().setImageResource(R.drawable.ic_lock_24dp); } - if (error && type == SENT) { - setImageTintError(viewHolder.indicator); + if (error && sent) { + setImageTintError(viewHolder.indicatorSecurity()); } else { - setImageTint(viewHolder.indicator, bubbleColor); + setImageTint(viewHolder.indicatorSecurity(), bubbleColor); } - viewHolder.indicator.setVisibility(View.VISIBLE); + viewHolder.indicatorSecurity().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); - } + if (message.edited()) { + viewHolder.indicatorEdit().setVisibility(View.VISIBLE); + if (error && sent) { + setImageTintError(viewHolder.indicatorEdit()); } else { - viewHolder.edit_indicator.setVisibility(View.GONE); + setImageTint(viewHolder.indicatorEdit(), bubbleColor); } + } else { + viewHolder.indicatorEdit().setVisibility(View.GONE); } final String formattedTime = - UIHelper.readableTimeDifferenceFull(getContext(), message.getMergedTimeSent()); + UIHelper.readableTimeDifferenceFull(getContext(), message.getTimeSent()); 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)); + + if (mForceNames || multiReceived || showUserNickname || (message.getTrueCounterpart() != null && message.getContact() != null)) { + final String displayName = UIHelper.getMessageDisplayName(message); + if (displayName != null) { + timeInfoBuilder.add(displayName); } + } + if (fileSize != null) { + timeInfoBuilder.add(fileSize); + } + if (bodyLanguage != null) { + timeInfoBuilder.add(bodyLanguage.toUpperCase(Locale.US)); + } + // 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 { - 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); - } + timeInfoBuilder.add(formattedTime); } final var timeInfo = timeInfoBuilder.build(); - viewHolder.time.setText(Joiner.on(" \u00B7 ").join(timeInfo)); + viewHolder.time().setText(Joiner.on(" · ").join(timeInfo)); } public static @DrawableRes Integer getMessageStatusAsDrawable( @@ -419,8 +417,8 @@ public class MessageAdapter extends ArrayAdapter { 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_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)) { @@ -458,31 +456,38 @@ public class MessageAdapter extends ArrayAdapter { } 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); + BubbleMessageItemViewHolder viewHolder, + CharSequence text, + final BubbleColor bubbleColor) { + viewHolder.downloadButton().setVisibility(View.GONE); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.image().setVisibility(View.GONE); + viewHolder.messageBody().setTypeface(null, Typeface.ITALIC); + 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 Message message, final BubbleColor bubbleColor, int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - 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); + final BubbleMessageItemViewHolder viewHolder, + final Message message, + final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.downloadButton().setVisibility(View.GONE); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.image().setVisibility(View.GONE); + viewHolder.messageBody().setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody().setVisibility(View.VISIBLE); + setTextColor(viewHolder.messageBody(), bubbleColor); final var body = getSpannableBody(message); ImageSpan[] imageSpans = body.getSpans(0, body.length(), ImageSpan.class); float size = imageSpans.length == 1 || Emoticons.isEmoji(body.toString()) ? 5.0f : 2.0f; body.setSpan( new RelativeSizeSpan(size), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(body); + viewHolder.messageBody().setText(body); } private void applyQuoteSpan( @@ -596,210 +601,199 @@ public class MessageAdapter extends ArrayAdapter { private SpannableStringBuilder getSpannableBody(final Message message) { Drawable fallbackImg = ResourcesCompat.getDrawable(activity.getResources(), R.drawable.ic_photo_24dp, null); - return message.getMergedBody(new Thumbnailer(message), fallbackImg); + return message.getSpannableBody(new Thumbnailer(message), fallbackImg); } private void displayTextMessage( - final ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - viewHolder.inReplyToQuote.setVisibility(View.GONE); - 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(); + final BubbleMessageItemViewHolder viewHolder, + final Message message, + final BubbleColor bubbleColor) { + viewHolder.inReplyToQuote().setVisibility(View.GONE); + viewHolder.downloadButton().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); + viewHolder.messageBody().setTypeface(null, Typeface.NORMAL); + + final ViewGroup.LayoutParams layoutParams = viewHolder.messageBody().getLayoutParams(); layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; - viewHolder.messageBody.setLayoutParams(layoutParams); + viewHolder.messageBody().setLayoutParams(layoutParams); - final ViewGroup.LayoutParams qlayoutParams = viewHolder.inReplyToQuote.getLayoutParams(); + final ViewGroup.LayoutParams qlayoutParams = viewHolder.inReplyToQuote().getLayoutParams(); qlayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; - viewHolder.messageBody.setLayoutParams(qlayoutParams); - - 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; - 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); - } - if (processMarkup) StylingHelper.format(body, viewHolder.messageBody.getCurrentTextColor()); - MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid()); - boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true) : false; - 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); - if (start < 0 || end < 0) continue; - - body.removeSpan(quote); - applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true); - if (start == 0) { - if (message.getInReplyTo() == null) { - startsWithQuote = true; - } else { - viewHolder.inReplyToQuote.setText(body.subSequence(start, end)); - viewHolder.inReplyToQuote.setVisibility(View.VISIBLE); - body.delete(start, end); - while (body.length() > start && body.charAt(start) == '\n') body.delete(start, 1); // Newlines after quote - continue; - } + viewHolder.inReplyToQuote().setLayoutParams(qlayoutParams); + + final var rawBody = message.getBody(); + if (Strings.isNullOrEmpty(rawBody)) { + viewHolder.messageBody().setText(""); + viewHolder.messageBody().setTextIsSelectable(false); + toggleWhisperInfo(viewHolder, message, bubbleColor); + return; + } + viewHolder.messageBody().setTextIsSelectable(true); + final String nick = UIHelper.getMessageDisplayName(message); + SpannableStringBuilder body = getSpannableBody(message); + final var processMarkup = body.getSpans(0, body.length(), Message.PlainTextSpan.class).length > 0; + if (body.length() > Config.MAX_DISPLAY_MESSAGE_CHARS) { + body = new SpannableStringBuilder(body, 0, Config.MAX_DISPLAY_MESSAGE_CHARS); + body.append("…"); + } + if (processMarkup) StylingHelper.format(body, viewHolder.messageBody().getCurrentTextColor()); + MyLinkify.addLinks(body, message.getConversation().getAccount(), message.getConversation().getJid()); + boolean startsWithQuote = processMarkup ? handleTextQuotes(viewHolder.messageBody(), body, bubbleColor, true) : false; + 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); + if (start < 0 || end < 0) continue; + + body.removeSpan(quote); + applyQuoteSpan(viewHolder.messageBody(), body, start, end, bubbleColor, true); + if (start == 0) { + if (message.getInReplyTo() == null) { + startsWithQuote = true; + } else { + viewHolder.inReplyToQuote().setText(body.subSequence(start, end)); + viewHolder.inReplyToQuote().setVisibility(View.VISIBLE); + body.delete(start, end); + while (body.length() > start && body.charAt(start) == '\n') body.delete(start, 1); // Newlines after quote + continue; } } - boolean hasMeCommand = body.toString().startsWith(Message.ME_COMMAND); - if (hasMeCommand) { - body = body.replace(0, Message.ME_COMMAND.length(), nick + " "); + } + boolean hasMeCommand = body.toString().startsWith(Message.ME_COMMAND); + if (hasMeCommand) { + body.replace(0, Message.ME_COMMAND.length(), String.format("%s ", nick)); + } + if (!message.isPrivateMessage()) { + if (hasMeCommand && body.length() > nick.length()) { + body.setSpan( + new StyleSpan(Typeface.BOLD_ITALIC), + 0, + nick.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (!message.isPrivateMessage()) { - if (hasMeCommand && body.length() > nick.length()) { - 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 { - 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, " "); - } + 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 ForegroundColorSpan( - bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), - 0, + 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), - 0, - privateMarkerIndex, + new StyleSpan(Typeface.BOLD_ITALIC), + privateMarkerIndex + 1, + privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - if (hasMeCommand) { + } + } + 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_ITALIC), - privateMarkerIndex + 1, - privateMarkerIndex + 1 + nick.length(), + new StyleSpan(Typeface.BOLD), + matcher.start(), + matcher.end(), 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); - } - } - } - for (final var emoji : EmojiManager.extractEmojisInOrderWithIndex(body.toString())) { - var end = emoji.getCharIndex() + emoji.getEmoji().getEmoji().length(); - if (body.length() > end && body.charAt(end) == '\uFE0F') end++; - body.setSpan( - new RelativeSizeSpan(1.2f), - emoji.getCharIndex(), - end, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - // Make custom emoji bigger too, to match emoji - for (final var span : body.getSpans(0, body.length(), com.cheogram.android.InlineImageSpan.class)) { - body.setSpan( - new RelativeSizeSpan(1.2f), - body.getSpanStart(span), - body.getSpanEnd(span), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } + for (final var emoji : EmojiManager.extractEmojisInOrderWithIndex(body.toString())) { + var end = emoji.getCharIndex() + emoji.getEmoji().getEmoji().length(); + if (body.length() > end && body.charAt(end) == '\uFE0F') end++; + body.setSpan( + new RelativeSizeSpan(1.2f), + emoji.getCharIndex(), + end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + // Make custom emoji bigger too, to match emoji + for (final var span : body.getSpans(0, body.length(), com.cheogram.android.InlineImageSpan.class)) { + body.setSpan( + new RelativeSizeSpan(1.2f), + body.getSpanStart(span), + body.getSpanEnd(span), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } - if (highlightedTerm != null) { - StylingHelper.highlight(viewHolder.messageBody, body, highlightedTerm); - } + if (highlightedTerm != null) { + StylingHelper.highlight(viewHolder.messageBody(), body, highlightedTerm); + } - viewHolder.messageBody.setAutoLinkMask(0); - viewHolder.messageBody.setText(body); - if (body.length() <= 0) viewHolder.messageBody.setVisibility(View.GONE); - 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; - } + viewHolder.messageBody().setAutoLinkMask(0); + viewHolder.messageBody().setText(body); + if (body.length() <= 0) viewHolder.messageBody().setVisibility(View.GONE); + 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)); - } + 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); - } + } + }; + 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); } private void displayDownloadableMessage( - ViewHolder viewHolder, + final BubbleMessageItemViewHolder viewHolder, final Message message, String text, - final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.setVisibility(View.GONE); + final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.image().setVisibility(View.GONE); List thumbs = message.getFileParams() != null ? message.getFileParams().getThumbnails() : null; if (thumbs != null && !thumbs.isEmpty()) { for (Element thumb : thumbs) { @@ -832,40 +826,41 @@ public class MessageAdapter extends ArrayAdapter { 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, message.getInReplyTo() != null, true, type, viewHolder); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); + viewHolder.image().setVisibility(View.VISIBLE); + imagePreviewLayout(width, height, viewHolder.image(), message.getInReplyTo() != null, true, 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); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.downloadButton().setVisibility(View.VISIBLE); + viewHolder.downloadButton().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)); + viewHolder.downloadButton().setIconResource(imageResource); + viewHolder + .downloadButton() + .setOnClickListener(v -> ConversationFragment.downloadFile(activity, message)); } - private void displayWebxdcMessage(ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { + private void displayWebxdcMessage(BubbleMessageItemViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { Cid webxdcCid = message.getFileParams().getCids().get(0); WebxdcPage webxdc = new WebxdcPage(activity, webxdcCid, message); - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button.setIconResource(0); - viewHolder.download_button.setText("Open " + webxdc.getName()); - viewHolder.download_button.setOnClickListener(v -> { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.image().setVisibility(View.GONE); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.downloadButton().setVisibility(View.VISIBLE); + viewHolder.downloadButton().setIconResource(0); + viewHolder.downloadButton().setText("Open " + webxdc.getName()); + viewHolder.downloadButton().setOnClickListener(v -> { Conversation conversation = (Conversation) message.getConversation(); if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { conversation.startWebxdc(webxdc); } }); - viewHolder.image.setOnClickListener(v -> { + viewHolder.image().setOnClickListener(v -> { Conversation conversation = (Conversation) message.getConversation(); if (!conversation.switchToSession("webxdc\0" + message.getUuid())) { conversation.startWebxdc(webxdc); @@ -884,8 +879,8 @@ public class MessageAdapter extends ArrayAdapter { }).start(); } else { if (lastUpdate != null && (lastUpdate.getSummary() != null || lastUpdate.getDocument() != null)) { - viewHolder.messageBody.setVisibility(View.VISIBLE); - viewHolder.messageBody.setText( + viewHolder.messageBody().setVisibility(View.VISIBLE); + viewHolder.messageBody().setText( (lastUpdate.getDocument() == null ? "" : lastUpdate.getDocument() + "\n") + (lastUpdate.getSummary() == null ? "" : lastUpdate.getSummary()) ); @@ -903,103 +898,112 @@ public class MessageAdapter extends ArrayAdapter { } }).start(); } else { - viewHolder.image.setVisibility(View.VISIBLE); - viewHolder.image.setImageDrawable(d); - imagePreviewLayout(d.getIntrinsicWidth(), d.getIntrinsicHeight(), viewHolder.image, message.getInReplyTo() != null, true, type, viewHolder); + viewHolder.image().setVisibility(View.VISIBLE); + viewHolder.image().setImageDrawable(d); + imagePreviewLayout(d.getIntrinsicWidth(), d.getIntrinsicHeight(), viewHolder.image(), message.getInReplyTo() != null, true, viewHolder); } } 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 BubbleMessageItemViewHolder viewHolder, + final Message message, + final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.image().setVisibility(View.GONE); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.downloadButton().setVisibility(View.VISIBLE); + viewHolder + .downloadButton() + .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)); + viewHolder.downloadButton().setIconResource(imageResource); + viewHolder.downloadButton().setOnClickListener(v -> openDownloadable(message)); } private void displayURIMessage( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.GONE); - viewHolder.audioPlayer.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); + BubbleMessageItemViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.messageBody().setVisibility(View.GONE); + viewHolder.image().setVisibility(View.GONE); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.downloadButton().setVisibility(View.VISIBLE); final var uri = message.wholeIsKnownURI(); if ("bitcoin".equals(uri.getScheme())) { final var amount = uri.getQueryParameter("amount"); final var formattedAmount = amount == null || amount.equals("") ? "" : amount + " "; - viewHolder.download_button.setIconResource(R.drawable.bitcoin_24dp); - viewHolder.download_button.setText("Send " + formattedAmount + "Bitcoin"); + viewHolder.downloadButton().setIconResource(R.drawable.bitcoin_24dp); + viewHolder.downloadButton().setText("Send " + formattedAmount + "Bitcoin"); } else if ("bitcoincash".equals(uri.getScheme())) { final var amount = uri.getQueryParameter("amount"); final var formattedAmount = amount == null || amount.equals("") ? "" : amount + " "; - viewHolder.download_button.setIconResource(R.drawable.bitcoin_cash_24dp); - viewHolder.download_button.setText("Send " + formattedAmount + "Bitcoin Cash"); + viewHolder.downloadButton().setIconResource(R.drawable.bitcoin_cash_24dp); + viewHolder.downloadButton().setText("Send " + formattedAmount + "Bitcoin Cash"); } else if ("ethereum".equals(uri.getScheme())) { final var amount = uri.getQueryParameter("value"); final var formattedAmount = amount == null || amount.equals("") ? "" : amount + " "; - viewHolder.download_button.setIconResource(R.drawable.eth_24dp); - viewHolder.download_button.setText("Send " + formattedAmount + "via Ethereum"); + viewHolder.downloadButton().setIconResource(R.drawable.eth_24dp); + viewHolder.downloadButton().setText("Send " + formattedAmount + "via Ethereum"); } else if ("monero".equals(uri.getScheme())) { final var amount = uri.getQueryParameter("tx_amount"); final var formattedAmount = amount == null || amount.equals("") ? "" : amount + " "; - viewHolder.download_button.setIconResource(R.drawable.monero_24dp); - viewHolder.download_button.setText("Send " + formattedAmount + "Monero"); + viewHolder.downloadButton().setIconResource(R.drawable.monero_24dp); + viewHolder.downloadButton().setText("Send " + formattedAmount + "Monero"); } else if ("wownero".equals(uri.getScheme())) { final var amount = uri.getQueryParameter("tx_amount"); final var formattedAmount = amount == null || amount.equals("") ? "" : amount + " "; - viewHolder.download_button.setIconResource(R.drawable.wownero_24dp); - viewHolder.download_button.setText("Send " + formattedAmount + "Wownero"); + viewHolder.downloadButton().setIconResource(R.drawable.wownero_24dp); + viewHolder.downloadButton().setText("Send " + formattedAmount + "Wownero"); } - viewHolder.download_button.setOnClickListener(v -> new FixedURLSpan(message.getRawBody()).onClick(v)); + viewHolder.downloadButton().setOnClickListener(v -> new FixedURLSpan(message.getRawBody()).onClick(v)); } private void displayLocationMessage( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor, final int type) { - displayTextMessage(viewHolder, message, bubbleColor, type); - viewHolder.messageBody.setVisibility(View.GONE); - 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 BubbleMessageItemViewHolder viewHolder, + final Message message, + final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.image().setVisibility(View.GONE); + viewHolder.audioPlayer().setVisibility(View.GONE); + viewHolder.downloadButton().setVisibility(View.VISIBLE); + viewHolder.downloadButton().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)); + viewHolder.downloadButton().setIconResource(imageResource); + viewHolder.downloadButton().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; + final BubbleMessageItemViewHolder viewHolder, + Message message, + final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.image().setVisibility(View.GONE); + viewHolder.downloadButton().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 BubbleMessageItemViewHolder viewHolder, + final Message message, + final BubbleColor bubbleColor) { + displayTextMessage(viewHolder, message, bubbleColor); + viewHolder.downloadButton().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, message.getInReplyTo() != null, viewHolder.messageBody.getVisibility() != View.GONE, type, viewHolder); - activity.loadBitmap(message, viewHolder.image); - viewHolder.image.setOnClickListener(v -> openDownloadable(message)); + imagePreviewLayout(params.width, params.height, viewHolder.image(), message.getInReplyTo() != null, viewHolder.messageBody().getVisibility() != View.GONE, viewHolder); + activity.loadBitmap(message, viewHolder.image()); + viewHolder.image().setOnClickListener(v -> openDownloadable(message)); } - private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean otherAbove, boolean otherBelow, int type, ViewHolder viewHolder) { + private void imagePreviewLayout(int w, int h, ShapeableImageView image, boolean otherAbove, boolean otherBelow, BubbleMessageItemViewHolder viewHolder) { final float target = activity.getResources().getDimension(R.dimen.image_preview_width); final int scaledW; final int scaledH; @@ -1016,7 +1020,7 @@ public class MessageAdapter extends ArrayAdapter { scaledW = (int) target; scaledH = (int) (h / ((double) w / target)); } - final var bodyWidth = Math.max(viewHolder.messageBody.getWidth(), viewHolder.download_button.getWidth() + (20 * metrics.density)); + final var bodyWidth = Math.max(viewHolder.messageBody().getWidth(), viewHolder.downloadButton().getWidth() + (20 * metrics.density)); var targetImageWidth = 200 * metrics.density; if (!otherBelow) targetImageWidth = 110 * metrics.density; if (bodyWidth > 0 && bodyWidth < targetImageWidth) targetImageWidth = bodyWidth; @@ -1029,7 +1033,7 @@ public class MessageAdapter extends ArrayAdapter { var shape = new ShapeAppearanceModel.Builder(); if (!otherAbove) { shape = shape.setTopRightCorner(CornerFamily.ROUNDED, bubbleRadius); - if (type == SENT) { + if (viewHolder instanceof EndBubbleMessageItemViewHolder) { shape = shape.setTopLeftCorner(CornerFamily.ROUNDED, bubbleRadius); } } @@ -1043,18 +1047,20 @@ public class MessageAdapter extends ArrayAdapter { image.setShapeAppearanceModel(shape.build()); if (!small) { - final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody.getLayoutParams(); + final ViewGroup.LayoutParams blayoutParams = viewHolder.messageBody().getLayoutParams(); blayoutParams.width = (int) (scaledW - (22 * metrics.density)); - viewHolder.messageBody.setLayoutParams(blayoutParams); + viewHolder.messageBody().setLayoutParams(blayoutParams); - final ViewGroup.LayoutParams qlayoutParams = viewHolder.inReplyToQuote.getLayoutParams(); + final ViewGroup.LayoutParams qlayoutParams = viewHolder.inReplyToQuote().getLayoutParams(); qlayoutParams.width = (int) (scaledW - (22 * metrics.density)); - viewHolder.messageBody.setLayoutParams(qlayoutParams); + viewHolder.messageBody().setLayoutParams(qlayoutParams); } } private void toggleWhisperInfo( - ViewHolder viewHolder, final Message message, final BubbleColor bubbleColor) { + final BubbleMessageItemViewHolder viewHolder, + final Message message, + final BubbleColor bubbleColor) { if (message.isPrivateMessage()) { final String privateMarker; if (message.getStatus() <= Message.STATUS_RECEIVED) { @@ -1069,7 +1075,7 @@ public class MessageAdapter extends ArrayAdapter { final SpannableString body = new SpannableString(privateMarker); body.setSpan( new ForegroundColorSpan( - bubbleToOnSurfaceVariant(viewHolder.messageBody, bubbleColor)), + bubbleToOnSurfaceVariant(viewHolder.messageBody(), bubbleColor)), 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -1078,14 +1084,15 @@ public class MessageAdapter extends ArrayAdapter { 0, privateMarker.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - viewHolder.messageBody.setText(body); - viewHolder.messageBody.setVisibility(View.VISIBLE); + viewHolder.messageBody().setText(body); + viewHolder.messageBody().setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody().setVisibility(View.VISIBLE); } else { - viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.messageBody().setVisibility(View.GONE); } } - private void loadMoreMessages(Conversation conversation) { + private void loadMoreMessages(final Conversation conversation) { conversation.setLastClearHistory(0, null); activity.xmppConnectionService.updateConversation(conversation); conversation.setHasMessagesLeftOnServer(true); @@ -1111,126 +1118,122 @@ public class MessageAdapter extends ArrayAdapter { } } + private MessageItemViewHolder getViewHolder( + final View view, final @NonNull ViewGroup parent, final int type) { + if (view != null && view.getTag() instanceof MessageItemViewHolder messageItemViewHolder) { + return messageItemViewHolder; + } else { + final MessageItemViewHolder viewHolder = + switch (type) { + case RTP_SESSION -> + new RtpSessionMessageItemViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.getContext()), + R.layout.item_message_rtp_session, + parent, + false)); + case DATE_SEPARATOR -> + new DateSeperatorMessageItemViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.getContext()), + R.layout.item_message_date_bubble, + parent, + false)); + case STATUS -> + new StatusMessageItemViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.getContext()), + R.layout.item_message_status, + parent, + false)); + case END -> + new EndBubbleMessageItemViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.getContext()), + R.layout.item_message_end, + parent, + false)); + case START -> + new StartBubbleMessageItemViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(parent.getContext()), + R.layout.item_message_start, + parent, + false)); + default -> throw new AssertionError("Unable to create ViewHolder for type"); + }; + viewHolder.itemView.setTag(viewHolder); + return viewHolder; + } + } + + @NonNull @Override - public View getView(final int position, View view, final @NonNull ViewGroup parent) { + public View getView(final int position, final View view, final @NonNull ViewGroup parent) { final Message message = getItem(position); + final int type = getItemViewType(message, bubbleDesign.alignStart); + final MessageItemViewHolder viewHolder = getViewHolder(view, parent, type); + + if (type == DATE_SEPARATOR + && viewHolder instanceof DateSeperatorMessageItemViewHolder messageItemViewHolder) { + return render(message, messageItemViewHolder); + } + + if (type == RTP_SESSION + && viewHolder instanceof RtpSessionMessageItemViewHolder messageItemViewHolder) { + return render(message, messageItemViewHolder); + } + + if (type == STATUS + && viewHolder instanceof StatusMessageItemViewHolder messageItemViewHolder) { + return render(message, messageItemViewHolder); + } + + if ((type == END || type == START) + && viewHolder instanceof BubbleMessageItemViewHolder messageItemViewHolder) { + return render(position, message, messageItemViewHolder); + } + + throw new AssertionError(); + } + + private View render( + final int position, + final Message message, + final BubbleMessageItemViewHolder viewHolder) { 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.inReplyTo = view.findViewById(R.id.in_reply_to); - viewHolder.inReplyToBox = view.findViewById(R.id.in_reply_to_box); - viewHolder.inReplyToQuote = view.findViewById(R.id.in_reply_to_quote); - viewHolder.indicatorReceived = view.findViewById(R.id.indicator_received); - viewHolder.audioPlayer = view.findViewById(R.id.audio_player); - viewHolder.link_descriptions = view.findViewById(R.id.link_descriptions); - viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); - viewHolder.reactions = view.findViewById(R.id.reactions); - 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.inReplyTo = view.findViewById(R.id.in_reply_to); - viewHolder.inReplyToQuote = view.findViewById(R.id.in_reply_to_quote); - viewHolder.inReplyToBox = view.findViewById(R.id.in_reply_to_box); - 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.link_descriptions = view.findViewById(R.id.link_descriptions); - viewHolder.thread_identicon = view.findViewById(R.id.thread_identicon); - viewHolder.reactions = view.findViewById(R.id.reactions); - 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"); - } - if (viewHolder.link_descriptions != null) { - viewHolder.link_descriptions.setOnItemClickListener((adapter, v, pos, id) -> { - final var desc = (Element) adapter.getItemAtPosition(pos); - var url = desc.findChildContent("url", "https://ogp.me/ns#"); - // should we prefer about? Maybe, it's the real original link, but it's not what we show the user - if (url == null || url.length() < 1) url = desc.getAttribute("{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about"); - if (url == null || url.length() < 1) return; - new FixedURLSpan(url).onClick(v); - }); - } - 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)); + viewHolder.linkDescriptions().setOnItemClickListener((adapter, v, pos, id) -> { + final var desc = (Element) adapter.getItemAtPosition(pos); + var url = desc.findChildContent("url", "https://ogp.me/ns#"); + // should we prefer about? Maybe, it's the real original link, but it's not what we show the user + if (url == null || url.length() < 1) url = desc.getAttribute("{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about"); + if (url == null || url.length() < 1) return; + new FixedURLSpan(url).onClick(v); + }); + + if (viewHolder.messageBody() != null) { + viewHolder.messageBody().setCustomSelectionActionModeCallback(new MessageTextActionModeCallback(this, viewHolder.messageBody())); } - if (viewHolder.time != null) { + if (viewHolder.time() != null) { if (message.isAttention()) { - viewHolder.time.setTypeface(null, Typeface.BOLD); + viewHolder.time().setTypeface(null, Typeface.BOLD); } else { - viewHolder.time.setTypeface(null, Typeface.NORMAL); + viewHolder.time().setTypeface(null, Typeface.NORMAL); } } - final var black = MaterialColors.getColor(view, com.google.android.material.R.attr.colorSecondaryContainer) == view.getContext().getColor(android.R.color.black); + final var black = MaterialColors.getColor(viewHolder.root(), com.google.android.material.R.attr.colorSecondaryContainer) == viewHolder.root().getContext().getColor(android.R.color.black); final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final boolean received = message.getStatus() == Message.STATUS_RECEIVED; final BubbleColor bubbleColor; - if (type == RECEIVED) { + if (received) { if (isInValidSession) { bubbleColor = colorfulBackground || black ? BubbleColor.SECONDARY : BubbleColor.SURFACE; } else { @@ -1244,132 +1247,43 @@ public class MessageAdapter extends ArrayAdapter { } } - if (viewHolder.thread_identicon != null) { - viewHolder.thread_identicon.setVisibility(View.GONE); + if (viewHolder.threadIdenticon() != null) { + viewHolder.threadIdenticon().setVisibility(View.GONE); final Element thread = message.getThread(); if (thread != null) { final String threadId = thread.getContent(); if (threadId != null) { final var roles = MaterialColors.getColorRoles(activity, UIHelper.getColorForName(threadId)); - viewHolder.thread_identicon.setVisibility(View.VISIBLE); - viewHolder.thread_identicon.setColor(roles.getAccent()); - viewHolder.thread_identicon.setHash(UIHelper.identiconHash(threadId)); + viewHolder.threadIdenticon().setVisibility(View.VISIBLE); + viewHolder.threadIdenticon().setColor(roles.getAccent()); + viewHolder.threadIdenticon().setHash(UIHelper.identiconHash(threadId)); } } } - 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; + final var mergeIntoTop = mergeIntoTop(position, message); + final var mergeIntoBottom = mergeIntoBottom(position, message); + final var showAvatar = + bubbleDesign.showAvatars + || (viewHolder instanceof StartBubbleMessageItemViewHolder + && message.getConversation().getMode() == Conversation.MODE_MULTI); + setBubblePadding(viewHolder.root(), mergeIntoTop, mergeIntoBottom); + if (showAvatar) { + final var requiresAvatar = + viewHolder instanceof StartBubbleMessageItemViewHolder + ? !mergeIntoTop + : !mergeIntoBottom; + setRequiresAvatar(viewHolder, requiresAvatar); + AvatarWorkerTask.loadAvatar(message, viewHolder.contactPicture(), R.dimen.avatar); } 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_on_conversation_overview); + viewHolder.contactPicture().setVisibility(View.GONE); } + setAvatarDistance(viewHolder.messageBox(), viewHolder.getClass(), showAvatar); + //viewHolder.messageBox().setClipToOutline(true); remove to show tails - resetClickListener(viewHolder.message_box, viewHolder.messageBody); + resetClickListener(viewHolder.messageBox(), viewHolder.messageBody()); - viewHolder.message_box.setOnClickListener(v -> { + viewHolder.messageBox().setOnClickListener(v -> { if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { MessageAdapter.this.mOnMessageBoxClickedListener .onContactPictureClicked(message); @@ -1380,13 +1294,13 @@ public class MessageAdapter extends ArrayAdapter { MessageAdapter.this.mOnMessageBoxSwipedListener.onContactPictureClicked(message); } }); - viewHolder.message_box.setOnTouchListener(swipeDetector); - viewHolder.image.setOnTouchListener(swipeDetector); - viewHolder.time.setOnTouchListener(swipeDetector); + viewHolder.messageBox().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) -> { + viewHolder.messageBody().setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_UP) { if (MessageAdapter.this.mOnMessageBoxClickedListener != null) { MessageAdapter.this.mOnMessageBoxClickedListener @@ -1398,32 +1312,37 @@ public class MessageAdapter extends ArrayAdapter { return false; }); - viewHolder.messageBody.setOnClickListener(v -> { + 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); + viewHolder.messageBody().setAccessibilityDelegate(null); + + viewHolder + .contactPicture() + .setOnClickListener( + v -> { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); + } + }); + viewHolder + .contactPicture() + .setOnLongClickListener( + v -> { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(v, message); + return true; + } else { + return false; + } + }); boolean footerWrap = false; - final Transferable transferable = message.getTransferable(); final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(message); @@ -1433,9 +1352,9 @@ public class MessageAdapter extends ArrayAdapter { 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); + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.download_x_file, UIHelper.getFileDescriptionString(activity, message)), bubbleColor); } 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); + displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), bubbleColor); } else { displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity.xmppConnectionService, message).first, bubbleColor); } @@ -1443,16 +1362,13 @@ public class MessageAdapter extends ArrayAdapter { && 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; - } + displayMediaPreviewMessage(viewHolder, message, bubbleColor); } else if (message.getFileParams().runtime > 0) { - displayAudioMessage(viewHolder, message, bubbleColor, type); + displayAudioMessage(viewHolder, message, bubbleColor); } else if ("application/webxdc+zip".equals(message.getFileParams().getMediaType()) && message.getConversation() instanceof Conversation && message.getThread() != null && !message.getFileParams().getCids().isEmpty()) { - displayWebxdcMessage(viewHolder, message, bubbleColor, type); + displayWebxdcMessage(viewHolder, message, bubbleColor); } else { - displayOpenableMessage(viewHolder, message, bubbleColor, type); + displayOpenableMessage(viewHolder, message, bubbleColor); } } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { if (account.isPgpDecryptionServiceConnected()) { @@ -1469,8 +1385,8 @@ public class MessageAdapter extends ArrayAdapter { } else { displayInfoMessage( viewHolder, activity.getString(R.string.install_openkeychain), bubbleColor); - viewHolder.message_box.setOnClickListener(this::promptOpenKeychainInstall); - viewHolder.messageBody.setOnClickListener(this::promptOpenKeychainInstall); + viewHolder.messageBox().setOnClickListener(this::promptOpenKeychainInstall); + viewHolder.messageBody().setOnClickListener(this::promptOpenKeychainInstall); } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { displayInfoMessage( @@ -1485,9 +1401,9 @@ public class MessageAdapter extends ArrayAdapter { viewHolder, activity.getString(R.string.omemo_decryption_failed), bubbleColor); } else { if (message.wholeIsKnownURI() != null) { - displayURIMessage(viewHolder, message, bubbleColor, type); + displayURIMessage(viewHolder, message, bubbleColor); } else if (message.isGeoUri()) { - displayLocationMessage(viewHolder, message, bubbleColor, type); + displayLocationMessage(viewHolder, message, bubbleColor); } else if (message.treatAsDownloadable()) { try { final URI uri = message.getOob(); @@ -1497,7 +1413,7 @@ public class MessageAdapter extends ArrayAdapter { R.string.check_x_filesize_on_host, UIHelper.getFileDescriptionString(activity, message), uri.getHost()), - bubbleColor, type); + bubbleColor); } catch (Exception e) { displayDownloadableMessage( viewHolder, @@ -1505,133 +1421,136 @@ public class MessageAdapter extends ArrayAdapter { activity.getString( R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)), - bubbleColor, type); + bubbleColor); } } else if (message.bodyIsOnlyEmojis() && message.getType() != Message.TYPE_PRIVATE) { - displayEmojiMessage(viewHolder, message, bubbleColor, type); + displayEmojiMessage(viewHolder, message, bubbleColor); } else { - displayTextMessage(viewHolder, message, bubbleColor, message.getType()); + displayTextMessage(viewHolder, message, bubbleColor); } } - 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); + if (!black && viewHolder.image().getLayoutParams().width > metrics.density * 110) { + footerWrap = true; + } - setBackgroundTint(viewHolder.message_box, bubbleColor); - setTextColor(viewHolder.messageBody, bubbleColor); - viewHolder.messageBody.setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody, bubbleColor)); + viewHolder.messageBoxInner().setMinimumWidth(footerWrap ? (int) (110 * metrics.density) : 0); + LinearLayout.LayoutParams statusParams = (LinearLayout.LayoutParams) viewHolder.statusLine().getLayoutParams(); + statusParams.width = footerWrap ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; + viewHolder.statusLine().setLayoutParams(statusParams); final Function reactionThumbnailer = (r) -> new Thumbnailer(conversation.getAccount(), r, conversation.canInferPresence()); - if (type == RECEIVED) { + if (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) -> { + viewHolder.commandsList().setAdapter(adapter); + viewHolder.commandsList().setVisibility(View.VISIBLE); + viewHolder.commandsList().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(); + ListAdapter adapter = viewHolder.commandsList().getAdapter(); if (adapter instanceof ArrayAdapter) { ((ArrayAdapter) adapter).clear(); } - viewHolder.commands_list.setVisibility(View.GONE); - viewHolder.commands_list.setOnItemClickListener(null); + viewHolder.commandsList().setVisibility(View.GONE); + viewHolder.commandsList().setOnItemClickListener(null); } + } - setTextColor(viewHolder.encryption, bubbleColor); + setBackgroundTint(viewHolder.messageBox(), bubbleColor); + setTextColor(viewHolder.messageBody(), bubbleColor); + viewHolder.messageBody().setLinkTextColor(bubbleToOnSurfaceColor(viewHolder.messageBody(), bubbleColor)); + if (received && viewHolder instanceof StartBubbleMessageItemViewHolder startViewHolder) { + setTextColor(startViewHolder.encryption(), bubbleColor); if (isInValidSession) { - viewHolder.encryption.setVisibility(View.GONE); + startViewHolder.encryption().setVisibility(View.GONE); } else { - viewHolder.encryption.setVisibility(View.VISIBLE); + startViewHolder.encryption().setVisibility(View.VISIBLE); if (omemoEncryption && !message.isTrusted()) { - viewHolder.encryption.setText(R.string.not_trusted); + startViewHolder.encryption().setText(R.string.not_trusted); } else { - viewHolder.encryption.setText( - CryptoHelper.encryptionTypeToText(message.getEncryption())); + startViewHolder + .encryption() + .setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); } } final var aggregatedReactions = conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message, reactionThumbnailer) : message.getAggregatedReactions(); BindingAdapters.setReactionsOnReceived( - viewHolder.reactions, - conversation instanceof Conversation ? (Conversation) conversation : null, + viewHolder.reactions(), aggregatedReactions, reactions -> sendReactions(message, reactions), + emoji -> showDetailedReaction(message, emoji), emoji -> sendCustomReaction(message, emoji), reaction -> removeCustomReaction(conversation, reaction), () -> addReaction(message)); - } else if (type == SENT) { - final var aggregatedReactions = conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message, reactionThumbnailer) : message.getAggregatedReactions(); - BindingAdapters.setReactionsOnReceived( - viewHolder.reactions, - conversation instanceof Conversation ? (Conversation) conversation : null, - aggregatedReactions, + } else { + if (viewHolder instanceof StartBubbleMessageItemViewHolder startViewHolder) { + startViewHolder.encryption().setVisibility(View.GONE); + } + BindingAdapters.setReactionsOnSent( + viewHolder.reactions(), + message.getAggregatedReactions(), reactions -> sendReactions(message, reactions), - emoji -> sendCustomReaction(message, emoji), - reaction -> removeCustomReaction(conversation, reaction), - () -> addReaction(message)); + emoji -> showDetailedReaction(message, emoji)); } - 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); - } + var 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); + } - if (message.getInReplyTo() == null) { - viewHolder.inReplyToBox.setVisibility(View.GONE); - } else { - viewHolder.inReplyToBox.setVisibility(View.VISIBLE); - viewHolder.inReplyTo.setText(UIHelper.getMessageDisplayName(message.getInReplyTo())); - viewHolder.inReplyTo.setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo())); - viewHolder.inReplyToQuote.setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo())); - setTextColor(viewHolder.inReplyTo, bubbleColor); - } + if (message.getInReplyTo() == null) { + viewHolder.inReplyToBox().setVisibility(View.GONE); + } else { + viewHolder.inReplyToBox().setVisibility(View.VISIBLE); + viewHolder.inReplyTo().setText(UIHelper.getMessageDisplayName(message.getInReplyTo())); + viewHolder.inReplyTo().setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo())); + viewHolder.inReplyToQuote().setOnClickListener((v) -> mConversationFragment.jumpTo(message.getInReplyTo())); + setTextColor(viewHolder.inReplyTo(), bubbleColor); + } - if (appSettings.showLinkPreviews()) { - final var descriptions = message.getLinkDescriptions(); - viewHolder.link_descriptions.setAdapter(new ArrayAdapter<>(activity, 0, descriptions) { - @Override - public View getView(int position, View view, @NonNull ViewGroup parent) { - final LinkDescriptionBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.link_description, parent, false); - binding.title.setText(getItem(position).findChildContent("title", "https://ogp.me/ns#")); - binding.description.setText(getItem(position).findChildContent("description", "https://ogp.me/ns#")); - binding.url.setText(getItem(position).findChildContent("url", "https://ogp.me/ns#")); - final var video = getItem(position).findChildContent("video", "https://ogp.me/ns#"); - if (video != null && video.length() > 0) { - binding.playButton.setVisibility(View.VISIBLE); - binding.playButton.setOnClickListener((v) -> { - new FixedURLSpan(video).onClick(v); - }); - } - return binding.getRoot(); + if (appSettings.showLinkPreviews()) { + final var descriptions = message.getLinkDescriptions(); + viewHolder.linkDescriptions().setAdapter(new ArrayAdapter<>(activity, 0, descriptions) { + @Override + public View getView(int position, View view, @NonNull ViewGroup parent) { + final LinkDescriptionBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.link_description, parent, false); + binding.title.setText(getItem(position).findChildContent("title", "https://ogp.me/ns#")); + binding.description.setText(getItem(position).findChildContent("description", "https://ogp.me/ns#")); + binding.url.setText(getItem(position).findChildContent("url", "https://ogp.me/ns#")); + final var video = getItem(position).findChildContent("video", "https://ogp.me/ns#"); + if (video != null && video.length() > 0) { + binding.playButton.setVisibility(View.VISIBLE); + binding.playButton.setOnClickListener((v) -> { + new FixedURLSpan(video).onClick(v); + }); } - }); - Util.justifyListViewHeightBasedOnChildren(viewHolder.link_descriptions, (int)(metrics.density * 100), true); - } + return binding.getRoot(); + } + }); + Util.justifyListViewHeightBasedOnChildren(viewHolder.linkDescriptions(), (int)(metrics.density * 100), true); } - displayStatus(viewHolder, message, type, bubbleColor); + displayStatus(viewHolder, message, bubbleColor); - viewHolder.messageBody.setAccessibilityDelegate(new View.AccessibilityDelegate() { + 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()) { + if (viewHolder.messageBody().hasSelection()) { selectionUuid = message.getUuid(); } else if (message.getUuid() != null && message.getUuid().equals(selectionUuid)) { selectionUuid = null; @@ -1640,7 +1559,247 @@ public class MessageAdapter extends ArrayAdapter { } }); - return view; + return viewHolder.root(); + } + + private View render( + final Message message, final DateSeperatorMessageItemViewHolder viewHolder) { + final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + if (UIHelper.today(message.getTimeSent())) { + viewHolder.binding.messageBody.setText(R.string.today); + } else if (UIHelper.yesterday(message.getTimeSent())) { + viewHolder.binding.messageBody.setText(R.string.yesterday); + } else { + viewHolder.binding.messageBody.setText( + DateUtils.formatDateTime( + activity, + message.getTimeSent(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR)); + } + if (colorfulBackground) { + setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.PRIMARY); + setTextColor(viewHolder.binding.messageBody, BubbleColor.PRIMARY); + } else { + setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.SURFACE_HIGH); + setTextColor(viewHolder.binding.messageBody, BubbleColor.SURFACE_HIGH); + } + return viewHolder.binding.getRoot(); + } + + private View render(final Message message, final RtpSessionMessageItemViewHolder viewHolder) { + final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles; + final boolean received = message.getStatus() <= Message.STATUS_RECEIVED; + final RtpSessionStatus rtpSessionStatus = RtpSessionStatus.of(message.getBody()); + final long duration = rtpSessionStatus.duration; + if (received) { + if (duration > 0) { + viewHolder.binding.messageBody.setText( + activity.getString( + R.string.incoming_call_duration_timestamp, + TimeFrameUtils.resolve(activity, duration), + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } else if (rtpSessionStatus.successful) { + viewHolder.binding.messageBody.setText(R.string.incoming_call); + } else { + viewHolder.binding.messageBody.setText( + activity.getString( + R.string.missed_call_timestamp, + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } + } else { + if (duration > 0) { + viewHolder.binding.messageBody.setText( + activity.getString( + R.string.outgoing_call_duration_timestamp, + TimeFrameUtils.resolve(activity, duration), + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } else { + viewHolder.binding.messageBody.setText( + activity.getString( + R.string.outgoing_call_timestamp, + UIHelper.readableTimeDifferenceFull( + activity, message.getTimeSent()))); + } + } + if (colorfulBackground) { + setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.SECONDARY); + setTextColor(viewHolder.binding.messageBody, BubbleColor.SECONDARY); + setImageTint(viewHolder.binding.indicatorReceived, BubbleColor.SECONDARY); + } else { + setBackgroundTint(viewHolder.binding.messageBox, BubbleColor.SURFACE_HIGH); + setTextColor(viewHolder.binding.messageBody, BubbleColor.SURFACE_HIGH); + setImageTint(viewHolder.binding.indicatorReceived, BubbleColor.SURFACE_HIGH); + } + viewHolder.binding.indicatorReceived.setImageResource( + RtpSessionStatus.getDrawable(received, rtpSessionStatus.successful)); + return viewHolder.binding.getRoot(); + } + + private View render(final Message message, final StatusMessageItemViewHolder viewHolder) { + final var conversation = message.getConversation(); + if ("LOAD_MORE".equals(message.getBody())) { + viewHolder.binding.statusMessage.setVisibility(View.GONE); + viewHolder.binding.messagePhoto.setVisibility(View.GONE); + viewHolder.binding.loadMoreMessages.setVisibility(View.VISIBLE); + viewHolder.binding.loadMoreMessages.setOnClickListener( + v -> loadMoreMessages((Conversation) message.getConversation())); + } else { + viewHolder.binding.statusMessage.setVisibility(View.VISIBLE); + viewHolder.binding.loadMoreMessages.setVisibility(View.GONE); + viewHolder.binding.statusMessage.setText(message.getBody()); + boolean showAvatar; + if (conversation.getMode() == Conversation.MODE_SINGLE) { + showAvatar = true; + AvatarWorkerTask.loadAvatar( + message, viewHolder.binding.messagePhoto, R.dimen.avatar_on_status_message); + } else if (message.getCounterpart() != null + || message.getTrueCounterpart() != null + || (message.getCounterparts() != null + && !message.getCounterparts().isEmpty())) { + showAvatar = true; + AvatarWorkerTask.loadAvatar( + message, viewHolder.binding.messagePhoto, R.dimen.avatar_on_status_message); + } else { + showAvatar = false; + } + if (showAvatar) { + viewHolder.binding.messagePhoto.setAlpha(0.5f); + viewHolder.binding.messagePhoto.setVisibility(View.VISIBLE); + } else { + viewHolder.binding.messagePhoto.setVisibility(View.GONE); + } + } + return viewHolder.binding.getRoot(); + } + + private void setAvatarDistance( + final LinearLayout messageBox, + final Class clazz, + final boolean showAvatar) { + final ViewGroup.MarginLayoutParams layoutParams = + (ViewGroup.MarginLayoutParams) messageBox.getLayoutParams(); + if (false) { // no need for space since the shape has space inside it for tails + final var resources = messageBox.getResources(); + if (clazz == StartBubbleMessageItemViewHolder.class) { + layoutParams.setMarginStart( + resources.getDimensionPixelSize(R.dimen.bubble_avatar_distance)); + layoutParams.setMarginEnd(0); + } else if (clazz == EndBubbleMessageItemViewHolder.class) { + layoutParams.setMarginStart(0); + layoutParams.setMarginEnd( + resources.getDimensionPixelSize(R.dimen.bubble_avatar_distance)); + } else { + throw new AssertionError("Avatar distances are not available on this view type"); + } + } else { + layoutParams.setMarginStart(0); + layoutParams.setMarginEnd(0); + } + messageBox.setLayoutParams(layoutParams); + } + + private void setBubblePadding( + final ConstraintLayout root, + final boolean mergeIntoTop, + final boolean mergeIntoBottom) { + final var resources = root.getResources(); + final var horizontal = resources.getDimensionPixelSize(R.dimen.bubble_horizontal_padding); + final int top = + resources.getDimensionPixelSize( + mergeIntoTop + ? R.dimen.bubble_vertical_padding_minimum + : R.dimen.bubble_vertical_padding); + final int bottom = + resources.getDimensionPixelSize( + mergeIntoBottom + ? R.dimen.bubble_vertical_padding_minimum + : R.dimen.bubble_vertical_padding); + root.setPadding(horizontal, top, horizontal, bottom); + } + + private void setRequiresAvatar( + final BubbleMessageItemViewHolder viewHolder, final boolean requiresAvatar) { + final var layoutParams = viewHolder.contactPicture().getLayoutParams(); + if (requiresAvatar) { + final var resources = viewHolder.contactPicture().getResources(); + final var avatarSize = resources.getDimensionPixelSize(R.dimen.bubble_avatar_size); + layoutParams.height = avatarSize; + viewHolder.contactPicture().setVisibility(View.VISIBLE); + viewHolder.messageBox().setMinimumHeight(avatarSize); + } else { + layoutParams.height = 0; + viewHolder.contactPicture().setVisibility(View.INVISIBLE); + viewHolder.messageBox().setMinimumHeight(0); + } + viewHolder.contactPicture().setLayoutParams(layoutParams); + } + + private boolean mergeIntoTop(final int position, final Message message) { + if (position < 0) { + return false; + } + final var top = getItem(position - 1); + return merge(top, message); + } + + private boolean mergeIntoBottom(final int position, final Message message) { + final Message bottom; + try { + bottom = getItem(position + 1); + } catch (final IndexOutOfBoundsException e) { + return false; + } + return merge(message, bottom); + } + + private static boolean merge(final Message a, final Message b) { + if (getItemViewType(a, false) != getItemViewType(b, false)) { + return false; + } + final var receivedA = a.getStatus() == Message.STATUS_RECEIVED; + final var receivedB = b.getStatus() == Message.STATUS_RECEIVED; + if (receivedA != receivedB) { + return false; + } + if (a.getConversation().getMode() == Conversation.MODE_MULTI + && a.getStatus() == Message.STATUS_RECEIVED) { + final var occupantIdA = a.getOccupantId(); + final var occupantIdB = b.getOccupantId(); + if (occupantIdA != null && occupantIdB != null) { + if (!occupantIdA.equals(occupantIdB)) { + return false; + } + } + final var counterPartA = a.getCounterpart(); + final var counterPartB = b.getCounterpart(); + if (counterPartA == null || !counterPartA.equals(counterPartB)) { + return false; + } + } + return b.getTimeSent() - a.getTimeSent() <= Config.MESSAGE_MERGE_WINDOW; + } + + private boolean showDetailedReaction(final Message message, Map.Entry> reaction) { + final var c = message.getConversation(); + if (c instanceof Conversation conversation && c.getMode() == Conversational.MODE_MULTI) { + final var reactions = reaction.getValue(); + final var mucOptions = conversation.getMucOptions(); + final var users = mucOptions.findUsers(reactions); + if (users.isEmpty()) { + return true; + } + final MaterialAlertDialogBuilder dialogBuilder = + new MaterialAlertDialogBuilder(activity); + dialogBuilder.setTitle(reaction.getKey().toString()); + dialogBuilder.setMessage(UIHelper.concatNames(users)); + dialogBuilder.create().show(); + return true; + } else { + return false; + } } private void sendReactions(final Message message, final Collection reactions) { @@ -1678,7 +1837,15 @@ public class MessageAdapter extends ArrayAdapter { } private void addReaction(final Message message) { - activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message,reactions)); + activity.addReaction( + message, + reactions -> { + if (activity.xmppConnectionService.sendReactions(message, reactions)) { + return; + } + Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG) + .show(); + }); } private void promptOpenKeychainInstall(View view) { @@ -1737,7 +1904,11 @@ public class MessageAdapter extends ArrayAdapter { public void updatePreferences() { this.bubbleDesign = - new BubbleDesign(appSettings.isColorfulChatBubbles(), appSettings.isLargeFont()); + new BubbleDesign( + appSettings.isColorfulChatBubbles(), + appSettings.isAlignStart(), + appSettings.isLargeFont(), + appSettings.isShowAvatars()); } public void setHighlightedTerm(List terms) { @@ -1756,7 +1927,7 @@ public class MessageAdapter extends ArrayAdapter { boolean onInlineImageLongClicked(Cid cid); } - private static void setBackgroundTint(final View view, final BubbleColor bubbleColor) { + private static void setBackgroundTint(final LinearLayout view, final BubbleColor bubbleColor) { view.setBackgroundTintList(bubbleToColorStateList(view, bubbleColor)); } @@ -1764,12 +1935,15 @@ public class MessageAdapter extends ArrayAdapter { 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 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; @@ -1859,39 +2033,341 @@ public class MessageAdapter extends ArrayAdapter { private static class BubbleDesign { public final boolean colorfulChatBubbles; + public final boolean alignStart; public final boolean largeFont; + public final boolean showAvatars; - private BubbleDesign(final boolean colorfulChatBubbles, final boolean largeFont) { + private BubbleDesign( + final boolean colorfulChatBubbles, + final boolean alignStart, + final boolean largeFont, + final boolean showAvatars) { this.colorfulChatBubbles = colorfulChatBubbles; + this.alignStart = alignStart; this.largeFont = largeFont; + this.showAvatars = showAvatars; + } + } + + private abstract static class MessageItemViewHolder /*extends RecyclerView.ViewHolder*/ { + + private View itemView; + + private MessageItemViewHolder(@NonNull View itemView) { + this.itemView = itemView; + } + } + + private abstract static class BubbleMessageItemViewHolder extends MessageItemViewHolder { + + private BubbleMessageItemViewHolder(@NonNull View itemView) { + super(itemView); + } + + public abstract ConstraintLayout root(); + + protected abstract ImageView indicatorEdit(); + + protected abstract RelativeLayout audioPlayer(); + + protected abstract LinearLayout messageBox(); + + protected abstract MaterialButton downloadButton(); + + protected abstract ShapeableImageView image(); + + protected abstract ImageView indicatorSecurity(); + + protected abstract ImageView indicatorReceived(); + + protected abstract TextView time(); + + protected abstract TextView messageBody(); + + protected abstract ImageView contactPicture(); + + protected abstract ChipGroup reactions(); + + protected abstract ListView commandsList(); + + protected abstract View messageBoxInner(); + + protected abstract View statusLine(); + + protected abstract GithubIdenticonView threadIdenticon(); + + protected abstract ListView linkDescriptions(); + + protected abstract LinearLayout inReplyToBox(); + + protected abstract TextView inReplyTo(); + + protected abstract TextView inReplyToQuote(); + + protected abstract TextView subject(); + } + + private static class StartBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder { + + private final ItemMessageStartBinding binding; + + public StartBubbleMessageItemViewHolder(@NonNull ItemMessageStartBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + @Override + public ConstraintLayout root() { + return (ConstraintLayout) this.binding.getRoot(); + } + + @Override + protected ImageView indicatorEdit() { + return this.binding.editIndicator; + } + + @Override + protected RelativeLayout audioPlayer() { + return this.binding.messageContent.audioPlayer; + } + + @Override + protected LinearLayout messageBox() { + return this.binding.messageBox; + } + + @Override + protected MaterialButton downloadButton() { + return this.binding.messageContent.downloadButton; + } + + @Override + protected ShapeableImageView image() { + return this.binding.messageContent.messageImage; + } + + protected ImageView indicatorSecurity() { + return this.binding.securityIndicator; + } + + @Override + protected ImageView indicatorReceived() { + return this.binding.indicatorReceived; + } + + @Override + protected TextView time() { + return this.binding.messageTime; + } + + @Override + protected TextView messageBody() { + return this.binding.messageContent.messageBody; + } + + protected TextView encryption() { + return this.binding.messageEncryption; + } + + @Override + protected ImageView contactPicture() { + return this.binding.messagePhoto; + } + + @Override + protected ChipGroup reactions() { + return this.binding.reactions; + } + + @Override + protected ListView commandsList() { + return this.binding.messageContent.commandsList; + } + + @Override + protected View messageBoxInner() { + return this.binding.messageBoxInner; + } + + @Override + protected View statusLine() { + return this.binding.statusLine; + } + + @Override + protected GithubIdenticonView threadIdenticon() { + return this.binding.threadIdenticon; + } + + @Override + protected ListView linkDescriptions() { + return this.binding.messageContent.linkDescriptions; + } + + @Override + protected LinearLayout inReplyToBox() { + return this.binding.messageContent.inReplyToBox; + } + + @Override + protected TextView inReplyTo() { + return this.binding.messageContent.inReplyTo; + } + + @Override + protected TextView inReplyToQuote() { + return this.binding.messageContent.inReplyToQuote; + } + + @Override + protected TextView subject() { + return this.binding.messageSubject; } } - 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 inReplyTo; - protected TextView inReplyToQuote; - protected LinearLayout inReplyToBox; - protected TextView messageBody; - protected ImageView contact_picture; - protected TextView status_message; - protected TextView encryption; - protected ListView commands_list; - protected ListView link_descriptions; - protected GithubIdenticonView thread_identicon; - protected ChipGroup reactions; + private static class EndBubbleMessageItemViewHolder extends BubbleMessageItemViewHolder { + + private final ItemMessageEndBinding binding; + + private EndBubbleMessageItemViewHolder(@NonNull ItemMessageEndBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + @Override + public ConstraintLayout root() { + return (ConstraintLayout) this.binding.getRoot(); + } + + @Override + protected ImageView indicatorEdit() { + return this.binding.editIndicator; + } + + @Override + protected RelativeLayout audioPlayer() { + return this.binding.messageContent.audioPlayer; + } + + @Override + protected LinearLayout messageBox() { + return this.binding.messageBox; + } + + @Override + protected MaterialButton downloadButton() { + return this.binding.messageContent.downloadButton; + } + + @Override + protected ShapeableImageView image() { + return this.binding.messageContent.messageImage; + } + + @Override + protected ImageView indicatorSecurity() { + return this.binding.securityIndicator; + } + + @Override + protected ImageView indicatorReceived() { + return this.binding.indicatorReceived; + } + + @Override + protected TextView time() { + return this.binding.messageTime; + } + + @Override + protected TextView messageBody() { + return this.binding.messageContent.messageBody; + } + + @Override + protected ImageView contactPicture() { + return this.binding.messagePhoto; + } + + @Override + protected ChipGroup reactions() { + return this.binding.reactions; + } + + @Override + protected ListView commandsList() { + return this.binding.messageContent.commandsList; + } + + @Override + protected View messageBoxInner() { + return this.binding.messageBoxInner; + } + + @Override + protected View statusLine() { + return this.binding.statusLine; + } + + @Override + protected GithubIdenticonView threadIdenticon() { + return this.binding.threadIdenticon; + } + + @Override + protected ListView linkDescriptions() { + return this.binding.messageContent.linkDescriptions; + } + + @Override + protected LinearLayout inReplyToBox() { + return this.binding.messageContent.inReplyToBox; + } + + @Override + protected TextView inReplyTo() { + return this.binding.messageContent.inReplyTo; + } + + @Override + protected TextView inReplyToQuote() { + return this.binding.messageContent.inReplyToQuote; + } + + @Override + protected TextView subject() { + return this.binding.messageSubject; + } + } + + private static class DateSeperatorMessageItemViewHolder extends MessageItemViewHolder { + + private final ItemMessageDateBubbleBinding binding; + + private DateSeperatorMessageItemViewHolder(@NonNull ItemMessageDateBubbleBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + private static class RtpSessionMessageItemViewHolder extends MessageItemViewHolder { + + private final ItemMessageRtpSessionBinding binding; + + private RtpSessionMessageItemViewHolder(@NonNull ItemMessageRtpSessionBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + private static class StatusMessageItemViewHolder extends MessageItemViewHolder { + + private final ItemMessageStatusBinding binding; + + private StatusMessageItemViewHolder(@NonNull ItemMessageStatusBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } } class Thumbnailer implements GetThumbnailForCid { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/InterfaceBubblesSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/InterfaceBubblesSettingsFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..73ff4df0b7f7793db77fb3e7195005457ca0cdac --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/InterfaceBubblesSettingsFragment.java @@ -0,0 +1,19 @@ +package eu.siacs.conversations.ui.fragment.settings; + +import android.os.Bundle; +import androidx.annotation.Nullable; +import eu.siacs.conversations.R; + +public class InterfaceBubblesSettingsFragment extends XmppPreferenceFragment { + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.preferences_interface_bubbles, rootKey); + } + + @Override + public void onStart() { + super.onStart(); + requireActivity().setTitle(R.string.pref_title_bubbles); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java index b71681b985bacaed8238cdc8db80e6942eb43df2..7fa9fc99a356a1dd78332ba7766a46fd09255f64 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/MainSettingsFragment.java @@ -18,7 +18,8 @@ public class MainSettingsFragment extends PreferenceFragmentCompat { setPreferencesFromResource(R.xml.preferences_main, rootKey); final var about = findPreference("about"); final var connection = findPreference("connection"); - if (about == null || connection == null) { + final var up = findPreference("up"); + if (about == null || connection == null || up == null) { throw new IllegalStateException( "The preference resource file is missing some preferences"); } @@ -43,6 +44,8 @@ public class MainSettingsFragment extends PreferenceFragmentCompat { .commit(); return true; }); + + up.setVisible(!Strings.isNullOrEmpty(getString(R.string.default_push_server))); } @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 4ba9ac8c41a517673d77f60024d248b84a9a472a..6e78636e68fa764ec3e373a06cd3da5cd474f48c 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 @@ -10,7 +10,6 @@ import android.os.Bundle; import android.provider.Settings; import android.util.Log; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -18,10 +17,10 @@ import androidx.preference.Preference; import androidx.preference.ListPreference; import com.google.common.base.Optional; - import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.services.CallIntegration; import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.ui.activity.result.PickRingtone; import eu.siacs.conversations.utils.Compatibility; @@ -68,13 +67,15 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment { final var notificationLed = findPreference(AppSettings.NOTIFICATION_LED); final var chatRequests = (ListPreference) findPreference("chat_requests"); final var foregroundService = findPreference(AppSettings.KEEP_FOREGROUND_SERVICE); + final var callIntegration = findPreference(AppSettings.CALL_INTEGRATION); if (messageNotificationSettings == null || fullscreenNotification == null || notificationRingtone == null || notificationHeadsUp == null || notificationVibrate == null || notificationLed == null - || foregroundService == null) { + || foregroundService == null + || callIntegration == null) { throw new IllegalStateException("The preference resource file is missing preferences"); } if (Compatibility.runsTwentySix()) { @@ -98,6 +99,8 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment { if (!sharedPreferences.getBoolean("notifications_from_strangers", true) && sharedPreferences.getString("chat_requests", null) == null) { chatRequests.setValue("strangers"); } + + callIntegration.setVisible(CallIntegration.selfManagedAvailable(requireContext())); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 9f347cef6ec82ea8f51605e9b9158a17f9b67775..8c13a2e7e2b939d6fff30fdcd9ea9ed8e3135bad 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -53,114 +53,118 @@ import eu.siacs.conversations.xmpp.Jid; public class ShareUtil { - public static void share(XmppActivity activity, Message message) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - if (message.isGeoUri()) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); - shareIntent.setType("text/plain"); - } else if (!message.isFileOrImage()) { - shareIntent.putExtra(Intent.EXTRA_TEXT, message.getMergedBody().toString()); - shareIntent.setType("text/plain"); - shareIntent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, message.getStatus() == Message.STATUS_RECEIVED); - } else { - final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); - final var fp = message.getFileParams(); - final var name = fp == null ? null : fp.getName(); - final var displayName = name == null ? file.getName() : name; - try { - shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file, displayName)); - } catch (SecurityException e) { - Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); - return; - } - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - String mime = message.getMimeType(); - if (mime == null) { - mime = "*/*"; - } - shareIntent.setType(mime); - } - try { - activity.startActivity(Intent.createChooser(shareIntent, activity.getText(R.string.share_with))); - } catch (ActivityNotFoundException e) { - //This should happen only on faulty androids because normally chooser is always available - Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } - } - - public static void copyToClipboard(XmppActivity activity, Message message) { - if (activity.copyTextToClipboard(message.getQuoteableBody(), R.string.message)) { - Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } + public static void share(XmppActivity activity, Message message) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + if (message.isGeoUri()) { + shareIntent.putExtra(Intent.EXTRA_TEXT, message.getRawBody()); + shareIntent.setType("text/plain"); + } else if (!message.isFileOrImage()) { + shareIntent.putExtra(Intent.EXTRA_TEXT, message.getQuoteableBody()); + shareIntent.setType("text/plain"); + shareIntent.putExtra( + ConversationsActivity.EXTRA_AS_QUOTE, + message.getStatus() == Message.STATUS_RECEIVED); + } else { + final DownloadableFile file = + activity.xmppConnectionService.getFileBackend().getFile(message); + final var fp = message.getFileParams(); + final var name = fp == null ? null : fp.getName(); + final var displayName = name == null ? file.getName() : name; + try { + shareIntent.putExtra( + Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file, displayName)); + } catch (SecurityException e) { + Toast.makeText( + activity, + activity.getString( + R.string.no_permission_to_access_x, file.getAbsolutePath()), + Toast.LENGTH_SHORT) + .show(); + return; + } + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + String mime = message.getMimeType(); + if (mime == null) { + mime = "*/*"; + } + shareIntent.setType(mime); + } + try { + activity.startActivity( + Intent.createChooser(shareIntent, activity.getText(R.string.share_with))); + } catch (ActivityNotFoundException e) { + // This should happen only on faulty androids because normally chooser is always + // available + Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT) + .show(); + } + } - public static void copyUrlToClipboard(XmppActivity activity, Message message) { - final String url; - final int resId; - if (message.isGeoUri()) { - resId = R.string.location; - url = message.getRawBody(); - } else if (message.hasFileOnRemoteHost()) { - resId = R.string.file_url; - url = message.getFileParams().url; - } else { - final Message.FileParams fileParams = message.getFileParams(); - url = (fileParams != null && fileParams.url != null) ? fileParams.url : message.getBody().trim(); - resId = R.string.file_url; - } - if (activity.copyTextToClipboard(url, resId)) { - Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } + public static void copyToClipboard(XmppActivity activity, Message message) { + if (activity.copyTextToClipboard(message.getQuoteableBody(), R.string.message)) { + Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT) + .show(); + } + } - public static void copyLinkToClipboard(final Context context, final String url) { - final Uri uri = Uri.parse(url); - if ("xmpp".equals(uri.getScheme())) { - try { - final Jid jid = new XmppUri(uri).getJid(); - if (copyTextToClipboard(context, jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { - Toast.makeText(context, R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } catch (final Exception e) { } - } else { - if (copyTextToClipboard(context, url, R.string.web_address)) { - Toast.makeText(context, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } - } - } + public static void copyUrlToClipboard(XmppActivity activity, Message message) { + final String url; + final int resId; + if (message.isGeoUri()) { + resId = R.string.location; + url = message.getRawBody(); + } else if (message.hasFileOnRemoteHost()) { + resId = R.string.file_url; + url = message.getFileParams().url; + } else { + final Message.FileParams fileParams = message.getFileParams(); + url = + (fileParams != null && fileParams.url != null) + ? fileParams.url + : message.getRawBody().trim(); + resId = R.string.file_url; + } + if (activity.copyTextToClipboard(url, resId)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + } - public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final SpannableStringBuilder body = message.getMergedBody(); - MyLinkify.addLinks(body, true); - for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { - copyLinkToClipboard(activity, urlspan.getURL()); - return; - } - } + public static void copyLinkToClipboard(final Context context, final String url) { + final Uri uri = Uri.parse(url); + if ("xmpp".equals(uri.getScheme())) { + try { + final Jid jid = new XmppUri(uri).getJid(); + if (copyTextToClipboard(context, jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { + Toast.makeText(context, R.string.jabber_id_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + } catch (final Exception e) { } + } else { + if (copyTextToClipboard(context, url, R.string.web_address)) { + Toast.makeText(context, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); + } + } + } - public static boolean containsXmppUri(String body) { - Matcher xmppPatternMatcher = Patterns.XMPP_PATTERN.matcher(body); - if (xmppPatternMatcher.find()) { - try { - return new XmppUri(body.substring(xmppPatternMatcher.start(), xmppPatternMatcher.end())).isValidJid(); - } catch (Exception e) { - return false; - } - } - return false; - } + public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { + final SpannableStringBuilder body = message.getSpannableBody(); + MyLinkify.addLinks(body, true); + for (final URLSpan urlspan : body.getSpans(0, body.length() - 1, URLSpan.class)) { + copyLinkToClipboard(activity, urlspan.getURL()); + return; + } + } - public static boolean copyTextToClipboard(Context context, String text, int labelResId) { - ClipboardManager mClipBoardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - String label = context.getResources().getString(labelResId); - if (mClipBoardManager != null) { - ClipData mClipData = ClipData.newPlainText(label, text); - mClipBoardManager.setPrimaryClip(mClipData); - return true; - } - return false; - } + public static boolean copyTextToClipboard(Context context, String text, int labelResId) { + ClipboardManager mClipBoardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + String label = context.getResources().getString(labelResId); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, text); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } public static String getLinkScheme(final SpannableStringBuilder body) { MyLinkify.addLinks(body, false); diff --git a/src/main/java/eu/siacs/conversations/utils/Emoticons.java b/src/main/java/eu/siacs/conversations/utils/Emoticons.java index 84ff002ea74d50d22ce099c17e1d18ec84a19a94..5a055903c7d0a788479fb0b105a48b8f3d6437fc 100644 --- a/src/main/java/eu/siacs/conversations/utils/Emoticons.java +++ b/src/main/java/eu/siacs/conversations/utils/Emoticons.java @@ -1,8 +1,70 @@ package eu.siacs.conversations.utils; import net.fellbaum.jemoji.EmojiManager; +import com.google.common.collect.ImmutableSet; +import java.util.Set; public class Emoticons { + + private static final int VARIATION_16 = 0xFE0F; + private static final int VARIATION_15 = 0xFE0E; + private static final String VARIATION_16_STRING = new String(new char[] {VARIATION_16}); + private static final String VARIATION_15_STRING = new String(new char[] {VARIATION_15}); + + private static final Set TEXT_DEFAULT_TO_VS16 = + ImmutableSet.of( + "❤", + "✔", + "✖", + "➕", + "➖", + "➗", + "⭐", + "⚡", + "\uD83C\uDF96", + "\uD83C\uDFC6", + "\uD83E\uDD47", + "\uD83E\uDD48", + "\uD83E\uDD49", + "\uD83D\uDC51", + "⚓", + "⛵", + "✈", + "⚖", + "⛑", + "⚒", + "⛏", + "☎", + "⛄", + "⛅", + "⚠", + "⚛", + "✡", + "☮", + "☯", + "☀", + "⬅", + "➡", + "⬆", + "⬇"); + + public static String normalizeToVS16(final String input) { + return TEXT_DEFAULT_TO_VS16.contains(input) && !input.endsWith(VARIATION_15_STRING) + ? input + VARIATION_16_STRING + : input; + } + + public static String existingVariant(final String original, final Set existing) { + if (existing.contains(original) || original.endsWith(VARIATION_15_STRING)) { + return original; + } + final var variant = + original.endsWith(VARIATION_16_STRING) + ? original.substring(0, original.length() - 1) + : original + VARIATION_16_STRING; + return existing.contains(variant) ? variant : original; + } + public static boolean isEmoji(String input) { return EmojiManager.isEmoji(input); } diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java index 948f7537ad8f6af1dc90d71691b21b964548f938..8b07c98f66eadfa8e259e6458303d4314c177ad5 100644 --- a/src/main/java/eu/siacs/conversations/utils/IP.java +++ b/src/main/java/eu/siacs/conversations/utils/IP.java @@ -1,20 +1,30 @@ package eu.siacs.conversations.utils; import com.google.common.net.InetAddresses; - +import java.net.InetAddress; import java.util.regex.Pattern; public class IP { - private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); - - public static boolean matches(String server) { - return server != null && ( - PATTERN_IPV4.matcher(server).matches() + private static final Pattern PATTERN_IPV4 = + Pattern.compile( + "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" + + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = + Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + + public static boolean matches(final String server) { + return server != null + && (PATTERN_IPV4.matcher(server).matches() || PATTERN_IPV6.matcher(server).matches() || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() @@ -22,8 +32,14 @@ public class IP { } public static String wrapIPv6(final String host) { - if (matches(host)) { - return String.format("[%s]", host); + if (InetAddresses.isInetAddress(host)) { + final InetAddress inetAddress; + try { + inetAddress = InetAddresses.forString(host); + } catch (final IllegalArgumentException e) { + return host; + } + return InetAddresses.toUriString(inetAddress); } else { return host; } @@ -31,12 +47,11 @@ public class IP { public static String unwrapIPv6(final String host) { if (host.length() > 2 && host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') { - final String ip = host.substring(1,host.length() -1); + final String ip = host.substring(1, host.length() - 1); if (InetAddresses.isInetAddress(ip)) { return ip; } } return host; } - } diff --git a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java index 803615d23372db7a73d3fd509a2702d397c1437a..ab0c49142fa704c42b5743acefde9e6f425f0574 100644 --- a/src/main/java/eu/siacs/conversations/utils/MessageUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MessageUtils.java @@ -30,16 +30,14 @@ package eu.siacs.conversations.utils; import com.google.common.base.Strings; - -import java.net.URI; -import java.net.URISyntaxException; -import java.util.regex.Pattern; - import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.http.AesGcmURL; import eu.siacs.conversations.http.URL; import eu.siacs.conversations.ui.util.QuoteHelper; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.regex.Pattern; public class MessageUtils { @@ -47,7 +45,7 @@ public class MessageUtils { public static final String EMPTY_STRING = ""; - public static String prepareQuote(Message message) { + public static String prepareQuote(final Message message) { final StringBuilder builder = new StringBuilder(); final String body; if (message.hasMeCommand()) { @@ -102,8 +100,12 @@ public class MessageUtils { final String protocol = uri.getScheme(); final boolean encrypted = ref != null && AesGcmURL.IV_KEY.matcher(ref).matches(); final boolean followedByDataUri = lines.length == 2 && lines[1].startsWith("data:"); - final boolean validAesGcm = AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) && encrypted && (lines.length == 1 || followedByDataUri); - final boolean validProtocol = "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol); + final boolean validAesGcm = + AesGcmURL.PROTOCOL_NAME.equalsIgnoreCase(protocol) + && encrypted + && (lines.length == 1 || followedByDataUri); + final boolean validProtocol = + "http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol); final boolean validOob = validProtocol && (oob || encrypted || (legacyEncryption && uri.getPath() != null && (uri.getPath().endsWith(".xdc") || uri.getPath().endsWith(".webp") || uri.getPath().endsWith(".gif") || uri.getPath().endsWith(".png")))) && lines.length == 1; return validAesGcm || validOob; } @@ -140,7 +142,10 @@ public class MessageUtils { } public static boolean unInitiatedButKnownSize(Message message) { - return message.getType() == Message.TYPE_TEXT && message.getTransferable() == null && message.isOOb() && message.getFileParams().url != null && - (message.getFileParams().size != null || (message.getOob() != null && message.getOob().getScheme() != null && message.getOob().getScheme().equalsIgnoreCase("cid"))); + return message.getType() == Message.TYPE_TEXT + && message.getTransferable() == null + && message.isOOb() + && (message.getFileParams().size != null || (message.getOob() != null && message.getOob().getScheme() != null && message.getOob().getScheme().equalsIgnoreCase("cid"))) + && message.getFileParams().url != null; } } diff --git a/src/main/java/eu/siacs/conversations/utils/Resolver.java b/src/main/java/eu/siacs/conversations/utils/Resolver.java index 31266218e485857c0a56c2145a0f0e294a269622..91df1954ad714e44a4c374591b329e8d21420a5d 100644 --- a/src/main/java/eu/siacs/conversations/utils/Resolver.java +++ b/src/main/java/eu/siacs/conversations/utils/Resolver.java @@ -3,13 +3,12 @@ package eu.siacs.conversations.utils; import android.content.ContentValues; import android.database.Cursor; 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.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -20,21 +19,6 @@ 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.net.Inet4Address; @@ -52,6 +36,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import eu.siacs.conversations.Config; +import eu.siacs.conversations.Conversations; import eu.siacs.conversations.R; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; @@ -62,6 +47,7 @@ import org.minidns.DnsClient; import org.minidns.cache.LruCache; import org.minidns.dnsmessage.Question; import org.minidns.dnsname.DnsName; +import org.minidns.dnsname.InvalidDnsNameException; import org.minidns.dnssec.DnssecResultNotAuthenticException; import org.minidns.dnssec.DnssecValidationFailedException; import org.minidns.dnsserverlookup.AndroidUsingExec; @@ -96,7 +82,7 @@ public class Resolver { return left.ip != null ? -1 : 1; } } else { - return left.directTls ? -1 : 1; + return left.directTls ? 1 : -1; } } else { return left.priority - right.priority; @@ -105,7 +91,8 @@ public class Resolver { private static final ExecutorService DNS_QUERY_EXECUTOR = Executors.newFixedThreadPool(12); - public static final int DEFAULT_PORT_XMPP = 5222; + public static final int XMPP_PORT_STARTTLS = 5222; + private static final int XMPP_PORT_DIRECT_TLS = 5223; private static final String DIRECT_TLS_SERVICE = "_xmpps-client"; private static final String STARTTLS_SERVICE = "_xmpp-client"; @@ -284,7 +271,7 @@ public class Resolver { } public static boolean useDirectTls(final int port) { - return port == 443 || port == 5223; + return port == 443 || port == XMPP_PORT_DIRECT_TLS; } public static List resolve(final String domain) { @@ -331,14 +318,11 @@ public class Resolver { if (IP.matches(domain)) { final InetAddress inetAddress; try { - inetAddress = InetAddress.getByName(domain); - } catch (final UnknownHostException e) { + inetAddress = InetAddresses.forString(domain); + } catch (final IllegalArgumentException e) { return Collections.emptyList(); } - final Result result = new Result(); - result.ip = inetAddress; - result.port = DEFAULT_PORT_XMPP; - return Collections.singletonList(result); + return Result.createWithDefaultPorts(null, inetAddress); } else { return Collections.emptyList(); } @@ -477,7 +461,7 @@ public class Resolver { noSrvFallbacks, results -> { if (results.isEmpty()) { - return Collections.singletonList(Result.createDefault(dnsName)); + return Result.createDefaults(dnsName); } else { return results; } @@ -529,7 +513,7 @@ public class Resolver { public static final String AUTHENTICATED = "authenticated"; private InetAddress ip; private DnsName hostname; - private int port = DEFAULT_PORT_XMPP; + private int port = XMPP_PORT_STARTTLS; private boolean directTls = false; private boolean authenticated = false; private int priority; @@ -543,17 +527,40 @@ public class Resolver { return result; } - static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) { + static List createWithDefaultPorts(final DnsName hostname, final InetAddress ip) { + return Lists.transform( + Arrays.asList(XMPP_PORT_STARTTLS), + p -> createDefault(hostname, ip, p, false)); + } + + static Result createDefault(final DnsName hostname, final InetAddress ip, final int port, final boolean authenticated) { Result result = new Result(); - result.port = DEFAULT_PORT_XMPP; + result.port = port; result.hostname = hostname; result.ip = ip; result.authenticated = authenticated; return result; } + static Result createDefault(final DnsName hostname, final InetAddress ip, final boolean authenticated) { + return createDefault(hostname, ip, XMPP_PORT_STARTTLS, authenticated); + } + static Result createDefault(final DnsName hostname) { - return createDefault(hostname, null, false); + return createDefault(hostname, null, XMPP_PORT_STARTTLS, false); + } + + static List createDefaults( + final DnsName hostname, final Collection inetAddresses) { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final InetAddress inetAddress : inetAddresses) { + builder.addAll(createWithDefaultPorts(hostname, inetAddress)); + } + return builder.build(); + } + + static List createDefaults(final DnsName hostname) { + return createWithDefaultPorts(hostname, null); } public static Result fromCursor(final Cursor cursor) { @@ -624,6 +631,10 @@ public class Resolver { .toString(); } + public String asDestination() { + return ip != null ? InetAddresses.toAddrString(ip) : hostname.toString(); + } + public ContentValues toContentValues() { final ContentValues contentValues = new ContentValues(); contentValues.put(IP, ip == null ? null : ip.getAddress()); diff --git a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java index 2b9d42d7ad1c53215604d752ef24953555344555..81ad58e98be22f36329565c74abfc66fba37051b 100644 --- a/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java +++ b/src/main/java/eu/siacs/conversations/utils/SocksSocketFactory.java @@ -1,37 +1,56 @@ package eu.siacs.conversations.utils; import com.google.common.io.ByteStreams; - +import com.google.common.net.InetAddresses; +import eu.siacs.conversations.Config; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; -import eu.siacs.conversations.Config; - public class SocksSocketFactory { - private static final byte[] LOCALHOST = new byte[]{127, 0, 0, 1}; + private static final byte[] LOCALHOST = new byte[] {127, 0, 0, 1}; - public static void createSocksConnection(final Socket socket, final String destination, final int port) throws IOException { - //TODO use different Socks Addr Type if destination is IP or IPv6 + public static void createSocksConnection( + final Socket socket, final String destination, final int port) throws IOException { final InputStream proxyIs = socket.getInputStream(); final OutputStream proxyOs = socket.getOutputStream(); - proxyOs.write(new byte[]{0x05, 0x01, 0x00}); + proxyOs.write(new byte[] {0x05, 0x01, 0x00}); proxyOs.flush(); final byte[] handshake = new byte[2]; ByteStreams.readFully(proxyIs, handshake); if (handshake[0] != 0x05 || handshake[1] != 0x00) { throw new SocksConnectionException("Socks 5 handshake failed"); } - final byte[] dest = destination.getBytes(); - final ByteBuffer request = ByteBuffer.allocate(7 + dest.length); - request.put(new byte[]{0x05, 0x01, 0x00, 0x03}); - request.put((byte) dest.length); - request.put(dest); + final byte type; + final ByteBuffer request; + if (InetAddresses.isInetAddress(destination)) { + final var ip = InetAddresses.forString(destination); + final var dest = ip.getAddress(); + request = ByteBuffer.allocate(6 + dest.length); + if (ip instanceof Inet4Address) { + type = 0x01; + } else if (ip instanceof Inet6Address) { + type = 0x04; + } else { + throw new IOException("IP address is of unknown subtype"); + } + request.put(new byte[] {0x05, 0x01, 0x00, type}); + request.put(dest); + } else { + final byte[] dest = destination.getBytes(); + type = 0x03; + request = ByteBuffer.allocate(7 + dest.length); + request.put(new byte[] {0x05, 0x01, 0x00, type}); + request.put((byte) dest.length); + request.put(dest); + } request.putShort((short) port); proxyOs.write(request.array()); proxyOs.flush(); @@ -42,13 +61,16 @@ public class SocksSocketFactory { throw new IOException(String.format("Unknown Socks version %02X ", ver)); } final byte status = response[1]; - final byte bndAddrType = response[3]; - final byte[] bndDestination = readDestination(bndAddrType, proxyIs); + final byte bndAddressType = response[3]; + final byte[] bndDestination = readDestination(bndAddressType, proxyIs); final byte[] bndPort = new byte[2]; - if (bndAddrType == 0x03) { + if (bndAddressType == 0x03) { final String receivedDestination = new String(bndDestination); if (!receivedDestination.equalsIgnoreCase(destination)) { - throw new IOException(String.format("Destination mismatch. Received %s Expected %s", receivedDestination, destination)); + throw new IOException( + String.format( + "Destination mismatch. Received %s Expected %s", + receivedDestination, destination)); } } ByteStreams.readFully(proxyIs, bndPort); @@ -63,7 +85,8 @@ public class SocksSocketFactory { } } - private static byte[] readDestination(final byte type, final InputStream inputStream) throws IOException { + private static byte[] readDestination(final byte type, final InputStream inputStream) + throws IOException { final byte[] bndDestination; if (type == 0x01) { bndDestination = new byte[4]; @@ -88,7 +111,8 @@ public class SocksSocketFactory { return false; } - private static Socket createSocket(InetSocketAddress address, String destination, int port) throws IOException { + private static Socket createSocket(InetSocketAddress address, String destination, int port) + throws IOException { Socket socket = new Socket(); try { socket.connect(address, Config.CONNECT_TIMEOUT * 1000); @@ -100,7 +124,10 @@ public class SocksSocketFactory { } public static Socket createSocketOverTor(String destination, int port) throws IOException { - return createSocket(new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), destination, port); + return createSocket( + new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050), + destination, + port); } private static class SocksConnectionException extends IOException { @@ -109,9 +136,7 @@ public class SocksSocketFactory { } } - public static class SocksProxyNotFoundException extends IOException { - - } + public static class SocksProxyNotFoundException extends IOException {} public static class HostNotFoundException extends SocksConnectionException { HostNotFoundException(String message) { diff --git a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java index 1f6c1b0d9d9e27b0590afff25b7c645c0aa97b12..be523aa1310f587b4a0527b796bfe2b247589b16 100644 --- a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java @@ -29,7 +29,6 @@ package eu.siacs.conversations.utils; -import android.content.Context; import android.graphics.Color; import android.graphics.Typeface; import android.preference.PreferenceManager; @@ -47,12 +46,9 @@ import android.text.style.StyleSpan; import android.text.style.TypefaceSpan; import android.widget.EditText; import android.widget.TextView; - import androidx.annotation.ColorInt; -import androidx.core.content.ContextCompat; - import com.google.android.material.color.MaterialColors; - +import eu.siacs.conversations.ui.text.QuoteSpan; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -64,150 +60,162 @@ import eu.siacs.conversations.ui.text.QuoteSpan; public class StylingHelper { - public static final int XHTML_IGNORE = 1; - public static final int XHTML_REMOVE = 2; - public static final int XHTML_EMPHASIS = 3; - public static final int XHTML_CODE = 4; - public static final int NOLINKIFY = 0xf0; - - private static final List> SPAN_CLASSES = Arrays.asList( - StyleSpan.class, - StrikethroughSpan.class, - TypefaceSpan.class, - ForegroundColorSpan.class, - RelativeSizeSpan.class - ); - - public static void clear(final Editable editable) { - final int end = editable.length() - 1; - for (Class clazz : SPAN_CLASSES) { - for (ParcelableSpan span : editable.getSpans(0, end, clazz)) { - editable.removeSpan(span); - } - } - } - - public static void format(final Editable editable, int start, int end, @ColorInt int textColor, final boolean composing) { - for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) { - final int keywordLength = style.getKeyword().length(); - int keywordLengthStart = keywordLength; - if ("```".equals(style.getKeyword())) { - int i; - for (i = style.getStart(); i < editable.length(); i++) { - if (editable.charAt(i) == '\n') break; - } - keywordLengthStart = i - style.getStart() + 1; - } - - editable.setSpan( - createSpanForStyle(style), - style.getStart() + keywordLengthStart, - style.getEnd() - keywordLength + 1, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | - ("*".equals(style.getKeyword()) || "_".equals(style.getKeyword()) ? XHTML_EMPHASIS << Spanned.SPAN_USER_SHIFT : 0) | - ("```".equals(style.getKeyword()) && keywordLengthStart > 4 ? XHTML_CODE << Spanned.SPAN_USER_SHIFT : 0) | - ("`".equals(style.getKeyword()) || "```".equals(style.getKeyword()) ? NOLINKIFY << Spanned.SPAN_USER_SHIFT : 0) - ); - makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLengthStart, textColor, composing); - makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor, composing); - } - } - - public static void format(final Editable editable, @ColorInt int textColor) { - format(editable, textColor, false); - } - - public static void format(final Editable editable, @ColorInt int textColor, final boolean composing) { - int end = 0; - Message.MergeSeparator[] spans = editable.getSpans(0, editable.length() - 1, Message.MergeSeparator.class); - for (Message.MergeSeparator span : spans) { - format(editable, end, editable.getSpanStart(span), textColor, composing); - end = editable.getSpanEnd(span); - } - format(editable, end, editable.length() - 1, textColor, composing); - } - - public static void highlight(final TextView view, final Editable editable, final List needles) { - for (final String needle : needles) { - if (!FtsUtils.isKeyword(needle)) { - highlight(view, editable, needle); - } - } - } - - public static List filterHighlightedWords(List terms) { - List words = new ArrayList<>(); - for (String term : terms) { - if (!FtsUtils.isKeyword(term)) { - StringBuilder builder = new StringBuilder(); - for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) { - codepoint = term.codePointAt(i); - if (Character.isLetterOrDigit(codepoint)) { - builder.append(Character.toChars(codepoint)); - } else if (builder.length() > 0) { - words.add(builder.toString()); - builder.delete(0, builder.length()); - } - } - if (builder.length() > 0) { - words.add(builder.toString()); - } - } - } - return words; - } - - private static void highlight(final TextView view, final Editable editable, final String needle) { - final int length = needle.length(); - String string = editable.toString(); - int start = indexOfIgnoreCase(string, needle, 0); - while (start != -1) { - int end = start + length; - editable.setSpan(new BackgroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimaryFixedDim)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - editable.setSpan(new ForegroundColorSpan(MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryFixed)), start, end, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); - start = indexOfIgnoreCase(string, needle, start + length); - } - - } - - static CharSequence subSequence(CharSequence charSequence, int start, int end) { - if (start == 0 && charSequence.length() + 1 == end) { - return charSequence; - } - if (charSequence instanceof Spannable spannable) { - Spannable sub = (Spannable) spannable.subSequence(start, end); - for (Class clazz : SPAN_CLASSES) { - ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz); - for (ParcelableSpan parcelableSpan : spannables) { - int beginSpan = spannable.getSpanStart(parcelableSpan); - int endSpan = spannable.getSpanEnd(parcelableSpan); - if (beginSpan >= start && endSpan <= end) { - continue; - } - sub.setSpan(clone(parcelableSpan), Math.max(beginSpan - start, 0), Math.min(Math.max(sub.length() - 1, 0), endSpan), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - return sub; - } else { - return charSequence.subSequence(start, end); - } - } - - private static ParcelableSpan clone(ParcelableSpan span) { - if (span instanceof ForegroundColorSpan) { - return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor()); - } else if (span instanceof TypefaceSpan) { - return new TypefaceSpan(((TypefaceSpan) span).getFamily()); - } else if (span instanceof StyleSpan) { - return new StyleSpan(((StyleSpan) span).getStyle()); - } else if (span instanceof StrikethroughSpan) { - return new StrikethroughSpan(); - } else { - throw new AssertionError("Unknown Span"); - } - } - - private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) { + public static final int XHTML_IGNORE = 1; + public static final int XHTML_REMOVE = 2; + public static final int XHTML_EMPHASIS = 3; + public static final int XHTML_CODE = 4; + public static final int NOLINKIFY = 0xf0; + + private static final List> SPAN_CLASSES = + Arrays.asList( + StyleSpan.class, + StrikethroughSpan.class, + TypefaceSpan.class, + RelativeSizeSpan.class, + ForegroundColorSpan.class); + + public static void clear(final Editable editable) { + final int end = editable.length() - 1; + for (Class clazz : SPAN_CLASSES) { + for (ParcelableSpan span : editable.getSpans(0, end, clazz)) { + editable.removeSpan(span); + } + } + } + + public static void format(final Editable editable, int start, int end, @ColorInt int textColor, final boolean composing) { + for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) { + final int keywordLength = style.getKeyword().length(); + int keywordLengthStart = keywordLength; + if ("```".equals(style.getKeyword())) { + int i; + for (i = style.getStart(); i < editable.length(); i++) { + if (editable.charAt(i) == '\n') break; + } + keywordLengthStart = i - style.getStart() + 1; + } + + editable.setSpan( + createSpanForStyle(style), + style.getStart() + keywordLengthStart, + style.getEnd() - keywordLength + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | + ("*".equals(style.getKeyword()) || "_".equals(style.getKeyword()) ? XHTML_EMPHASIS << Spanned.SPAN_USER_SHIFT : 0) | + ("```".equals(style.getKeyword()) && keywordLengthStart > 4 ? XHTML_CODE << Spanned.SPAN_USER_SHIFT : 0) | + ("`".equals(style.getKeyword()) || "```".equals(style.getKeyword()) ? NOLINKIFY << Spanned.SPAN_USER_SHIFT : 0) + ); + makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLengthStart, textColor, composing); + makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor, composing); + } + } + + public static void format(final Editable editable, @ColorInt int textColor) { + format(editable, textColor, false); + } + + public static void format(final Editable editable, @ColorInt int textColor, final boolean composing) { + int end = 0; + format(editable, end, editable.length() - 1, textColor, composing); + } + + public static void highlight( + final TextView view, final Editable editable, final List needles) { + for (final String needle : needles) { + if (!FtsUtils.isKeyword(needle)) { + highlight(view, editable, needle); + } + } + } + + public static List filterHighlightedWords(List terms) { + List words = new ArrayList<>(); + for (String term : terms) { + if (!FtsUtils.isKeyword(term)) { + StringBuilder builder = new StringBuilder(); + for (int codepoint, i = 0; i < term.length(); i += Character.charCount(codepoint)) { + codepoint = term.codePointAt(i); + if (Character.isLetterOrDigit(codepoint)) { + builder.append(Character.toChars(codepoint)); + } else if (builder.length() > 0) { + words.add(builder.toString()); + builder.delete(0, builder.length()); + } + } + if (builder.length() > 0) { + words.add(builder.toString()); + } + } + } + return words; + } + + private static void highlight( + final TextView view, final Editable editable, final String needle) { + final int length = needle.length(); + String string = editable.toString(); + int start = indexOfIgnoreCase(string, needle, 0); + while (start != -1) { + int end = start + length; + editable.setSpan( + new BackgroundColorSpan( + MaterialColors.getColor( + view, com.google.android.material.R.attr.colorPrimaryFixedDim)), + start, + end, + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + editable.setSpan( + new ForegroundColorSpan( + MaterialColors.getColor( + view, com.google.android.material.R.attr.colorOnPrimaryFixed)), + start, + end, + SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE); + start = indexOfIgnoreCase(string, needle, start + length); + } + } + + static CharSequence subSequence(CharSequence charSequence, int start, int end) { + if (start == 0 && charSequence.length() + 1 == end) { + return charSequence; + } + if (charSequence instanceof Spannable spannable) { + Spannable sub = (Spannable) spannable.subSequence(start, end); + for (Class clazz : SPAN_CLASSES) { + ParcelableSpan[] spannables = spannable.getSpans(start, end, clazz); + for (ParcelableSpan parcelableSpan : spannables) { + int beginSpan = spannable.getSpanStart(parcelableSpan); + int endSpan = spannable.getSpanEnd(parcelableSpan); + if (beginSpan >= start && endSpan <= end) { + continue; + } + sub.setSpan( + clone(parcelableSpan), + Math.max(beginSpan - start, 0), + Math.min(sub.length() - 1, endSpan), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + return sub; + } else { + return charSequence.subSequence(start, end); + } + } + + private static ParcelableSpan clone(ParcelableSpan span) { + if (span instanceof ForegroundColorSpan) { + return new ForegroundColorSpan(((ForegroundColorSpan) span).getForegroundColor()); + } else if (span instanceof TypefaceSpan) { + return new TypefaceSpan(((TypefaceSpan) span).getFamily()); + } else if (span instanceof StyleSpan) { + return new StyleSpan(((StyleSpan) span).getStyle()); + } else if (span instanceof StrikethroughSpan) { + return new StrikethroughSpan(); + } else { + throw new AssertionError("Unknown Span"); + } + } + + private static ParcelableSpan createSpanForStyle(final ImStyleParser.Style style) { return switch (style.getKeyword()) { case "*" -> new StyleSpan(Typeface.BOLD); case "_" -> new StyleSpan(Typeface.ITALIC); @@ -215,78 +223,74 @@ public class StylingHelper { case "`", "```" -> new TypefaceSpan("monospace"); default -> throw new AssertionError("Unknown Style"); }; - } - - private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor, final boolean composing) { - QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class); - @ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor; - @ColorInt int keywordColor = transformColor(textColor); - if (composing) { - if (end-start > 1) { - editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); - } else { - editable.setSpan(new RelativeSizeSpan(0), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); - } - } else { - editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - - private static - @ColorInt - int transformColor(@ColorInt int c) { - return Color.argb(Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c)); - } - - private static int indexOfIgnoreCase(final String haystack, final String needle, final int start) { - if (haystack == null || needle == null) { - return -1; - } - final int endLimit = haystack.length() - needle.length() + 1; - if (start > endLimit) { - return -1; - } - if (needle.length() == 0) { - return start; - } - for (int i = start; i < endLimit; i++) { - if (haystack.regionMatches(true, i, needle, 0, needle.length())) { - return i; - } - } - return -1; - } - - public static class MessageEditorStyler implements TextWatcher { - - private final EditText mEditText; - private final MessageAdapter mAdapter; - - public MessageEditorStyler(EditText editText, MessageAdapter adapter) { - this.mEditText = editText; - this.mAdapter = adapter; - } - - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { - - } - - @Override - public void afterTextChanged(Editable editable) { - clear(editable); - final var p = PreferenceManager.getDefaultSharedPreferences(mEditText.getContext()); - if (!p.getBoolean("compose_rich_text", mEditText.getContext().getResources().getBoolean(R.bool.compose_rich_text))) return; - for (final var span : editable.getSpans(0, editable.length() - 1, QuoteSpan.class)) { - editable.removeSpan(span); - } - format(editable, mEditText.getCurrentTextColor(), true); - mAdapter.handleTextQuotes(mEditText, editable, false); - } - } + } + + private static void makeKeywordOpaque(final Editable editable, int start, int end, @ColorInt int fallbackTextColor, final boolean composing) { + QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class); + @ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor; + @ColorInt int keywordColor = transformColor(textColor); + if (composing) { + if (end-start > 1) { + editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); + } else { + editable.setSpan(new RelativeSizeSpan(0), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | XHTML_REMOVE << Spanned.SPAN_USER_SHIFT); + } + } else { + editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + private static @ColorInt int transformColor(@ColorInt int c) { + return Color.argb( + Math.round(Color.alpha(c) * 0.45f), Color.red(c), Color.green(c), Color.blue(c)); + } + + private static int indexOfIgnoreCase( + final String haystack, final String needle, final int start) { + if (haystack == null || needle == null) { + return -1; + } + final int endLimit = haystack.length() - needle.length() + 1; + if (start > endLimit) { + return -1; + } + if (needle.length() == 0) { + return start; + } + for (int i = start; i < endLimit; i++) { + if (haystack.regionMatches(true, i, needle, 0, needle.length())) { + return i; + } + } + return -1; + } + + public static class MessageEditorStyler implements TextWatcher { + + private final EditText mEditText; + private final MessageAdapter mAdapter; + + public MessageEditorStyler(EditText editText, MessageAdapter adapter) { + this.mEditText = editText; + this.mAdapter = adapter; + } + + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void afterTextChanged(Editable editable) { + clear(editable); + final var p = PreferenceManager.getDefaultSharedPreferences(mEditText.getContext()); + if (!p.getBoolean("compose_rich_text", mEditText.getContext().getResources().getBoolean(R.bool.compose_rich_text))) return; + for (final var span : editable.getSpans(0, editable.length() - 1, QuoteSpan.class)) { + editable.removeSpan(span); + } + format(editable, mEditText.getCurrentTextColor(), true); + mAdapter.handleTextQuotes(mEditText, editable, false); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 7633e4f30665b6834b7327b8cd80621f27110c9d..0707453ff0df3ca02b5d1bbc27d8a7118433ad0a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -12,7 +12,6 @@ import android.util.Base64; import android.util.Log; import android.util.Pair; import android.util.SparseArray; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -21,6 +20,8 @@ import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.primitives.Ints; import org.xmlpull.v1.XmlPullParserException; @@ -57,6 +58,7 @@ import java.util.concurrent.ScheduledFuture; 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; @@ -76,8 +78,10 @@ import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.sasl.ChannelBinding; import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism; +import eu.siacs.conversations.crypto.sasl.DowngradeProtection; import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.crypto.sasl.ScramMechanism; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; @@ -91,6 +95,7 @@ import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Patterns; @@ -108,13 +113,13 @@ 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 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.cb.SaslChannelBinding; import im.conversations.android.xmpp.model.csi.Active; import im.conversations.android.xmpp.model.csi.Inactive; import im.conversations.android.xmpp.model.error.Condition; @@ -141,54 +146,12 @@ 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.streams.StreamError; 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; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.ConnectException; -import java.net.IDN; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; -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; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSocket; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509KeyManager; -import javax.net.ssl.X509TrustManager; - public class XmppConnection implements Runnable { protected final Account account; @@ -217,7 +180,7 @@ public class XmppConnection implements Runnable { private int stanzasSentBeforeAuthentication; private long lastPacketReceived = 0; private long lastPingSent = 0; - private long lastConnect = 0; + private long lastConnectionStarted = 0; private long lastSessionStarted = 0; private long lastDiscoStarted = 0; private boolean isMamPreferenceAlways = false; @@ -235,6 +198,7 @@ public class XmppConnection implements Runnable { private OnStatusChanged statusListener = null; private final Runnable bindListener; private OnMessageAcknowledged acknowledgedListener = null; + private final PendingItem pendingResumeId = new PendingItem<>(); private LoginInfo loginInfo; private HashedToken.Mechanism hashTokenRequest; private HttpUrl redirectionUrl = null; @@ -269,10 +233,10 @@ public class XmppConnection implements Runnable { } } - private static boolean validBase64(String input) { + private static boolean validBase64(final String input) { try { return Base64.decode(input, Base64.URL_SAFE).length == 3; - } catch (Throwable throwable) { + } catch (final Throwable throwable) { return false; } } @@ -325,7 +289,7 @@ public class XmppConnection implements Runnable { } public void prepareNewConnection() { - this.lastConnect = SystemClock.elapsedRealtime(); + this.lastConnectionStarted = SystemClock.elapsedRealtime(); this.lastPingSent = SystemClock.elapsedRealtime(); this.lastDiscoStarted = Long.MAX_VALUE; this.mWaitingForSmCatchup.set(false); @@ -345,49 +309,60 @@ public class XmppConnection implements Runnable { mXmppConnectionService.resetSendingToWaiting(account); } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting"); + this.pendingResumeId.clear(); this.loginInfo = null; this.features.encryptionEnabled = false; this.inSmacksSession = false; this.quickStartInProgress = false; this.isBound = false; this.attempt++; - this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified this.dane = false; - // with dnssec + this.currentResolverResult = null; + // will be set if user entered hostname is being used or hostname was verified with dnssec + this.verifiedHostname = null; try { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); this.changeStatus(Account.State.CONNECTING); final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion(); final boolean extended = mXmppConnectionService.showExtendedConnectionOptions(); + // TODO collapse Tor usage into normal connection code path if (useTor) { - String destination; - if (account.getHostname().isEmpty() || account.isOnion()) { - destination = account.getServer(); + final var seeOtherHost = this.seeOtherHostResolverResult; + final var hostname = account.getHostname().trim(); + final var port = account.getPort(); + final Resolver.Result resume = streamId == null ? null : streamId.location; + final Resolver.Result viaTor; + if (resume != null) { + viaTor = resume; + } else if (seeOtherHost != null) { + viaTor = seeOtherHost; + } else if (hostname.isEmpty() || port < 0) { + viaTor = + Iterables.getOnlyElement( + Resolver.fromHardCoded( + account.getServer(), Resolver.XMPP_PORT_STARTTLS)); } else { - destination = account.getHostname(); - this.verifiedHostname = destination; + viaTor = Iterables.getOnlyElement(Resolver.fromHardCoded(hostname, port)); + this.verifiedHostname = hostname; } - final int port = account.getPort(); - final boolean directTls = Resolver.useDirectTls(port); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + " via Tor: " + viaTor); - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": connect to " - + destination - + " via Tor. directTls=" - + directTls); - localSocket = SocksSocketFactory.createSocketOverTor(destination, port); + localSocket = + SocksSocketFactory.createSocketOverTor( + viaTor.asDestination(), viaTor.getPort()); - if (directTls) { + if (viaTor.isDirectTls()) { localSocket = upgradeSocketToTls(localSocket); features.encryptionEnabled = true; } try { - startXmpp(localSocket); + if (startXmpp(localSocket)) { + this.currentResolverResult = viaTor; + this.seeOtherHostResolverResult = null; + } } catch (final InterruptedException e) { Log.d( Config.LOGTAG, @@ -398,12 +373,12 @@ public class XmppConnection implements Runnable { throw new IOException("Could not start stream", e); } } else { + final var hostname = account.getHostname().trim(); final String domain = account.getServer(); final List results = new ArrayList<>(); - final boolean hardcoded = extended && !account.getHostname().isEmpty(); + final boolean hardcoded = extended && !hostname.isEmpty(); if (hardcoded) { - results.addAll( - Resolver.fromHardCoded(account.getHostname(), account.getPort())); + results.addAll(Resolver.fromHardCoded(hostname, account.getPort())); } else { results.addAll(Resolver.resolve(domain)); } @@ -496,16 +471,13 @@ public class XmppConnection implements Runnable { localSocket = new Socket(); localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - + localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); if (features.encryptionEnabled) { localSocket = upgradeSocketToTls(localSocket); } - - localSocket.setSoTimeout(Config.SOCKET_TIMEOUT * 1000); if (startXmpp(localSocket)) { - localSocket.setSoTimeout( - 0); // reset to 0; once the connection is established we don’t - // want this + // reset to 0; once the connection is established we don't want this + localSocket.setSoTimeout(0); if (!hardcoded && !result.equals(storedBackupResult)) { mXmppConnectionService.databaseBackend.saveResolverResult( domain, result); @@ -578,15 +550,17 @@ public class XmppConnection implements Runnable { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } + // this means we have at least found a socket to connect to. give the connection another 90s + this.lastConnectionStarted = SystemClock.elapsedRealtime(); this.socket = socket; - tagReader = new XmlReader(); + this.tagReader = new XmlReader(); if (tagWriter != null) { tagWriter.forceClose(); } - tagWriter = new TagWriter(); - tagWriter.setOutputStream(socket.getOutputStream()); - tagReader.setInputStream(socket.getInputStream()); - tagWriter.beginDocument(); + this.tagWriter = new TagWriter(); + this.tagWriter.setOutputStream(socket.getOutputStream()); + this.tagReader.setInputStream(socket.getInputStream()); + this.tagWriter.beginDocument(); final boolean quickStart; if (socket instanceof SSLSocket sslSocket) { SSLSockets.log(account, sslSocket); @@ -658,24 +632,28 @@ public class XmppConnection implements Runnable { this.mStreamCountDownLatch = streamCountDownLatch; Tag nextTag = tagReader.readTag(); while (nextTag != null && !nextTag.isEnd("stream")) { - if (nextTag.isStart("error")) { - processStreamError(nextTag); + if (nextTag.isStart("error", Namespace.STREAMS)) { + processStreamError(tagReader.readElement(nextTag, StreamError.class)); } else if (nextTag.isStart("features", Namespace.STREAMS)) { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { switchOverToTls(nextTag); } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); + } else if (!isSecure()) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } else if (account.isOptionSet(Account.OPTION_REGISTER) && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) { processIq(nextTag); - } else if (!isSecure() || this.loginInfo == null) { + } else if (this.loginInfo == null) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } else if (nextTag.isStart("success")) { - final Element success = tagReader.readElement(nextTag); - if (processSuccess(success)) { - break; - } + } else if (nextTag.isStart("success", Namespace.SASL)) { + processSuccess(tagReader.readElement(nextTag, Success.class)); + break; + } else if (nextTag.isStart("success", Namespace.SASL_2)) { + processSuccess( + tagReader.readElement( + nextTag, im.conversations.android.xmpp.model.sasl2.Success.class)); } else if (nextTag.isStart("failure", Namespace.SASL)) { final var failure = tagReader.readElement(nextTag, Failure.class); processFailure(failure); @@ -815,7 +793,7 @@ public class XmppConnection implements Runnable { tagWriter.writeElement(response); } - private boolean processSuccess(final Element element) + private void processSuccess(final StreamElement element) throws IOException, XmlPullParserException { final LoginInfo currentLoginInfo = this.loginInfo; final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo); @@ -964,12 +942,9 @@ public class XmppConnection implements Runnable { final Tag tag = tagReader.readTag(); if (tag != null && tag.isStart("stream", Namespace.STREAMS)) { processStream(); - return true; } else { throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); } - } else { - return false; } } @@ -1077,7 +1052,8 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": fast authentication failed. falling back to regular authentication"); + + ": fast authentication failed. falling back to regular" + + " authentication"); authenticate(); } else { throw new StateChangingException(Account.State.UNAUTHORIZED); @@ -1125,6 +1101,17 @@ public class XmppConnection implements Runnable { } private void processResumed(final Resumed resumed) throws StateChangingException { + final var pendingResumeId = this.pendingResumeId.pop(); + final var prevId = resumed.getPrevId(); + if (prevId == null || !prevId.equals(pendingResumeId)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": server tried resume with unknown id " + + prevId); + resetStreamId(); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } this.inSmacksSession = true; this.isBound = true; this.tagWriter.writeStanzaAsync(new Request()); @@ -1170,7 +1157,15 @@ public class XmppConnection implements Runnable { } sendPacket(packet); } - changeStatusToOnline(); + if (mWaitForDisco.get()) { + this.lastDiscoStarted = SystemClock.elapsedRealtime(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": awaiting disco results after resume"); + changeStatus(Account.State.CONNECTING); + } else { + changeStatusToOnline(); + } } private void changeStatusToOnline() { @@ -1546,9 +1541,9 @@ public class XmppConnection implements Runnable { + ": resuming after stanza #" + stanzasReceived); } - final var resume = new Resume(this.streamId.id, stanzasReceived); - this.mSmCatchupMessageCounter.set(0); - this.mWaitingForSmCatchup.set(true); + final var streamId = this.streamId.id; + final var resume = new Resume(streamId, stanzasReceived); + prepareForResume(streamId); this.tagWriter.writeStanzaAsync(resume); } else if (needsBinding) { if (this.streamFeatures.hasChild("bind", Namespace.BIND) @@ -1595,13 +1590,22 @@ public class XmppConnection implements Runnable { authElement = this.streamFeatures.getExtension(Authentication.class); } final Collection mechanisms = authElement.getMechanismNames(); - final Element cbElement = - this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING); - final Collection channelBindings = ChannelBinding.of(cbElement); + final var cbExtension = this.streamFeatures.getExtension(SaslChannelBinding.class); + final Collection channelBindings = ChannelBinding.of(cbExtension); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); this.validate(saslMechanism, mechanisms); + final DowngradeProtection downgradeProtection; + if (cbExtension != null) { + downgradeProtection = + new DowngradeProtection(mechanisms, cbExtension.getChannelBindingTypes()); + } else { + downgradeProtection = new DowngradeProtection(mechanisms); + } + if (saslMechanism instanceof ScramMechanism scramMechanism) { + scramMechanism.setDowngradeProtection(downgradeProtection); + } final boolean quickStartAvailable; final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); @@ -1626,6 +1630,11 @@ public class XmppConnection implements Runnable { hashTokenRequest = HashedToken.Mechanism.best( inline.getFastMechanisms(), SSLSockets.version(this.socket)); + // TODO warn or fail early if channel binding priority isn’t high enough compared to + // login mechanism + // ChannelBinding.priority(hashTokenRequest.channelBinding) + // < + // ChannelBindingMechanism.getPriority(saslMechanism) } else { hashTokenRequest = null; } @@ -1641,7 +1650,8 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": interrupted while waiting for DB restore during SASL2 bind"); + + ": interrupted while waiting for DB restore during SASL2" + + " bind"); return; } } @@ -1776,9 +1786,9 @@ public class XmppConnection implements Runnable { authenticate.addChild(generateBindRequest(bind)); } if (inlineStreamManagement && streamId != null) { - final var resume = new Resume(this.streamId.id, stanzasReceived); - this.mSmCatchupMessageCounter.set(0); - this.mWaitingForSmCatchup.set(true); + final var streamId = this.streamId.id; + final var resume = new Resume(streamId, stanzasReceived); + prepareForResume(streamId); authenticate.addExtension(resume); } if (hashedTokenRequest != null) { @@ -1790,6 +1800,12 @@ public class XmppConnection implements Runnable { return authenticate; } + private void prepareForResume(final String streamId) { + this.mSmCatchupMessageCounter.set(0); + this.mWaitingForSmCatchup.set(true); + this.pendingResumeId.push(streamId); + } + private Bind generateBindRequest(final Collection bindFeatures) { Log.d(Config.LOGTAG, "inline bind features: " + bindFeatures); final var bind = new Bind(); @@ -2154,9 +2170,9 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); mPendingServiceDiscoveries.set(0); mWaitForDisco.set(waitForDisco); - lastDiscoStarted = SystemClock.elapsedRealtime(); + this.lastDiscoStarted = SystemClock.elapsedRealtime(); mXmppConnectionService.scheduleWakeUpCall( - Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); + Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode()); final Element caps = streamFeatures.findChild("c"); final String hash = caps == null ? null : caps.getAttribute("hash"); final String ver = caps == null ? null : caps.getAttribute("ver"); @@ -2388,14 +2404,11 @@ public class XmppConnection implements Runnable { }); } - private void processStreamError(final Tag currentTag) throws IOException { - final Element streamError = tagReader.readElement(currentTag); - if (streamError == null) { - return; - } - if (streamError.hasChild("conflict")) { - final var loginInfo = this.loginInfo; - if (loginInfo != null && loginInfo.saslVersion == SaslMechanism.Version.SASL_2) { + private void processStreamError(final StreamError streamError) throws IOException { + final var loginInfo = this.loginInfo; + final var isSecureLoggedIn = isSecure() && LoginInfo.isSuccess(loginInfo); + if (isSecureLoggedIn && streamError.hasChild("conflict")) { + if (loginInfo.saslVersion == SaslMechanism.Version.SASL_2) { this.appSettings.resetInstallationId(); } account.setResource(createNewResource()); @@ -2409,10 +2422,12 @@ public class XmppConnection implements Runnable { } else if (streamError.hasChild("host-unknown")) { throw new StateChangingException(Account.State.HOST_UNKNOWN); } else if (streamError.hasChild("policy-violation")) { - this.lastConnect = SystemClock.elapsedRealtime(); + this.lastConnectionStarted = SystemClock.elapsedRealtime(); final String text = streamError.findChildContent("text"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": policy violation. " + text); - failPendingMessages(text); + if (isSecureLoggedIn) { + failPendingMessages(text); + } throw new StateChangingException(Account.State.POLICY_VIOLATION); } else if (streamError.hasChild("see-other-host")) { final String seeOtherHost = streamError.findChildContent("see-other-host"); @@ -2708,6 +2723,7 @@ public class XmppConnection implements Runnable { } private void resetStreamId() { + this.pendingResumeId.clear(); this.streamId = null; this.boundStreamFeatures = null; } @@ -2725,11 +2741,11 @@ public class XmppConnection implements Runnable { } public Jid findDiscoItemByFeature(final String feature) { - final List> items = findDiscoItemsByFeature(feature); - if (items.size() >= 1) { - return items.get(0).getKey(); + final var items = findDiscoItemsByFeature(feature); + if (items.isEmpty()) { + return null; } - return null; + return Iterables.getFirst(items, null).getKey(); } public boolean r() { @@ -2764,8 +2780,7 @@ public class XmppConnection implements Runnable { } public String getMucServer() { - List servers = getMucServers(); - return servers.size() > 0 ? servers.get(0) : null; + return Iterables.getFirst(getMucServers(), null); } public int getTimeToNextAttempt(final boolean aggressive) { @@ -2777,9 +2792,8 @@ public class XmppConnection implements Runnable { account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0; interval = Math.min((int) (25 * Math.pow(1.3, (additionalTime + attempt))), 300); } - final int secondsSinceLast = - (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); - return interval - secondsSinceLast; + final var connectionDuration = Ints.saturatedCast(getConnectionDuration() / 1000); + return interval - connectionDuration; } public int getAttempt() { @@ -2795,16 +2809,16 @@ public class XmppConnection implements Runnable { return System.currentTimeMillis() - diff; } - public long getLastConnect() { - return this.lastConnect; + public long getConnectionDuration() { + return SystemClock.elapsedRealtime() - this.lastConnectionStarted; } - public long getLastPingSent() { - return this.lastPingSent; + public long getDiscoDuration() { + return SystemClock.elapsedRealtime() - this.lastDiscoStarted; } - public long getLastDiscoStarted() { - return this.lastDiscoStarted; + public long getLastPingSent() { + return this.lastPingSent; } public long getLastPacketReceived() { @@ -2822,7 +2836,7 @@ public class XmppConnection implements Runnable { public void resetAttemptCount(boolean resetConnectTime) { this.attempt = 0; if (resetConnectTime) { - this.lastConnect = 0; + this.lastConnectionStarted = 0; } } @@ -2870,6 +2884,22 @@ public class XmppConnection implements Runnable { sendIqPacket(iqPacket, unregisteredIqListener); } + public void triggerConnectionTimeout() { + final var duration = getConnectionDuration(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": connection timeout after " + duration + "ms"); + + // last connection time gets reset so time to next attempt is calculated correctly + this.lastConnectionStarted = SystemClock.elapsedRealtime(); + + // interrupt needs to be called before status change; otherwise we interrupt the newly + // created thread + this.interrupt(); + this.forceCloseSocket(); + this.changeStatus(Account.State.CONNECTION_TIMEOUT); + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { @@ -3133,7 +3163,7 @@ public class XmppConnection implements Runnable { new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { List> items = findDiscoItemsByFeature(namespace); - if (items.size() > 0) { + if (!items.isEmpty()) { try { long maxsize = Long.parseLong( @@ -3147,7 +3177,8 @@ public class XmppConnection implements Runnable { Log.d( Config.LOGTAG, account.getJid().asBareJid() - + ": http upload is not available for files with size " + + ": http upload is not available for files with" + + " size " + filesize + " (max is " + maxsize @@ -3172,7 +3203,7 @@ public class XmppConnection implements Runnable { for (String namespace : new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) { List> items = findDiscoItemsByFeature(namespace); - if (items.size() > 0) { + if (!items.isEmpty()) { try { return Long.parseLong( items.get(0) @@ -3192,6 +3223,7 @@ public class XmppConnection implements Runnable { public boolean bookmarks2() { return pepPublishOptions() + && pepConfigNodeMax() && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT); } 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 7716792885eb154a8cb0d8a75b23ccad04f51f98..1b1a2d0e3ecf5f287467f789f771b7320df82ce2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -20,6 +20,7 @@ import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; + import im.conversations.android.xmpp.model.jingle.Jingle; import im.conversations.android.xmpp.model.stanza.Iq; 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 be090dab273131bc6e79a01d1941c76f9109505b..4d4ac2fb274313aae5d72e1657d371a19fb1efaf 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -789,6 +789,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { final var contact = account.getRoster().getContact(with); callIntegration.setCallerDisplayName( contact.getDisplayName(), TelecomManager.PRESENTATION_ALLOWED); + callIntegration.setInitialized(); callIntegration.setInitialAudioDevice(CallIntegration.initialAudioDevice(media)); callIntegration.startAudioRouting(); final RtpSessionProposal proposal = 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 712b7ccb2fa1bec643de6554d64155d65ef91f92..ca035d5dfc7a7ab00c165c14e6b6db78d95a9f35 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -1,9 +1,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; - import androidx.annotation.NonNull; - import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.base.Throwables; @@ -16,7 +14,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; - import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Conversation; @@ -38,19 +35,8 @@ 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 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; -import org.bouncycastle.crypto.io.CipherOutputStream; -import org.bouncycastle.crypto.modes.AEADBlockCipher; -import org.bouncycastle.crypto.modes.GCMBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.KeyParameter; -import org.webrtc.IceCandidate; - import java.io.Closeable; import java.io.EOFException; import java.io.File; @@ -68,6 +54,14 @@ import java.util.Objects; import java.util.Optional; import java.util.Queue; import java.util.concurrent.CountDownLatch; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.webrtc.IceCandidate; public class JingleFileTransferConnection extends AbstractJingleConnection implements Transport.Callback, Transferable { @@ -205,8 +199,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection if (transition( State.SESSION_INITIALIZED, () -> this.initiatorFileTransferContentMap = contentMap)) { - final var iq = - contentMap.toJinglePacket(Jingle.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 = @@ -456,8 +449,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void sendSessionAccept(final FileTransferContentMap contentMap) { setLocalContentMap(contentMap); - final var iq = - contentMap.toJinglePacket(Jingle.Action.SESSION_ACCEPT, id.sessionId); + 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(); @@ -562,7 +554,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.d(Config.LOGTAG, "peer confirmed received " + received); } - private void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) { + private synchronized void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) { respondOk(jinglePacket); final Jingle.ReasonWrapper wrapper = jingle.getReason(); final State previous = this.state; @@ -589,7 +581,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } terminateTransport(); final State target = reasonToState(wrapper.reason); - // TODO check if we were already terminated transitionOrThrow(target); finish(); } @@ -865,13 +856,21 @@ public class JingleFileTransferConnection extends AbstractJingleConnection @Override public void onTransportEstablished() { - Log.d(Config.LOGTAG, "on transport established"); + Log.d(Config.LOGTAG, "transport established"); final AbstractFileTransceiver fileTransceiver; try { fileTransceiver = setupTransceiver(isResponder()); } catch (final Exception e) { - Log.d(Config.LOGTAG, "failed to set up file transceiver", e); - sendSessionTerminate(Reason.ofThrowable(e), e.getMessage()); + terminateTransport(); + if (isTerminated()) { + Log.d( + Config.LOGTAG, + "failed to set up file transceiver but session has already been" + + " terminated"); + } else { + Log.d(Config.LOGTAG, "failed to set up file transceiver", e); + sendSessionTerminate(Reason.ofThrowable(e), e.getMessage()); + } return; } this.fileTransceiver = fileTransceiver; @@ -951,6 +950,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } private AbstractFileTransceiver setupTransceiver(final boolean receiving) throws IOException { + final var transport = this.transport; + if (transport == null) { + throw new IOException("No transport configured"); + } final var fileDescription = getLocalContentMap().requireOnlyFile(); final File file = xmppConnectionService.getFileBackend().getFile(message); final Runnable updateRunnable = () -> jingleConnectionManager.updateConversationUi(false); @@ -987,7 +990,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void sendSessionInfo(final FileTransferDescription.SessionInfo sessionInfo) { final var iq = new Iq(Iq.Type.SET); - final var jinglePacket = iq.addExtension(new Jingle(Jingle.Action.SESSION_INFO, this.id.sessionId)); + final var jinglePacket = + iq.addExtension(new Jingle(Jingle.Action.SESSION_INFO, this.id.sessionId)); jinglePacket.addChild(sessionInfo.asElement()); send(iq); } @@ -996,11 +1000,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection public void onTransportSetupFailed() { final var transport = this.transport; if (transport == null) { - // this can happen on IQ timeouts - if (isTerminated()) { - return; + synchronized (this) { + // this can happen on IQ timeouts + if (isTerminated()) { + return; + } + sendSessionTerminate(Reason.FAILED_APPLICATION, null); } - sendSessionTerminate(Reason.FAILED_APPLICATION, null); return; } Log.d(Config.LOGTAG, "onTransportSetupFailed"); @@ -1071,8 +1077,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection + contentName); return; } - final Iq iq = - transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); + final Iq iq = transportInfo.toJinglePacket(Jingle.Action.TRANSPORT_INFO, id.sessionId); send(iq); } @@ -1175,12 +1180,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } final var state = getState(); return switch (state) { - case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED -> Transferable - .STATUS_OFFER; + case NULL, SESSION_INITIALIZED, SESSION_INITIALIZED_PRE_APPROVED -> + Transferable.STATUS_OFFER; case TERMINATED_APPLICATION_FAILURE, - TERMINATED_CONNECTIVITY_ERROR, - TERMINATED_DECLINED_OR_BUSY, - TERMINATED_SECURITY_ERROR -> Transferable.STATUS_FAILED; + TERMINATED_CONNECTIVITY_ERROR, + TERMINATED_DECLINED_OR_BUSY, + TERMINATED_SECURITY_ERROR -> + Transferable.STATUS_FAILED; case TERMINATED_CANCEL_OR_TIMEOUT -> Transferable.STATUS_CANCELLED; case SESSION_ACCEPTED -> Transferable.STATUS_DOWNLOADING; default -> Transferable.STATUS_UNKNOWN; @@ -1255,7 +1261,8 @@ public class JingleFileTransferConnection extends AbstractJingleConnection } terminateTransport(); final Iq iq = new Iq(Iq.Type.SET); - final var jingle = iq.addExtension(new Jingle(Jingle.Action.SESSION_TERMINATE, id.sessionId)); + 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(); 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 5b8fe4db2fe01af6fc4c2fb5486d6152b441a79b..362f68a13810f20912e827273f6966dfccf4f62f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -249,20 +249,7 @@ public class JingleRtpConnection extends AbstractJingleConnection } receiveTransportInfo(jinglePacket, contentMap); } else { - if (isTerminated()) { - respondOk(jinglePacket); - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": ignoring out-of-order transport info; we where already terminated"); - } else { - Log.d( - Config.LOGTAG, - id.account.getJid().asBareJid() - + ": received transport info while in state=" - + this.state); - terminateWithOutOfOrder(jinglePacket); - } + receiveOutOfOrderAction(jinglePacket, Jingle.Action.TRANSPORT_INFO); } } @@ -350,7 +337,7 @@ public class JingleRtpConnection extends AbstractJingleConnection }, MoreExecutors.directExecutor()); } else { - terminateWithOutOfOrder(iq); + receiveOutOfOrderAction(iq, Jingle.Action.CONTENT_ADD); } } @@ -426,7 +413,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; if (outgoingContentAdd == null) { Log.d(Config.LOGTAG, "received content-accept when we had no outgoing content add"); - terminateWithOutOfOrder(jinglePacket); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_ACCEPT); return; } final Set ourSummary = ContentAddition.summary(outgoingContentAdd); @@ -456,7 +443,7 @@ public class JingleRtpConnection extends AbstractJingleConnection MoreExecutors.directExecutor()); } else { Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add"); - terminateWithOutOfOrder(jinglePacket); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_ACCEPT); } } @@ -497,7 +484,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void receiveContentModify(final Iq jinglePacket, final Jingle jingle) { if (this.state != State.SESSION_ACCEPTED) { - terminateWithOutOfOrder(jinglePacket); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_MODIFY); return; } final Map modification = @@ -623,7 +610,7 @@ public class JingleRtpConnection extends AbstractJingleConnection final RtpContentMap outgoingContentAdd = this.outgoingContentAdd; if (outgoingContentAdd == null) { Log.d(Config.LOGTAG, "received content-reject when we had no outgoing content add"); - terminateWithOutOfOrder(jinglePacket); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_REJECT); return; } final Set ourSummary = ContentAddition.summary(outgoingContentAdd); @@ -634,7 +621,7 @@ public class JingleRtpConnection extends AbstractJingleConnection receiveContentReject(ourSummary); } else { Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add"); - terminateWithOutOfOrder(jinglePacket); + receiveOutOfOrderAction(jinglePacket, Jingle.Action.CONTENT_REJECT); } } @@ -1678,8 +1665,7 @@ public class JingleRtpConnection extends AbstractJingleConnection // in environments where we always use discovery timeouts we always want to respond with // 'ringing' if (Config.JINGLE_MESSAGE_INIT_STRICT_DEVICE_TIMEOUT - || (xmppConnectionService.confirmMessages() - && id.getContact().showInContactList())) { + || id.getContact().showInContactList()) { sendJingleMessage("ringing"); } } else { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java index 575a9899e102e732c368eb414aa3f94d723db3cb..ede754f6153d129bff69156a2bfa61430eb236b4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.xmpp.jingle; import com.google.common.base.Strings; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSet; import java.util.ArrayList; import java.util.Arrays; @@ -9,6 +10,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Presence; @@ -25,14 +27,14 @@ public class RtpCapability { Namespace.JINGLE_APPS_RTP, Namespace.JINGLE_APPS_DTLS ); - private static final List VIDEO_REQUIREMENTS = Arrays.asList( + private static final Collection VIDEO_REQUIREMENTS = Arrays.asList( Namespace.JINGLE_FEATURE_AUDIO, Namespace.JINGLE_FEATURE_VIDEO ); public static Capability check(final Presence presence) { final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - final List features = disco == null ? Collections.emptyList() : disco.getFeatures(); + final Set features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures()); if (features.containsAll(BASIC_RTP_REQUIREMENTS)) { if (features.containsAll(VIDEO_REQUIREMENTS)) { return Capability.VIDEO; @@ -66,7 +68,7 @@ public class RtpCapability { public static Capability check(final Contact contact, final boolean allowFallback) { final Presences presences = contact.getPresences(); - if (presences.size() == 0 && allowFallback && contact.getAccount().isEnabled()) { + if (presences.isEmpty() && allowFallback && contact.getAccount().isEnabled()) { Contact gateway = contact.getAccount().getRoster().getContact(Jid.of(contact.getJid().getDomain())); if (gateway.showInRoster() && gateway.getPresences().anyIdentity("gateway", "pstn")) { return Capability.AUDIO; 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 904c4aba89568f863bef293db89959cf58e187c7..95b15438460b50ac229ed522162e714f820a48d6 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,6 +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 im.conversations.android.xmpp.model.stanza.Iq; import org.webrtc.CandidatePairChangeEvent; @@ -45,6 +46,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -455,7 +457,7 @@ public class WebRTCDataChannelTransport implements Transport { if (future != null && future.isDone()) { try { return future.get(); - } catch (final InterruptedException | ExecutionException e) { + } catch (final InterruptedException | ExecutionException | CancellationException e) { throw new WebRTCWrapper.PeerConnectionNotInitialized(); } } else { 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 ee3770ead9f3a56322ba8fd62c4660b7f954b701..c9b764752d3bbea3225ffa4b67bb1a3a75b31730 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/PublishOptions.java @@ -1,16 +1,13 @@ package eu.siacs.conversations.xmpp.pep; import android.os.Bundle; - import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.xmpp.model.stanza.Iq; public class PublishOptions { - private PublishOptions() { - - } + private PublishOptions() {} public static Bundle openAccess() { final Bundle options = new Bundle(); @@ -18,6 +15,12 @@ public class PublishOptions { return options; } + public static Bundle presenceAccess() { + final Bundle options = new Bundle(); + options.putString("pubsub#access_model", "presence"); + return options; + } + public static Bundle persistentWhitelistAccess() { final Bundle options = new Bundle(); options.putString("pubsub#persist_items", "true"); @@ -32,14 +35,15 @@ public class PublishOptions { options.putString("pubsub#send_last_published_item", "never"); options.putString("pubsub#max_items", "max"); options.putString("pubsub#notify_delete", "true"); - options.putString("pubsub#notify_retract", "true"); //one could also set notify=true on the retract + options.putString( + "pubsub#notify_retract", "true"); // one could also set notify=true on the retract return options; } public static boolean preconditionNotMet(Iq response) { - final Element error = response.getType() == Iq.Type.ERROR ? response.findChild("error") : null; + 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/im/conversations/android/xmpp/model/cb/ChannelBinding.java b/src/main/java/im/conversations/android/xmpp/model/cb/ChannelBinding.java new file mode 100644 index 0000000000000000000000000000000000000000..f4ad2c57072e18afab49596e33d5ffc5098ddd19 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/cb/ChannelBinding.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.cb; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class ChannelBinding extends Extension { + + public ChannelBinding() { + super(ChannelBinding.class); + } + + public String getType() { + return this.getAttribute("type"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java b/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java new file mode 100644 index 0000000000000000000000000000000000000000..c01ce1b9858114b64c96cbad72758d8967bf3d4f --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java @@ -0,0 +1,25 @@ +package im.conversations.android.xmpp.model.cb; + +import com.google.common.base.Predicates; +import com.google.common.collect.Collections2; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamFeature; +import java.util.Collection; + +@XmlElement +public class SaslChannelBinding extends StreamFeature { + + public SaslChannelBinding() { + super(SaslChannelBinding.class); + } + + public Collection getChannelBindings() { + return this.getExtensions(ChannelBinding.class); + } + + public Collection getChannelBindingTypes() { + return Collections2.filter( + Collections2.transform(getChannelBindings(), ChannelBinding::getType), + Predicates.notNull()); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/cb/package-info.java b/src/main/java/im/conversations/android/xmpp/model/cb/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..669c19d2949b37b0b433d622542aa94d546f7f36 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/cb/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.CHANNEL_BINDING) +package im.conversations.android.xmpp.model.cb; + +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 index dbd7395572c7b614f9c7caf83996fbe94e8aca55..7c96118be656520c1bbb04d167edd366d337916b 100644 --- a/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java +++ b/src/main/java/im/conversations/android/xmpp/model/correction/Replace.java @@ -14,6 +14,11 @@ public class Replace extends Extension { super(Replace.class); } + public Replace(final String id) { + this(); + this.setId(id); + } + public String getId() { return Strings.emptyToNull(this.getAttribute("id")); } 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 index eb240745fd4b1402e96c838ef71a868f1d7dcf68..bcb36d4093f2cbe4f04423b6a1393a62c6aca28e 100644 --- a/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java +++ b/src/main/java/im/conversations/android/xmpp/model/sm/Resumed.java @@ -15,4 +15,8 @@ public class Resumed extends StreamElement { public Optional getHandled() { return this.getOptionalIntAttribute("h"); } + + public String getPrevId() { + return this.getAttribute("previd"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/streams/StreamError.java b/src/main/java/im/conversations/android/xmpp/model/streams/StreamError.java new file mode 100644 index 0000000000000000000000000000000000000000..d1b1b6ca054c8a6406a4468392874eaf55af8b01 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/streams/StreamError.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.streams; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.StreamElement; + +@XmlElement(name = "error") +public class StreamError extends StreamElement { + + public StreamError() { + super(StreamError.class); + } +} 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 index 31a93962104ef834cae83978ce89d65946fb61b8..ce2a07a02e6018aaeb643f3c1891846c921c495d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java +++ b/src/main/java/im/conversations/android/xmpp/model/unique/OriginId.java @@ -1,5 +1,6 @@ package im.conversations.android.xmpp.model.unique; +import com.google.common.base.Strings; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; @@ -9,4 +10,13 @@ public class OriginId extends Extension { public OriginId() { super(OriginId.class); } + + public OriginId(final String id) { + this(); + this.setAttribute("id", id); + } + + public String getId() { + return Strings.emptyToNull(this.getAttribute("id")); + } } diff --git a/src/main/res/drawable/ic_account_circle_24dp.xml b/src/main/res/drawable/ic_account_circle_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..25c7c244b325d55cda6b94c3a48f7019706eca77 --- /dev/null +++ b/src/main/res/drawable/ic_account_circle_24dp.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/src/main/res/drawable/ic_colors_24dp.xml b/src/main/res/drawable/ic_colors_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..106cd34c100146f327ba89881c2041ad50ac0129 --- /dev/null +++ b/src/main/res/drawable/ic_colors_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/drawable/ic_format_align_left_24dp.xml b/src/main/res/drawable/ic_format_align_left_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..5cab3ad2c0ed7d13544039d5e55036ddadd50402 --- /dev/null +++ b/src/main/res/drawable/ic_format_align_left_24dp.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/src/main/res/drawable/ic_mobile_friendly_24dp.xml b/src/main/res/drawable/ic_mobile_friendly_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..3c1b49c7d28586a66d7538a0eb5fb95882a2404f --- /dev/null +++ b/src/main/res/drawable/ic_mobile_friendly_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/drawable/ic_warning_48dp.xml b/src/main/res/drawable/ic_warning_48dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..200b6a9fc98980de5a9dfe59cd19670a3f35a787 --- /dev/null +++ b/src/main/res/drawable/ic_warning_48dp.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/res/drawable/message_bubble_received.xml b/src/main/res/drawable/message_bubble_received.xml index 0cf5a7b1beeb16f716eb45f429d271837466460b..0e1fbffa820e11912d08e478ba5cc5cf1b660e79 100644 --- a/src/main/res/drawable/message_bubble_received.xml +++ b/src/main/res/drawable/message_bubble_received.xml @@ -1,6 +1,6 @@ - + - + - + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_publish_profile_picture.xml b/src/main/res/layout/activity_publish_profile_picture.xml index 0eb4dd516ca6e8af33a5682fe388476650859e1e..6516e432ce4a9ddd1c3e87f1d124da04a2dea662 100644 --- a/src/main/res/layout/activity_publish_profile_picture.xml +++ b/src/main/res/layout/activity_publish_profile_picture.xml @@ -1,5 +1,6 @@ - + - + android:layout_above="@+id/button_bar" + android:layout_below="@id/app_bar_layout"> @@ -63,17 +65,25 @@ android:text="@string/or_long_press_for_default" android:textAppearance="?textAppearanceBodyMedium" /> + + - + android:textColor="?colorError" + tools:text="@string/error_saving_avatar" /> - + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:soundEffectsEnabled="false" /> @@ -170,7 +207,8 @@ + android:layout_centerInParent="true" + android:background="@android:color/transparent"> - + + android:layout_height="wrap_content" + android:padding="?dialogPreferredPadding"> - + +