From af0b9b2f395f4814a8eb838940bd03d6770f5103 Mon Sep 17 00:00:00 2001 From: ghose Date: Wed, 23 Apr 2025 03:51:26 +0000 Subject: [PATCH 01/24] Translated using Weblate (Galician) Currently translated at 67.8% (59 of 87 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4214204.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4214204.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4214204.txt b/fastlane/metadata/android/gl-ES/changelogs/4214204.txt new file mode 100644 index 0000000000000000000000000000000000000000..527ce685ae76bed83856a020febd6ea6a22438d0 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4214204.txt @@ -0,0 +1,2 @@ +* compatibilidade con 'Service Outage Status' +* arranxo de problemas menores de seguridade ao procesar varios corpos da mensaxe, occupant-ids e stanza-id From 014db1da561c507ac9d824b7ae9459e55f0f22d0 Mon Sep 17 00:00:00 2001 From: mccode Date: Wed, 23 Apr 2025 09:03:53 +0000 Subject: [PATCH 02/24] Added translation using Weblate (Irish) --- src/quicksy/res/values-ga/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/quicksy/res/values-ga/strings.xml diff --git a/src/quicksy/res/values-ga/strings.xml b/src/quicksy/res/values-ga/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/quicksy/res/values-ga/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From b8cfe7e8e62169fd51aea008c6840be73bde9d86 Mon Sep 17 00:00:00 2001 From: mccode Date: Wed, 23 Apr 2025 09:08:26 +0000 Subject: [PATCH 03/24] Translated using Weblate (Irish) Currently translated at 30.7% (4 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/ga/ --- src/conversations/res/values-ga/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/conversations/res/values-ga/strings.xml b/src/conversations/res/values-ga/strings.xml index ee21d93fd943365566a149cdeee1e54576395699..91c996957b972fcfbb68429eb76993faf1f03c0d 100644 --- a/src/conversations/res/values-ga/strings.xml +++ b/src/conversations/res/values-ga/strings.xml @@ -3,4 +3,5 @@ Roghnaigh do freastalaí XMPP Bain úsáid as conversations.im Oscail cuntas nua + An bhfuil cuntas XMPP agat? From 2661038543b1b77b932b967ca08b766a935a1781 Mon Sep 17 00:00:00 2001 From: neighborsbear Date: Thu, 24 Apr 2025 04:06:46 +0000 Subject: [PATCH 04/24] Translated using Weblate (Korean) Currently translated at 35.1% (378 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ko/ --- src/main/res/values-ko/strings.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index 76248579b7dfe7ba19ede1fa71b385dff0257d97..66b9c197c86199bec9d9a12f50f5084283444c53 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -1,7 +1,7 @@ 설정 - 계정 + 계정 관리 연락처 정보 계정 추가 이름 편집 @@ -387,4 +387,8 @@ 메세지가 클립보드에 복사되었습니다 위치 표시 바쁨 - \ No newline at end of file + 계정 관리 + 채팅 기록 + 그룹 채팅 정보 + 채널 정보 + From 5b6cffcef6ee9963ab96bf58ba4aee4672f2a4a7 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 24 Apr 2025 19:17:47 +0000 Subject: [PATCH 05/24] Translated using Weblate (Russian) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index c1e5cbe638676d352d409d738a794bec58d576a7..3db84126ce10125e2ea70d056af37ac6e08f18b0 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -184,7 +184,7 @@ \nВаши собеседники не смогут больше отправлять вам зашифрованные OpenPGP сообщения. Публичный ключ OpenPGP опубликован. Включить аккаунт - Удалить свой аккаунт? Удаление аккаунта также сотрёт все историю бесед. + Удалить свой аккаунт? Удаление аккаунта также сотрёт всю историю бесед. Записать голос XMPP-адрес Заблокировать XMPP-адрес From 41211d7e27b83b1312f70661775e9163124094df Mon Sep 17 00:00:00 2001 From: themandalorian Date: Fri, 25 Apr 2025 04:38:46 +0000 Subject: [PATCH 06/24] Translated using Weblate (Portuguese (Brazil)) Currently translated at 10.3% (9 of 87 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pt_BR/ --- fastlane/metadata/android/pt-BR/changelogs/379.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/381.txt | 2 ++ fastlane/metadata/android/pt-BR/changelogs/42062.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/42065.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/4211604.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/4211804.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/4212104.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/4212504.txt | 1 + fastlane/metadata/android/pt-BR/changelogs/4212704.txt | 1 + 9 files changed, 10 insertions(+) create mode 100644 fastlane/metadata/android/pt-BR/changelogs/379.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/381.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/42062.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/42065.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/4211604.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/4211804.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/4212104.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/4212504.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/4212704.txt diff --git a/fastlane/metadata/android/pt-BR/changelogs/379.txt b/fastlane/metadata/android/pt-BR/changelogs/379.txt new file mode 100644 index 0000000000000000000000000000000000000000..6429a13141e1f846935cb33888a10a3fb554a84e --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/379.txt @@ -0,0 +1 @@ +Chamadas de Áudio/Vídeo (Requer suporte do servidor na forma de servidores STUN e TURN descobertos via XEP-0215) diff --git a/fastlane/metadata/android/pt-BR/changelogs/381.txt b/fastlane/metadata/android/pt-BR/changelogs/381.txt new file mode 100644 index 0000000000000000000000000000000000000000..049b99e4e62a5f11fde538d455168bca91023d1c --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/381.txt @@ -0,0 +1,2 @@ +* Feedback audível (discando, chamada iniciada, chamada encerrada) para chamadas de voz. +* Correção de problema com a tentativa de reintentar chamadas de vídeo falhadas diff --git a/fastlane/metadata/android/pt-BR/changelogs/42062.txt b/fastlane/metadata/android/pt-BR/changelogs/42062.txt new file mode 100644 index 0000000000000000000000000000000000000000..d239746242890bb4b29b0bfc25f7305c388edbd5 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/42062.txt @@ -0,0 +1 @@ +* Desabilitar a abertura de arquivos de backup (.ceb) pelo gerenciador de arquivos diff --git a/fastlane/metadata/android/pt-BR/changelogs/42065.txt b/fastlane/metadata/android/pt-BR/changelogs/42065.txt new file mode 100644 index 0000000000000000000000000000000000000000..50d69c942f7b14c2f193bd3d02a95d4416c2ee7c --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/42065.txt @@ -0,0 +1 @@ +* Introduzir novo formato de arquivo de backup diff --git a/fastlane/metadata/android/pt-BR/changelogs/4211604.txt b/fastlane/metadata/android/pt-BR/changelogs/4211604.txt new file mode 100644 index 0000000000000000000000000000000000000000..8df2f636760626284ec2ff2b269ed82f095d58a1 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4211604.txt @@ -0,0 +1 @@ +* Correções de bugs menores diff --git a/fastlane/metadata/android/pt-BR/changelogs/4211804.txt b/fastlane/metadata/android/pt-BR/changelogs/4211804.txt new file mode 100644 index 0000000000000000000000000000000000000000..6585028bdf1002ee96de2283564cd0a328a023fe --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4211804.txt @@ -0,0 +1 @@ +* Adicionar tempo limite para a iniciação da chamada diff --git a/fastlane/metadata/android/pt-BR/changelogs/4212104.txt b/fastlane/metadata/android/pt-BR/changelogs/4212104.txt new file mode 100644 index 0000000000000000000000000000000000000000..ae2419d89ac810372e91029ee045ad5cedd69282 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4212104.txt @@ -0,0 +1 @@ +* Suporte a Reações de Mensagens diff --git a/fastlane/metadata/android/pt-BR/changelogs/4212504.txt b/fastlane/metadata/android/pt-BR/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..6aec1d05f42e06866be479401c3b54e7c398c25f --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4212504.txt @@ -0,0 +1 @@ +* Melhorar o manuseio de algumas reações com emojis diff --git a/fastlane/metadata/android/pt-BR/changelogs/4212704.txt b/fastlane/metadata/android/pt-BR/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..91499dcccdbeb984912eb5720675c0df64a40a90 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/4212704.txt @@ -0,0 +1 @@ +* Adicionar a capacidade de exibir as bolhas de mensagem alinhadas à esquerda From 5c423ba6c37154d144b9148ef58eada5b4038ea7 Mon Sep 17 00:00:00 2001 From: ghose Date: Sat, 26 Apr 2025 05:03:43 +0000 Subject: [PATCH 07/24] Translated using Weblate (Galician) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 51ec7aaf88c72f6ab28828e7615dfe013f567c0e..87af85bb698684e6e8646683c325666eb0e05e45 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1110,7 +1110,7 @@ Mostrar só aos contactos Caducidade da conexión Reintentar con P2P - Non está dispoñible a vinculación de canles + Sen vinculación de canles Documento de Word Restaurar claves OMEMO Quicksy só pode restaurar copias de apoio de contas quicksy.im From 0211fb4858d6c5f236d018d7c0b02c0d04ae31bf Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Sat, 26 Apr 2025 08:24:36 +0000 Subject: [PATCH 08/24] Translated using Weblate (Polish) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index da6e940dab56231c7fb27e1b0b2654d3feb230b0..304792ed7e06dd71295b28a3a0315e5dfb4bf858 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1143,7 +1143,7 @@ Limit czasu połączenia Spróbuj ponownie używając P2P dokument Microsoft Word - Przywiązywanie kanału niedostępne + Brak przywiązywania kanału Przywróć klucze OMEMO Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im Lokalizacja kopii zapasowej From b1e5a5760521999d75d24f380fcb5633b02fcb29 Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 26 Apr 2025 08:23:40 +0000 Subject: [PATCH 09/24] Translated using Weblate (Russian) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 3db84126ce10125e2ea70d056af37ac6e08f18b0..13d221fca76da6e199422c753b40f35dabc0fe62 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -1157,7 +1157,7 @@ Показывать только контактам Истекло время ожидания подключения Повторить через P2P - Привязка канала недоступна + Нет привязки канала Документ Word Восстановить ключи OMEMO Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im From e1a77fa36c52d016adb7876266096f6fcf29dedd Mon Sep 17 00:00:00 2001 From: SomeTr Date: Fri, 25 Apr 2025 20:29:18 +0000 Subject: [PATCH 10/24] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index e1f2b8336c37c43d83082dac31fa03906a76e547..5720842b669c79ca5d36c82152dcb39d340fe115 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1158,7 +1158,7 @@ Бажаєте видалити свій аватар? Деякі клієнти можуть продовжувати відображати копію Вашого аватара з кешу. Час очікування з\'єднання вичерпано Повторити спробу з P2P - Прив\'язка каналу недоступна + Немає прив\'язки каналу документ Word Відновити ключі OMEMO Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im From 7124644521aee6f24e0447e3c4fc9a603e4656cd Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Sat, 26 Apr 2025 01:45:24 +0000 Subject: [PATCH 11/24] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index cb1eeb0eef11ad76c9a6ee6efe11a6fb59f6e528..a31c5315a16c967bc9a96a1a6f9158f6ce95a55a 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1105,7 +1105,7 @@ 仅对联系人显示 连接超时 使用 P2P 重试 - 不支持通道绑定 + 无通道绑定 Word 文档 恢复 OMEMO 密钥 Quicksy 只能恢复 quicksy.im 账号的备份 From 36bfc58e525884af26f1e4854d66ef82ec404b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 25 Apr 2025 21:50:55 +0000 Subject: [PATCH 12/24] Translated using Weblate (Estonian) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/et/ --- src/main/res/values-et/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 5ab238206bf055baa0983c834f9eceb0da4336ea..99e6dff62f146fb19e31782322bdf51d2cfd95e8 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -1130,7 +1130,7 @@ Näita vaid kontaktidele Ühenduse on aegunud Proovi uuesti võrdõigusvõrguga - Edastuskanaliga sidumine pole võimalik + Edastuskanaliga sidumine puudub Wordi-dokument Taasta OMEMO võtmed Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid From dfae4aebe5d1aba363113e24e66f787d984de18a Mon Sep 17 00:00:00 2001 From: Axel Reimer Date: Sun, 27 Apr 2025 23:39:40 +0200 Subject: [PATCH 13/24] Add ASUS Nexus 7 to the list of problematic AEC devices. --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 404461046576de1bc291601daa83353b276b0c9a..480a4415ebf0935a1f8e70121ce13d7c03f34459 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -70,6 +70,7 @@ public class WebRTCWrapper { .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) + .add("Nexus 7") ASUS Nexus 7 .build(); private final EventCallback eventCallback; From 59aa93856d6c93e608ef12df28b0fe02c05c29d5 Mon Sep 17 00:00:00 2001 From: Dirk Date: Sun, 27 Apr 2025 14:40:07 +0000 Subject: [PATCH 14/24] Translated using Weblate (German) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 13a4675658831880d5cafdbf594ece2a7189d420..0213884a4258d4a0223ac5edda12c631515d6b71 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1110,7 +1110,7 @@ Nur für Kontakte anzeigen Zeitüberschreitung beim Verbinden Erneut mit P2P versuchen - Kanalbindung nicht verfügbar + Keine Kanalbindung Word-Dokument OMEMO-Schlüssel wiederherstellen Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen From a252be8217c11b0239d9c49159d33c1b8b20d875 Mon Sep 17 00:00:00 2001 From: user11 Date: Sat, 26 Apr 2025 16:12:48 +0000 Subject: [PATCH 15/24] Translated using Weblate (Serbian) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sr/ --- src/main/res/values-sr/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 649ecd4b035aef62bd241e3e0ed964a740bb57d6..d9300d937df8d7b91d7770edb0903ade3f198374 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -1144,7 +1144,7 @@ Приказуј само контактима Истекла веза Покушај поново са P2P - Везивање канала недоступно + Нема везивања канала Word документ Quicksy може да врати резервне копије само за quicksy.im налоге Локација резервних копија @@ -1159,5 +1159,5 @@ Копирај URI Планирана недоступност Сервис недоступан (познат проблем) - Опоравак сервиса предвиђен у %s + Опоравак сервиса предвиђен за %s From f4bf533052e9be088280409f0bf896cb47031df9 Mon Sep 17 00:00:00 2001 From: "lucasmz.dev" Date: Sun, 27 Apr 2025 13:54:15 +0000 Subject: [PATCH 16/24] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 4e59b3ee8ebf9db90de91a218ba1da9ea29f1f10..eafe781032ff29b1545bc762b029f703654dbaf4 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -1128,7 +1128,7 @@ Você gostaria de excluir seu avatar? Alguns clientes podem continuar mostrando uma cópia em cache do seu avatar. Conexão demorou muito Tentar novamente com P2P - Vínculo de canal indisponível + Nenhum vínculo de canal Documento do Word Restaurar as chaves OMEMO O Quicksy só pode restaurar backups de contas quicksy.im From 5d4580adefb5eb138d353278d9ec368e00f7f046 Mon Sep 17 00:00:00 2001 From: random_r Date: Mon, 28 Apr 2025 14:55:59 +0000 Subject: [PATCH 17/24] Translated using Weblate (Italian) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index d08acec8155c9c41df7c23d883b30f21b7969d2d..d2c754e698551a13ddbbab44c4b79c499b12ad23 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -1124,7 +1124,7 @@ Messaggi di chat Colore di sfondo, dimensione caratteri, avatar Messaggi di chat - Associazione dei canali non disponibile + Nessuna associazione dei canali Documento Word Ripristina chiavi OMEMO Quicksy può ripristinare backup solo per profili quicksy.im From 3ba900bb762075fea0dfaa7f6fabc0cbc7925410 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 28 Apr 2025 03:47:42 +0000 Subject: [PATCH 18/24] Translated using Weblate (Hebrew) Currently translated at 33.4% (360 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/he/ --- src/main/res/values-iw/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 49c2b84ec85f2703b373438c520e7c2a519e9d5c..34a3af1039f0258240b4ac788f6b87c53de23942 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -353,7 +353,7 @@ מפתח הצפנה שגוי. שגיאה האפליקציה שבה השתמשת כדי לבחור תמונה זו לא סיפקה מספיק הרשאות לקרוא את הקובץ.\n\nהשתמש במנהל קבצים אחר כדי לבחור תמונה. - עטיפת ערוץ אינה זמינה + אין קישור ערוצים הסר את המפתח הציבורי של OpenPGP האם אתה בטוח שברצונך להסיר את מפתח OpenPGP הציבורי שלך מהודעת הנוכחות שלך?\nאנשי הקשר שלך לא יוכלו יותר לשלוח לך הודעות מוצפנות OpenPGP. מפתח ציבורי OpenPGP פורסם. From cdc04328cfaedcb876e4cda5f6c6463d7e762f1a Mon Sep 17 00:00:00 2001 From: hugoalh Date: Tue, 29 Apr 2025 09:54:21 +0000 Subject: [PATCH 19/24] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 98.3% (1058 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hant/ --- src/main/res/values-zh-rTW/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index f0814eca90c5db7ebb492a4935827649c04af804..b4cc9ebd27e2796d352b0b84edeb254826e11e8e 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -1002,7 +1002,7 @@ 已登出 您正在使用未驗證的設備。請掃描您其他設備上的 QR 碼進行驗證,以防止主動中間人攻擊。 有聲書 - 請勿嘗試還原非您自己建立的備份! + 僅還原由您親自建立的備份。 報告垃圾訊息 發送崩潰報告 相應的會話已存檔。 From f2fc1c6db87d2f761477814a1d65ea951611ef34 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Fri, 2 May 2025 15:37:48 +0000 Subject: [PATCH 20/24] Translated using Weblate (Ukrainian) Currently translated at 100.0% (87 of 87 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4211804.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/uk/changelogs/4211804.txt b/fastlane/metadata/android/uk/changelogs/4211804.txt index c3490793c919058ab1c1a700cb0fde390e090862..ee26cc2398c2408084f544b35c0d4decd23222e3 100644 --- a/fastlane/metadata/android/uk/changelogs/4211804.txt +++ b/fastlane/metadata/android/uk/changelogs/4211804.txt @@ -1 +1 @@ -* Додано таймаут для ініціювання виклику +* Додано тайм-аут для ініціювання виклику From f1e7c45d3e1b0d5fd2f67993cb78f4bb2c7b4f68 Mon Sep 17 00:00:00 2001 From: codimp Date: Mon, 5 May 2025 18:21:16 +0000 Subject: [PATCH 21/24] Translated using Weblate (French) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 05fbbf368536a14030530bb0daf28b35258886e6..141d38cce7db574997ccdf050e08da34e0c7bef9 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -820,12 +820,12 @@ e-book Original (non compressé) Ouvrir avec… - Photo de profil pour Conversations + Photo de profil Choisir un compte Restaurer la sauvegarde Restaurer Entrez votre mot de passe pour que le compte %s restaure la sauvegarde. - N\'utilisez pas la fonctionnalité de sauvegarde de la restauration pour tenter de cloner (exécuter simultanément) une installation. La restauration d’une sauvegarde ne concerne que les migrations ou en cas de perte de l\'appareil d’origine. + Ne restaurez pas les clés OMEMO pour tenter de cloner (exécuter simultanément) une installation. La restauration de clés OMEMO ne concerne que les migrations ou en cas de perte de l\'appareil d’origine. Impossible de restaurer la sauvegarde. Impossible de déchiffrer la sauvegarde. Le mot de passe est-il correct ? Sauvegarde & restauration @@ -1010,7 +1010,7 @@ Vous essayez d\'importer un format de fichier de sauvegarde obsolète Livre audio Distributeur UnifiedPush - Ne tentez pas de restaurer des sauvegardes que vous n\'avez pas créées vous-même ! + Ne restaurez que des sauvegardes que vous avez vous-même créées. Signaler un spam Politique de confidentialité Quicksy vous demande votre consentement pour utiliser vos données @@ -1099,7 +1099,7 @@ Modifier les paramètres de notification L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur. L\'appel passe par les écouteurs. - XEP-0386: Bind 2 + XEP-0386 : Bind 2 Éditer le pseudo Supprimer la clé OpenPGP L\'appel passe par le bluetooth. @@ -1121,7 +1121,7 @@ Montrer aux contacts uniquement Bulles de discussion Impossible de modifier l\'appel - Channel binding indisponible + Pas de Channel binding Le client XMPP de votre contact peut ne pas prendre en charge les appels audio/vidéo. Impossible d\'ajouter une réaction Intégration d\'appel @@ -1133,4 +1133,16 @@ Plus de réactions Ajouter une réaction Montrer l\'image de profil + Numéro de téléphone copié dans le presse-papier + Copier l\'URI + URI copiée dans le presse-papier + Copier le numéro de téléphone + Restaurer les clés OMEMO + Quicksy ne peut restaurer des sauvegardes que pour des comptes de quicksy.im + Emplacement de sauvegarde + URI + Copier l\'adresse mail + Adresse mail copiée dans le presse-papier + Interruption de Service Planifiée + Le retour du service est planifié pour %s From 27f337f09df8fbf98457041a05c8dac92537d0cc Mon Sep 17 00:00:00 2001 From: milimarg Date: Mon, 5 May 2025 18:22:25 +0000 Subject: [PATCH 22/24] Translated using Weblate (French) Currently translated at 100.0% (1076 of 1076 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 141d38cce7db574997ccdf050e08da34e0c7bef9..56fb3acf291bed70abc48cef80a5c1f891d66a1c 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -1145,4 +1145,6 @@ Adresse mail copiée dans le presse-papier Interruption de Service Planifiée Le retour du service est planifié pour %s + Copier la géolocalisation + Service en panne (problème connu) From e635de37c29f0fd6ab1b752a06281d0090c977f7 Mon Sep 17 00:00:00 2001 From: Axel Reimer Date: Wed, 7 May 2025 08:05:01 +0200 Subject: [PATCH 23/24] Add missing slashes --- .../java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 480a4415ebf0935a1f8e70121ce13d7c03f34459..52f0b6f0dc39fd70c39775ac67f5f68d3a56fe83 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -70,7 +70,7 @@ public class WebRTCWrapper { .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte) .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte) .add("GT-I9505") // Samsung Galaxy S4 (jfltexx) - .add("Nexus 7") ASUS Nexus 7 + .add("Nexus 7") // ASUS Nexus 7 .build(); private final EventCallback eventCallback; From 0b366cfaf8db40d9644f51c564913a56eae5ba73 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 9 May 2025 12:20:46 +0200 Subject: [PATCH 24/24] use new api for disco and caps --- build.gradle | 3 + .../siacs/conversations/entities/Account.java | 27 +- .../siacs/conversations/entities/Contact.java | 5 +- .../conversations/entities/MucOptions.java | 46 +- .../conversations/entities/Presence.java | 101 ----- .../entities/PresenceTemplate.java | 9 +- .../conversations/entities/Presences.java | 93 ++-- .../entities/ServiceDiscoveryResult.java | 349 --------------- .../generator/PresenceGenerator.java | 56 ++- .../conversations/parser/PresenceParser.java | 48 +- .../persistance/DatabaseBackend.java | 145 ++++--- .../services/MessageArchiveService.java | 5 +- .../services/XmppConnectionService.java | 181 +++----- .../ui/ContactDetailsActivity.java | 27 +- .../ui/ConversationFragment.java | 12 +- .../conversations/ui/EditAccountActivity.java | 14 +- .../ui/StartConversationActivity.java | 6 +- .../ui/adapter/ListItemAdapter.java | 273 ++++++------ .../conversations/ui/util/SendButtonTool.java | 53 ++- .../siacs/conversations/utils/UIHelper.java | 4 +- .../conversations/xml/XmlElementReader.java | 20 - .../conversations/xmpp/XmppConnection.java | 409 +++++++++--------- .../xmpp/jingle/AbstractJingleConnection.java | 21 +- .../xmpp/jingle/RtpCapability.java | 107 +++-- .../xmpp/manager/AbstractManager.java | 11 + .../xmpp/manager/DiscoManager.java | 307 +++++++++++++ .../android/xml/XmlElementReader.java | 21 + .../im/conversations/android/xmpp/Entity.java | 2 +- .../android/xmpp/EntityCapabilities2.java | 17 +- .../android/xmpp/model/data/Data.java | 4 + .../android/xmpp/model/data/Field.java | 8 +- .../xmpp/model/disco/info/InfoQuery.java | 38 ++ .../android/xmpp/model/stanza/Presence.java | 62 +++ .../android/xmpp/EntityCapabilitiesTest.java | 339 +++++++++++++++ 34 files changed, 1612 insertions(+), 1211 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/entities/Presence.java delete mode 100644 src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java delete mode 100644 src/main/java/eu/siacs/conversations/xml/XmlElementReader.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java create mode 100644 src/main/java/im/conversations/android/xml/XmlElementReader.java create mode 100644 src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java diff --git a/build.gradle b/build.gradle index 6b5c6655aa5dc1d1c7ffddda0f2f24897edf3efb..4de6bf686a763d81bb5992abc78577e23e1a5838 100644 --- a/build.gradle +++ b/build.gradle @@ -91,6 +91,9 @@ dependencies { quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.2.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.robolectric:robolectric:4.14.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + } ext { diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 66ec318071198811dfcd9387c1f2acd07700ffa0..5f38e1a51c50f17f24bd1a622660f953ece6e593 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -27,6 +27,7 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.RtpCapability; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -101,7 +102,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private final Map bookmarks = new HashMap<>(); - private Presence.Status presenceStatus; + private im.conversations.android.xmpp.model.stanza.Presence.Availability presenceStatus; private String presenceStatusMessage; private String pinnedMechanism; private String pinnedChannelBinding; @@ -121,7 +122,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable null, null, Resolver.XMPP_PORT_STARTTLS, - Presence.Status.ONLINE, + im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE, null, null, null, @@ -140,7 +141,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable String displayName, String hostname, int port, - final Presence.Status status, + final im.conversations.android.xmpp.model.stanza.Presence.Availability status, String statusMessage, final String pinnedMechanism, final String pinnedChannelBinding, @@ -203,7 +204,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)), cursor.getInt(cursor.getColumnIndexOrThrow(PORT)), - Presence.Status.fromShowString( + im.conversations.android.xmpp.model.stanza.Presence.Availability.valueOfShown( cursor.getString(cursor.getColumnIndexOrThrow(STATUS))), cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)), cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)), @@ -451,11 +452,12 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable && getXmppConnection().getAttempt() >= 3; } - public Presence.Status getPresenceStatus() { + public im.conversations.android.xmpp.model.stanza.Presence.Availability getPresenceStatus() { return this.presenceStatus; } - public void setPresenceStatus(Presence.Status status) { + public void setPresenceStatus( + im.conversations.android.xmpp.model.stanza.Presence.Availability status) { this.presenceStatus = status; } @@ -584,9 +586,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable } public int activeDevicesWithRtpCapability() { + final var connection = getXmppConnection(); + if (connection == null) { + return 0; + } int i = 0; - for (Presence presence : getSelfContact().getPresences().getPresences()) { - if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) { + for (String resource : getSelfContact().getPresences().getPresencesMap().keySet()) { + final var jid = + Strings.isNullOrEmpty(resource) + ? getJid().asBareJid() + : getJid().withResource(resource); + if (RtpCapability.check(connection.getManager(DiscoManager.class).get(jid)) + != RtpCapability.Capability.NONE) { i++; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 6508d00a966b88fccc8c66e61095c9c49be3c30f..f13ffa973ad35a5b69bcedeeb4fa14aea134c2ec 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -17,6 +17,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.RtpCapability; import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -55,7 +56,7 @@ public class Contact implements ListItem, Blockable { private String photoUri; private final JSONObject keys; private JSONArray groups = new JSONArray(); - private final Presences presences = new Presences(); + private final Presences presences = new Presences(this); protected Account account; protected Avatar avatar; @@ -275,7 +276,7 @@ public class Contact implements ListItem, Blockable { this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); } - public Presence.Status getShownStatus() { + public im.conversations.android.xmpp.model.stanza.Presence.Availability getShownStatus() { return this.presences.getShownStatus(); } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 7ba6f8a23276a8878c87e7735d2c10ff7871dd8b..2d939d153d600958bc5ecb9769132f0859062281 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -14,9 +14,10 @@ import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.chatstate.ChatState; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.model.data.Data; +import im.conversations.android.xmpp.model.data.Field; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,7 +44,7 @@ public class MucOptions { public OnRenameListener onRenameListener = null; private boolean mAutoPushConfiguration = true; private final Account account; - private ServiceDiscoveryResult serviceDiscoveryResult; + private InfoQuery infoQuery; private boolean isOnline = false; private Error error = Error.NONE; private User self; @@ -110,15 +111,24 @@ public class MucOptions { return MessageArchiveService.Version.has(getFeatures()); } - public boolean updateConfiguration(ServiceDiscoveryResult serviceDiscoveryResult) { - this.serviceDiscoveryResult = serviceDiscoveryResult; + private InfoQuery getServiceDiscoveryResult() { + return this.infoQuery; + } + + public boolean updateConfiguration(final InfoQuery serviceDiscoveryResult) { + this.infoQuery = serviceDiscoveryResult; + final var roomInfo = getRoomInfoForm(); String name; - Field roomConfigName = getRoomInfoForm().getFieldByName("muc#roomconfig_roomname"); + Field roomConfigName = + roomInfo == null ? null : roomInfo.getFieldByName("muc#roomconfig_roomname"); if (roomConfigName != null) { name = roomConfigName.getValue(); } else { final var identities = serviceDiscoveryResult.getIdentities(); - final String identityName = !identities.isEmpty() ? identities.get(0).getName() : null; + final String identityName = + !identities.isEmpty() + ? Iterables.getFirst(identities, null).getIdentityName() + : null; final Jid jid = conversation.getJid(); if (identityName != null && !identityName.equals(jid == null ? null : jid.getLocal())) { name = identityName; @@ -140,11 +150,11 @@ public class MucOptions { } private Data getRoomInfoForm() { - final List forms = - serviceDiscoveryResult == null - ? Collections.emptyList() - : serviceDiscoveryResult.forms; - return forms.isEmpty() ? new Data() : forms.get(0); + final var serviceDiscoveryResult = getServiceDiscoveryResult(); + return serviceDiscoveryResult == null + ? null + : serviceDiscoveryResult.getServiceDiscoveryExtension( + "http://jabber.org/protocol/muc#roominfo"); } public String getAvatar() { @@ -152,8 +162,9 @@ public class MucOptions { } public boolean hasFeature(String feature) { - return this.serviceDiscoveryResult != null - && this.serviceDiscoveryResult.features.contains(feature); + final var serviceDiscoveryResult = getServiceDiscoveryResult(); + return serviceDiscoveryResult != null + && serviceDiscoveryResult.getFeatureStrings().contains(feature); } public boolean hasVCards() { @@ -211,9 +222,10 @@ public class MucOptions { return conversation.getBooleanAttribute(Conversation.ATTRIBUTE_MEMBERS_ONLY, false); } - public List getFeatures() { - return this.serviceDiscoveryResult != null - ? this.serviceDiscoveryResult.features + public Collection getFeatures() { + final var serviceDiscoveryResult = getServiceDiscoveryResult(); + return serviceDiscoveryResult != null + ? serviceDiscoveryResult.getFeatureStrings() : Collections.emptyList(); } diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java deleted file mode 100644 index edafd95dedb79daa3ba310298540651107180b47..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/entities/Presence.java +++ /dev/null @@ -1,101 +0,0 @@ -package eu.siacs.conversations.entities; - -import androidx.annotation.NonNull; - -import java.util.Locale; - -import eu.siacs.conversations.xml.Element; - -public class Presence implements Comparable { - - public enum Status { - CHAT, ONLINE, AWAY, XA, DND, OFFLINE; - - public String toShowString() { - switch(this) { - case CHAT: return "chat"; - case AWAY: return "away"; - case XA: return "xa"; - case DND: return "dnd"; - } - return null; - } - - public static Status fromShowString(String show) { - if (show == null) { - return ONLINE; - } else { - switch (show.toLowerCase(Locale.US)) { - case "away": - return AWAY; - case "xa": - return XA; - case "dnd": - return DND; - case "chat": - return CHAT; - default: - return ONLINE; - } - } - } - } - - private final Status status; - private ServiceDiscoveryResult disco; - private final String ver; - private final String hash; - private final String node; - private final String message; - - private Presence(Status status, String ver, String hash, String node, String message) { - this.status = status; - this.ver = ver; - this.hash = hash; - this.node = node; - this.message = message; - } - - public static Presence parse(String show, Element caps, String message) { - final String hash = caps == null ? null : caps.getAttribute("hash"); - final String ver = caps == null ? null : caps.getAttribute("ver"); - final String node = caps == null ? null : caps.getAttribute("node"); - return new Presence(Status.fromShowString(show), ver, hash, node, message); - } - - public int compareTo(@NonNull Presence other) { - return this.status.compareTo(other.status); - } - - public Status getStatus() { - return this.status; - } - - public boolean hasCaps() { - return ver != null && hash != null; - } - - public String getVer() { - return this.ver; - } - - public String getNode() { - return this.node; - } - - public String getHash() { - return this.hash; - } - - public String getMessage() { - return this.message; - } - - public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) { - this.disco = disco; - } - - public ServiceDiscoveryResult getServiceDiscoveryResult() { - return disco; - } -} diff --git a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java index 958891d34d4b8915fdc3309a4827c37b41001347..0c36a42cf994bc84f0ff51559c2c0fb4450b7e95 100644 --- a/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java +++ b/src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Objects; public class PresenceTemplate extends AbstractEntity { @@ -13,9 +14,9 @@ public class PresenceTemplate extends AbstractEntity { private long lastUsed = 0; private String statusMessage; - private Presence.Status status = Presence.Status.ONLINE; + private Presence.Availability status = Presence.Availability.ONLINE; - public PresenceTemplate(Presence.Status status, String statusMessage) { + public PresenceTemplate(Presence.Availability status, String statusMessage) { this.status = status; this.statusMessage = statusMessage; this.lastUsed = System.currentTimeMillis(); @@ -41,11 +42,11 @@ public class PresenceTemplate extends AbstractEntity { template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED)); template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE)); template.status = - Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))); + Presence.Availability.valueOfShown(cursor.getString(cursor.getColumnIndex(STATUS))); return template; } - public Presence.Status getStatus() { + public Presence.Availability getStatus() { return status; } diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index d3bd706f87e42e3263c7693c77a063863468b1b5..98ab79efce3aa9532b9b708abe0c83b041b86377 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -1,15 +1,25 @@ package eu.siacs.conversations.entities; import android.util.Pair; - +import com.google.common.base.Strings; +import com.google.common.collect.Iterables; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.disco.info.Identity; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; import java.util.HashMap; -import java.util.Hashtable; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; public class Presences { - private final Hashtable presences = new Hashtable<>(); + private final HashMap presences = new HashMap<>(); + private final Contact contact; + + public Presences(final Contact contact) { + this.contact = contact; + } private static String nameWithoutVersion(String name) { String[] parts = name.split(" "); @@ -63,18 +73,19 @@ public class Presences { } } - public Presence.Status getShownStatus() { - Presence.Status status = Presence.Status.OFFLINE; + public Presence.Availability getShownStatus() { + Presence.Availability highestAvailability = Presence.Availability.OFFLINE; synchronized (this.presences) { - for (Presence p : presences.values()) { - if (p.getStatus() == Presence.Status.DND) { - return p.getStatus(); - } else if (p.getStatus().compareTo(status) < 0) { - status = p.getStatus(); + for (final Presence p : presences.values()) { + final var availability = p.getAvailability(); + if (availability == Presence.Availability.DND) { + return availability; + } else if (availability.compareTo(highestAvailability) < 0) { + highestAvailability = availability; } } } - return status; + return highestAvailability; } public int size() { @@ -100,10 +111,12 @@ public class Presences { public List asTemplates() { synchronized (this.presences) { ArrayList templates = new ArrayList<>(presences.size()); - for (Presence p : presences.values()) { - if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) { - templates.add(new PresenceTemplate(p.getStatus(), p.getMessage())); + for (Presence presence : this.presences.values()) { + String message = Strings.nullToEmpty(presence.getStatus()).trim(); + if (Strings.isNullOrEmpty(message)) { + continue; } + templates.add(new PresenceTemplate(presence.getAvailability(), message)); } return templates; } @@ -115,24 +128,35 @@ public class Presences { } } - public List getStatusMessages() { - ArrayList messages = new ArrayList<>(); + public Set getStatusMessages() { + Set messages = new HashSet<>(); synchronized (this.presences) { for (Presence presence : this.presences.values()) { - String message = presence.getMessage() == null ? null : presence.getMessage().trim(); - if (message != null && !message.isEmpty() && !messages.contains(message)) { - messages.add(message); + String message = Strings.nullToEmpty(presence.getStatus()).trim(); + if (Strings.isNullOrEmpty(message)) { + continue; } + messages.add(message); } } return messages; } public boolean allOrNonSupport(String namespace) { + final var connection = this.contact.getAccount().getXmppConnection(); + if (connection == null) { + return true; + } synchronized (this.presences) { - for (Presence presence : this.presences.values()) { - ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - if (disco == null || !disco.getFeatures().contains(namespace)) { + for (var resource : this.presences.keySet()) { + final var disco = + connection + .getManager(DiscoManager.class) + .get( + Strings.isNullOrEmpty(resource) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(resource)); + if (disco == null || !disco.getFeatureStrings().contains(namespace)) { return false; } } @@ -140,19 +164,28 @@ public class Presences { return true; } - public Pair, Map> toTypeAndNameMap() { Map typeMap = new HashMap<>(); Map nameMap = new HashMap<>(); + final var connection = this.contact.getAccount().getXmppConnection(); + if (connection == null) { + return new Pair<>(typeMap, nameMap); + } synchronized (this.presences) { - for (Map.Entry presenceEntry : this.presences.entrySet()) { - String resource = presenceEntry.getKey(); - Presence presence = presenceEntry.getValue(); - ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult(); - if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) { - ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0); + for (final String resource : this.presences.keySet()) { + final var serviceDiscoveryResult = + connection + .getManager(DiscoManager.class) + .get( + Strings.isNullOrEmpty(resource) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(resource)); + if (serviceDiscoveryResult != null + && !serviceDiscoveryResult.getIdentities().isEmpty()) { + final Identity identity = + Iterables.getFirst(serviceDiscoveryResult.getIdentities(), null); String type = identity.getType(); - String name = identity.getName(); + String name = identity.getIdentityName(); if (type != null) { typeMap.put(resource, type); } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java deleted file mode 100644 index 3a99f6ca28e5b26872119b6ba188c31e1231232e..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ /dev/null @@ -1,349 +0,0 @@ -package eu.siacs.conversations.entities; - -import android.content.ContentValues; -import android.database.Cursor; -import android.util.Base64; -import com.google.common.base.Strings; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.forms.Data; -import eu.siacs.conversations.xmpp.forms.Field; -import im.conversations.android.xmpp.model.stanza.Iq; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -public class ServiceDiscoveryResult { - public static final String TABLENAME = "discovery_results"; - public static final String HASH = "hash"; - public static final String VER = "ver"; - public static final String RESULT = "result"; - protected final String hash; - protected final byte[] ver; - protected final List features; - protected final List forms; - private final List identities; - - public ServiceDiscoveryResult(final Iq packet) { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = "sha-1"; // We only support sha-1 for now - - final List elements = packet.query().getChildren(); - - for (final Element element : elements) { - if (element.getName().equals("identity")) { - Identity id = new Identity(element); - if (id.getType() != null && id.getCategory() != null) { - identities.add(id); - } - } else if (element.getName().equals("feature")) { - if (element.getAttribute("var") != null) { - features.add(element.getAttribute("var")); - } - } else if (element.getName().equals("x") - && element.getAttribute("xmlns").equals(Namespace.DATA)) { - forms.add(Data.parse(element)); - } - } - this.ver = this.mkCapHash(); - } - - private ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { - this.identities = new ArrayList<>(); - this.features = new ArrayList<>(); - this.forms = new ArrayList<>(); - this.hash = hash; - this.ver = ver; - - JSONArray identities = o.optJSONArray("identities"); - if (identities != null) { - for (int i = 0; i < identities.length(); i++) { - this.identities.add(new Identity(identities.getJSONObject(i))); - } - } - JSONArray features = o.optJSONArray("features"); - if (features != null) { - for (int i = 0; i < features.length(); i++) { - this.features.add(features.getString(i)); - } - } - JSONArray forms = o.optJSONArray("forms"); - if (forms != null) { - for (int i = 0; i < forms.length(); i++) { - this.forms.add(createFormFromJSONObject(forms.getJSONObject(i))); - } - } - } - - private ServiceDiscoveryResult() { - this.hash = "sha-1"; - this.features = Collections.emptyList(); - this.identities = Collections.emptyList(); - this.ver = null; - this.forms = Collections.emptyList(); - } - - public static ServiceDiscoveryResult empty() { - return new ServiceDiscoveryResult(); - } - - public ServiceDiscoveryResult(Cursor cursor) throws JSONException { - this( - cursor.getString(cursor.getColumnIndexOrThrow(HASH)), - Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(VER)), Base64.DEFAULT), - new JSONObject(cursor.getString(cursor.getColumnIndexOrThrow(RESULT)))); - } - - private static String clean(String s) { - return s.replace("<", "<"); - } - - private static String blankNull(String s) { - return s == null ? "" : clean(s); - } - - private static Data createFormFromJSONObject(JSONObject o) { - Data data = new Data(); - JSONArray names = o.names(); - for (int i = 0; i < names.length(); ++i) { - try { - String name = names.getString(i); - JSONArray jsonValues = o.getJSONArray(name); - ArrayList values = new ArrayList<>(jsonValues.length()); - for (int j = 0; j < jsonValues.length(); ++j) { - values.add(jsonValues.getString(j)); - } - data.put(name, values); - } catch (Exception e) { - e.printStackTrace(); - } - } - return data; - } - - private static JSONObject createJSONFromForm(Data data) { - JSONObject object = new JSONObject(); - for (Field field : data.getFields()) { - try { - JSONArray jsonValues = new JSONArray(); - for (String value : field.getValues()) { - jsonValues.put(value); - } - object.put(field.getFieldName(), jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - } - try { - JSONArray jsonValues = new JSONArray(); - jsonValues.put(data.getFormType()); - object.put(Data.FORM_TYPE, jsonValues); - } catch (Exception e) { - e.printStackTrace(); - } - return object; - } - - public String getVer() { - return Base64.encodeToString(this.ver, Base64.NO_WRAP); - } - - public List getIdentities() { - return this.identities; - } - - public List getFeatures() { - return this.features; - } - - public boolean hasIdentity(String category, String type) { - for (Identity id : this.getIdentities()) { - if ((category == null || id.getCategory().equals(category)) - && (type == null || id.getType().equals(type))) { - return true; - } - } - - return false; - } - - public String getExtendedDiscoInformation(final String formType, final String name) { - for (final Data form : this.forms) { - if (formType.equals(form.getFormType())) { - for (final Field field : form.getFields()) { - if (name.equals(field.getFieldName())) { - return field.getValue(); - } - } - } - } - return null; - } - - private byte[] mkCapHash() { - StringBuilder s = new StringBuilder(); - - List identities = this.getIdentities(); - Collections.sort(identities); - - for (Identity id : identities) { - s.append(blankNull(id.getCategory())) - .append("/") - .append(blankNull(id.getType())) - .append("/") - .append(blankNull(id.getLang())) - .append("/") - .append(blankNull(id.getName())) - .append("<"); - } - - final List features = this.getFeatures(); - Collections.sort(features); - for (final String feature : features) { - s.append(clean(feature)).append("<"); - } - - Collections.sort(forms, Comparator.comparing(Data::getFormType)); - for (final Data form : forms) { - s.append(clean(form.getFormType())).append("<"); - final List fields = form.getFields(); - Collections.sort( - fields, Comparator.comparing(lhs -> Strings.nullToEmpty(lhs.getFieldName()))); - for (final Field field : fields) { - s.append(Strings.nullToEmpty(field.getFieldName())).append("<"); - final List values = field.getValues(); - Collections.sort(values, Comparator.comparing(ServiceDiscoveryResult::blankNull)); - for (final String value : values) { - s.append(blankNull(value)).append("<"); - } - } - } - - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - return null; - } - - return md.digest(s.toString().getBytes(StandardCharsets.UTF_8)); - } - - private JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - - JSONArray ids = new JSONArray(); - for (Identity id : this.getIdentities()) { - ids.put(id.toJSON()); - } - o.put("identities", ids); - - o.put("features", new JSONArray(this.getFeatures())); - - JSONArray forms = new JSONArray(); - for (Data data : this.forms) { - forms.put(createJSONFromForm(data)); - } - o.put("forms", forms); - - return o; - } catch (JSONException e) { - return null; - } - } - - public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(HASH, this.hash); - values.put(VER, getVer()); - JSONObject jsonObject = toJSON(); - values.put(RESULT, jsonObject == null ? "" : jsonObject.toString()); - return values; - } - - public static class Identity implements Comparable { - protected final String type; - protected final String lang; - protected final String name; - final String category; - - Identity(final String category, final String type, final String lang, final String name) { - this.category = category; - this.type = type; - this.lang = lang; - this.name = name; - } - - Identity(final Element el) { - this( - el.getAttribute("category"), - el.getAttribute("type"), - el.getAttribute("xml:lang"), - el.getAttribute("name")); - } - - Identity(final JSONObject o) { - - this( - o.optString("category", null), - o.optString("type", null), - o.optString("lang", null), - o.optString("name", null)); - } - - public String getCategory() { - return this.category; - } - - public String getType() { - return this.type; - } - - public String getLang() { - return this.lang; - } - - public String getName() { - return this.name; - } - - JSONObject toJSON() { - try { - JSONObject o = new JSONObject(); - o.put("category", this.getCategory()); - o.put("type", this.getType()); - o.put("lang", this.getLang()); - o.put("name", this.getName()); - return o; - } catch (JSONException e) { - return null; - } - } - - @Override - public int compareTo(final Identity o) { - int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory())); - if (r == 0) { - r = blankNull(this.getType()).compareTo(blankNull(o.getType())); - } - if (r == 0) { - r = blankNull(this.getLang()).compareTo(blankNull(o.getLang())); - } - if (r == 0) { - r = blankNull(this.getName()).compareTo(blankNull(o.getName())); - } - - return r; - } - } -} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index 7bb7341842997906b131073e91a010cad3777b0b..9fca46bd2fca91c8c0d144b0386a8c15ce9888f4 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -1,14 +1,13 @@ package eu.siacs.conversations.generator; import android.text.TextUtils; - import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.stanza.Presence; public class PresenceGenerator extends AbstractGenerator { @@ -16,20 +15,25 @@ public class PresenceGenerator extends AbstractGenerator { super(service); } - private im.conversations.android.xmpp.model.stanza.Presence subscription(String type, Contact contact) { - im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); + private im.conversations.android.xmpp.model.stanza.Presence subscription( + String type, Contact contact) { + im.conversations.android.xmpp.model.stanza.Presence packet = + new im.conversations.android.xmpp.model.stanza.Presence(); packet.setAttribute("type", type); packet.setTo(contact.getJid()); packet.setFrom(contact.getAccount().getJid().asBareJid()); return packet; } - public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom( + final Contact contact) { return requestPresenceUpdatesFrom(contact, null); } - public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom(final Contact contact, final String preAuth) { - im.conversations.android.xmpp.model.stanza.Presence packet = subscription("subscribe", contact); + public im.conversations.android.xmpp.model.stanza.Presence requestPresenceUpdatesFrom( + final Contact contact, final String preAuth) { + im.conversations.android.xmpp.model.stanza.Presence packet = + subscription("subscribe", contact); String displayName = contact.getAccount().getDisplayName(); if (!TextUtils.isEmpty(displayName)) { packet.addChild("nick", Namespace.NICK).setContent(displayName); @@ -40,41 +44,42 @@ public class PresenceGenerator extends AbstractGenerator { return packet; } - public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesFrom( + Contact contact) { return subscription("unsubscribe", contact); } - public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence stopPresenceUpdatesTo( + Contact contact) { return subscription("unsubscribed", contact); } - public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo(Contact contact) { + public im.conversations.android.xmpp.model.stanza.Presence sendPresenceUpdatesTo( + Contact contact) { return subscription("subscribed", contact); } - public im.conversations.android.xmpp.model.stanza.Presence selfPresence(Account account, Presence.Status status) { + public im.conversations.android.xmpp.model.stanza.Presence selfPresence( + Account account, Presence.Availability status) { return selfPresence(account, status, true); } - public im.conversations.android.xmpp.model.stanza.Presence selfPresence(final Account account, final Presence.Status status, final boolean personal) { - final im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); + public im.conversations.android.xmpp.model.stanza.Presence selfPresence( + final Account account, final Presence.Availability status, final boolean personal) { + final im.conversations.android.xmpp.model.stanza.Presence packet = + new im.conversations.android.xmpp.model.stanza.Presence(); if (personal) { final String sig = account.getPgpSignature(); final String message = account.getPresenceStatusMessage(); - if (status.toShowString() != null) { - packet.addChild("show").setContent(status.toShowString()); - } - if (!TextUtils.isEmpty(message)) { - packet.addChild(new Element("status").setContent(message)); - } + packet.setAvailability(status); + packet.setStatus(message); if (sig != null && mXmppConnectionService.getPgpEngine() != null) { packet.addChild("x", "jabber:x:signed").setContent(sig); } } final String capHash = getCapHash(account); if (capHash != null) { - Element cap = packet.addChild("c", - "http://jabber.org/protocol/caps"); + Element cap = packet.addChild("c", "http://jabber.org/protocol/caps"); cap.setAttribute("hash", "sha-1"); cap.setAttribute("node", "http://conversations.im"); cap.setAttribute("ver", capHash); @@ -83,15 +88,18 @@ public class PresenceGenerator extends AbstractGenerator { } public im.conversations.android.xmpp.model.stanza.Presence leave(final MucOptions mucOptions) { - im.conversations.android.xmpp.model.stanza.Presence presence = new im.conversations.android.xmpp.model.stanza.Presence(); + im.conversations.android.xmpp.model.stanza.Presence presence = + new im.conversations.android.xmpp.model.stanza.Presence(); presence.setTo(mucOptions.getSelf().getFullJid()); presence.setFrom(mucOptions.getAccount().getJid()); presence.setAttribute("type", "unavailable"); return presence; } - public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence(Account account) { - im.conversations.android.xmpp.model.stanza.Presence packet = new im.conversations.android.xmpp.model.stanza.Presence(); + public im.conversations.android.xmpp.model.stanza.Presence sendOfflinePresence( + Account account) { + im.conversations.android.xmpp.model.stanza.Presence packet = + new im.conversations.android.xmpp.model.stanza.Presence(); packet.setFrom(account.getJid()); packet.setAttribute("type", "unavailable"); return packet; diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 244da8e40807e568c07f9427c16e4e8a2708dd91..d3c269f965ee0dfdc6cf864178cb1a78f4a892dc 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -1,7 +1,12 @@ package eu.siacs.conversations.parser; import android.util.Log; +import androidx.annotation.NonNull; import com.google.common.base.Strings; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -10,7 +15,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.services.XmppConnectionService; @@ -18,10 +22,13 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.occupant.OccupantId; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import org.openintents.openpgp.util.OpenPgpUtils; @@ -359,13 +366,17 @@ public class PresenceParser extends AbstractParser final int sizeBefore = contact.getPresences().size(); - final String show = packet.findChildContent("show"); - final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); - final String message = packet.findChildContent("status"); - final Presence presence = Presence.parse(show, caps, message); - contact.updatePresence(resource, presence); - if (presence.hasCaps()) { - mXmppConnectionService.fetchCaps(account, from, presence); + contact.updatePresence(resource, packet); + + final var nodeHash = packet.getCapabilities(); + final var connection = account.getXmppConnection(); + if (nodeHash != null && connection != null) { + final var discoFuture = + connection + .getManager(DiscoManager.class) + .infoOrCache(Entity.presence(from), nodeHash.node, nodeHash.hash); + + logDiscoFailure(from, discoFuture); } final Element idle = packet.findChild("idle", Namespace.IDLE); @@ -412,7 +423,8 @@ public class PresenceParser extends AbstractParser } else { contact.removePresence(from.getResource()); } - if (contact.getShownStatus() == Presence.Status.OFFLINE) { + if (contact.getShownStatus() + == im.conversations.android.xmpp.model.stanza.Presence.Availability.OFFLINE) { contact.flagInactive(); } mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false); @@ -453,6 +465,24 @@ public class PresenceParser extends AbstractParser mXmppConnectionService.updateRosterUi(); } + private static void logDiscoFailure(final Jid from, ListenableFuture discoFuture) { + Futures.addCallback( + discoFuture, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) {} + + @Override + public void onFailure(@NonNull Throwable throwable) { + if (throwable instanceof TimeoutException) { + return; + } + Log.d(Config.LOGTAG, "could not retrieve disco from " + from, throwable); + } + }, + MoreExecutors.directExecutor()); + } + @Override public void accept(final im.conversations.android.xmpp.model.stanza.Presence packet) { if (packet.hasChild("x", Namespace.MUC_USER)) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e4787999c275e3fd7e546e70d3e0eccb081fd630..3673e6dcf6585a329b4d66ecfb98d36b469a8f06 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -21,7 +21,6 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.Roster; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.ShortcutService; import eu.siacs.conversations.utils.CryptoHelper; @@ -31,9 +30,14 @@ import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.mam.MamReference; +import im.conversations.android.xml.XmlElementReader; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -46,7 +50,6 @@ import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; -import org.json.JSONException; import org.json.JSONObject; import org.jxmpp.jid.parts.Localpart; import org.jxmpp.stringprep.XmppStringprepException; @@ -61,11 +64,11 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 53; + private static final int DATABASE_VERSION = 54; private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; - private static final String CREATE_CONTATCS_STATEMENT = + private static final String CREATE_CONTACTS_STATEMENT = "create table " + Contact.TABLENAME + "(" @@ -108,22 +111,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Contact.JID + ") ON CONFLICT REPLACE);"; - private static final String CREATE_DISCOVERY_RESULTS_STATEMENT = - "create table " - + ServiceDiscoveryResult.TABLENAME - + "(" - + ServiceDiscoveryResult.HASH - + " TEXT, " - + ServiceDiscoveryResult.VER - + " TEXT, " - + ServiceDiscoveryResult.RESULT - + " TEXT, " - + "UNIQUE(" - + ServiceDiscoveryResult.HASH - + ", " - + ServiceDiscoveryResult.VER - + ") ON CONFLICT REPLACE);"; - private static final String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE " + PresenceTemplate.TABELNAME @@ -252,6 +239,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON CONFLICT IGNORE" + ");"; + private static final String CREATE_CAPS_CACHE_TABLE = + "CREATE TABLE caps_cache (caps TEXT, caps2 TEXT, disco_info TEXT, UNIQUE (caps), UNIQUE" + + " (caps2));"; + private static final String CREATE_CAPS_CACHE_INDEX_CAPS = + "CREATE INDEX idx_caps ON caps_cache(caps);"; + private static final String CREATE_CAPS_CACHE_INDEX_CAPS2 = + "CREATE INDEX idx_caps2 ON caps_cache(caps2);"; + private static final String RESOLVER_RESULTS_TABLENAME = "resolver_results"; private static final String CREATE_RESOLVER_RESULTS_TABLE = @@ -495,8 +490,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_MESSAGE_DELETED_INDEX); db.execSQL(CREATE_MESSAGE_RELATIVE_FILE_PATH_INDEX); db.execSQL(CREATE_MESSAGE_TYPE_INDEX); - db.execSQL(CREATE_CONTATCS_STATEMENT); - db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); + db.execSQL(CREATE_CONTACTS_STATEMENT); db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); @@ -507,6 +501,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER); db.execSQL(CREATE_MESSAGE_UPDATE_TRIGGER); db.execSQL(CREATE_MESSAGE_DELETE_TRIGGER); + db.execSQL(CREATE_CAPS_CACHE_TABLE); + db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS); + db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS2); } @Override @@ -527,7 +524,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 5 && newVersion >= 5) { db.execSQL("DROP TABLE " + Contact.TABLENAME); - db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL(CREATE_CONTACTS_STATEMENT); db.execSQL("UPDATE " + Account.TABLENAME + " SET " + Account.ROSTERVERSION + " = NULL"); } if (oldVersion < 6 && newVersion >= 6) { @@ -727,10 +724,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { + SQLiteAxolotlStore.CERTIFICATE); } - if (oldVersion < 23 && newVersion >= 23) { - db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); - } - if (oldVersion < 24 && newVersion >= 24) { db.execSQL( "ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); @@ -745,10 +738,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT); } - if (oldVersion < 27 && newVersion >= 27) { - db.execSQL("DELETE FROM " + ServiceDiscoveryResult.TABLENAME); - } - if (oldVersion < 28 && newVersion >= 28) { canonicalizeJids(db); } @@ -1086,6 +1075,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { } } } + if (oldVersion < 54 && newVersion >= 54) { + db.execSQL("DROP TABLE discovery_results"); + db.execSQL(CREATE_CAPS_CACHE_TABLE); + db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS); + db.execSQL(CREATE_CAPS_CACHE_INDEX_CAPS2); + } } private void canonicalizeJids(SQLiteDatabase db) { @@ -1224,40 +1219,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.insert(Account.TABLENAME, null, account.getContentValues()); } - public void insertDiscoveryResult(ServiceDiscoveryResult result) { - SQLiteDatabase db = this.getWritableDatabase(); - db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues()); - } - - public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) { - SQLiteDatabase db = this.getReadableDatabase(); - String[] selectionArgs = {hash, ver}; - Cursor cursor = - db.query( - ServiceDiscoveryResult.TABLENAME, - null, - ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", - selectionArgs, - null, - null, - null); - if (cursor.getCount() == 0) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - - ServiceDiscoveryResult result = null; - try { - result = new ServiceDiscoveryResult(cursor); - } catch (JSONException e) { - /* result is still null */ - } - - cursor.close(); - return result; - } - public void saveResolverResult(String domain, Resolver.Result result) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues contentValues = result.toContentValues(); @@ -1612,6 +1573,60 @@ public class DatabaseBackend extends SQLiteOpenHelper { return message; } + public void insertCapsCache( + EntityCapabilities.EntityCapsHash caps, + EntityCapabilities2.EntityCaps2Hash caps2, + InfoQuery infoQuery) { + final var contentValues = new ContentValues(); + contentValues.put("caps", caps.encoded()); + contentValues.put("caps2", caps2.encoded()); + contentValues.put("disco_info", infoQuery.toString()); + getWritableDatabase() + .insertWithOnConflict( + "caps_cache", null, contentValues, SQLiteDatabase.CONFLICT_REPLACE); + } + + public InfoQuery getInfoQuery(final EntityCapabilities.Hash hash) { + final String selection; + final String[] args; + if (hash instanceof EntityCapabilities.EntityCapsHash) { + selection = "caps=?"; + args = new String[] {hash.encoded()}; + } else if (hash instanceof EntityCapabilities2.EntityCaps2Hash) { + selection = "caps2=?"; + args = new String[] {hash.encoded()}; + } else { + return null; + } + try (final Cursor cursor = + getReadableDatabase() + .query( + "caps_cache", + new String[] {"disco_info"}, + selection, + args, + null, + null, + null)) { + if (cursor.moveToFirst()) { + final var cached = cursor.getString(0); + try { + final var element = + XmlElementReader.read(cached.getBytes(StandardCharsets.UTF_8)); + if (element instanceof InfoQuery infoQuery) { + return infoQuery; + } + } catch (final IOException e) { + Log.e(Config.LOGTAG, "could not restore info query from cache", e); + return null; + } + } else { + return null; + } + } + return null; + } + public static class FilePath { public final UUID uuid; public final String path; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 11580e20fa4442cb5ffc9ec0c0c3ffa7d19d9abc..502b51ef89b55bedd2386954cda5f4530f908ad4 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -19,6 +19,7 @@ import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.stanza.Message; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -55,7 +56,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - private static Version get(List features) { + private static Version get(final Collection features) { final Version[] values = values(); for (int i = values.length - 1; i >= 0; --i) { for (String feature : features) { @@ -67,7 +68,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return MAM_0; } - public static boolean has(List features) { + public static boolean has(final Collection features) { for (String feature : features) { for (Version version : values()) { if (version.namespace.equals(feature)) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ff38d2ed57d57de0d58ef928beba1465e282182e..5ec73cc419c4ecf4064aa1a9bc789f97ea05afe9 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -81,11 +81,8 @@ import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.entities.Reaction; -import eu.siacs.conversations.entities.Roster; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.AbstractGenerator; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.MessageGenerator; @@ -125,6 +122,7 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.LocalizedContent; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.IqErrorResponseException; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; @@ -140,10 +138,13 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.mam.MamReference; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.PublishOptions; +import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.avatar.Metadata; import im.conversations.android.xmpp.model.bookmark.Storage; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.mds.Displayed; import im.conversations.android.xmpp.model.pubsub.PubSub; import im.conversations.android.xmpp.model.stanza.Iq; @@ -358,8 +359,6 @@ public class XmppConnectionService extends Service { public final Set FILENAMES_TO_IGNORE_DELETION = new HashSet<>(); private final AtomicLong mLastExpiryRun = new AtomicLong(0); - private final LruCache, ServiceDiscoveryResult> discoCache = - new LruCache<>(20); private final OnStatusChanged statusListener = new OnStatusChanged() { @@ -1268,13 +1267,13 @@ public class XmppConnectionService extends Service { getResources().getString(R.string.picture_compression)); } - private Presence.Status getTargetPresence() { + private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() { if (dndOnSilentMode() && isPhoneSilenced()) { - return Presence.Status.DND; + return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND; } else if (awayWhenScreenLocked() && isScreenLocked()) { - return Presence.Status.AWAY; + return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY; } else { - return Presence.Status.ONLINE; + return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE; } } @@ -3721,7 +3720,8 @@ public class XmppConnectionService extends Service { final var packet = mPresenceGenerator.selfPresence( account, - Presence.Status.ONLINE, + im.conversations.android.xmpp.model.stanza.Presence + .Availability.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null); packet.setTo(joinJid); @@ -4117,7 +4117,9 @@ public class XmppConnectionService extends Service { final var packet = mPresenceGenerator.selfPresence( - account, Presence.Status.ONLINE, options.nonanonymous()); + account, + im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE, + options.nonanonymous()); packet.setTo(joinJid); sendPresencePacket(account, packet); if (nick.equals(MucOptions.defaultNick(account)) @@ -4174,7 +4176,9 @@ public class XmppConnectionService extends Service { account.getJid().asBareJid(), joinJid, current)); final var packet = mPresenceGenerator.selfPresence( - account, Presence.Status.ONLINE, options.nonanonymous()); + account, + im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE, + options.nonanonymous()); packet.setTo(joinJid); sendPresencePacket(account, packet); } @@ -4367,11 +4371,19 @@ public class XmppConnectionService extends Service { final Conversation conversation, final OnConferenceConfigurationFetched callback) { final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid()); final var account = conversation.getAccount(); - sendIqPacket( - account, - request, - response -> { - if (response.getType() == Iq.Type.RESULT) { + final var connection = account.getXmppConnection(); + if (connection == null) { + return; + } + final var future = + connection + .getManager(DiscoManager.class) + .info(Entity.discoItem(conversation.getJid().asBareJid()), null); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(InfoQuery result) { final MucOptions mucOptions = conversation.getMucOptions(); final Bookmark bookmark = conversation.getBookmark(); final boolean sameBefore = @@ -4380,7 +4392,7 @@ public class XmppConnectionService extends Service { mucOptions.getName()); final var hadOccupantId = mucOptions.occupantId(); - if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) { + if (mucOptions.updateConfiguration(result)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -4402,7 +4414,8 @@ public class XmppConnectionService extends Service { final var packet = mPresenceGenerator.selfPresence( account, - Presence.Status.ONLINE, + im.conversations.android.xmpp.model.stanza.Presence + .Availability.ONLINE, mucOptions.nonanonymous()); packet.setTo(me); sendPresencePacket(account, packet); @@ -4421,18 +4434,27 @@ public class XmppConnectionService extends Service { } updateConversationUi(); - } else if (response.getType() == Iq.Type.TIMEOUT) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": received timeout waiting for conference configuration" - + " fetch"); - } else { - if (callback != null) { - callback.onFetchFailed(conversation, response.getErrorCondition()); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + if (throwable instanceof TimeoutException) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": received timeout waiting for conference" + + " configuration fetch"); + } else if (throwable + instanceof IqErrorResponseException errorResponseException) { + if (callback != null) { + callback.onFetchFailed( + conversation, + errorResponseException.getResponse().getErrorCondition()); + } } } - }); + }, + MoreExecutors.directExecutor()); } public void pushNodeConfiguration( @@ -5270,7 +5292,10 @@ public class XmppConnectionService extends Service { if (mucOptions.online()) { final var packet = mPresenceGenerator.selfPresence( - account, Presence.Status.ONLINE, mucOptions.nonanonymous()); + account, + im.conversations.android.xmpp.model.stanza.Presence + .Availability.ONLINE, + mucOptions.nonanonymous()); packet.setTo(mucOptions.getSelf().getFullJid()); connection.sendPresencePacket(packet); } @@ -5982,7 +6007,7 @@ public class XmppConnectionService extends Service { } private void sendPresence(final Account account, final boolean includeIdleTimestamp) { - final Presence.Status status; + final im.conversations.android.xmpp.model.stanza.Presence.Availability status; if (manuallyChangePresence()) { status = account.getPresenceStatus(); } else { @@ -6217,100 +6242,6 @@ public class XmppConnectionService extends Service { }); } - public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair key) { - ServiceDiscoveryResult result = discoCache.get(key); - if (result != null) { - return result; - } else { - result = databaseBackend.findDiscoveryResult(key.first, key.second); - if (result != null) { - discoCache.put(key, result); - } - return result; - } - } - - public void fetchCaps(final Account account, final Jid jid, final Presence presence) { - final Pair key = new Pair<>(presence.getHash(), presence.getVer()); - final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); - if (disco != null) { - presence.setServiceDiscoveryResult(disco); - final Contact contact = account.getRoster().getContact(jid); - if (contact.refreshRtpCapability()) { - syncRoster(account); - } - } else { - final Iq request = new Iq(Iq.Type.GET); - request.setTo(jid); - final String node = presence.getNode(); - final String ver = presence.getVer(); - final Element query = request.query(Namespace.DISCO_INFO); - if (node != null && ver != null) { - query.setAttribute("node", node + "#" + ver); - } - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": making disco request for " - + key.second - + " to " - + jid); - sendIqPacket( - account, - request, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final ServiceDiscoveryResult discoveryResult = - new ServiceDiscoveryResult(response); - if (presence.getVer().equals(discoveryResult.getVer())) { - databaseBackend.insertDiscoveryResult(discoveryResult); - injectServiceDiscoveryResult( - account.getRoster(), - presence.getHash(), - presence.getVer(), - discoveryResult); - } 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); - } - }); - } - } - - private void injectServiceDiscoveryResult( - Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { - boolean rosterNeedsSync = false; - for (final Contact contact : roster.getContacts()) { - boolean serviceDiscoverySet = false; - for (final Presence presence : contact.getPresences().getPresences()) { - if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) { - presence.setServiceDiscoveryResult(disco); - serviceDiscoverySet = true; - } - } - if (serviceDiscoverySet) { - rosterNeedsSync |= contact.refreshRtpCapability(); - } - } - if (rosterNeedsSync) { - syncRoster(roster.getAccount()); - } - } - public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) { final MessageArchiveService.Version version = MessageArchiveService.Version.get(account); final Iq request = new Iq(Iq.Type.GET); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index a21fd5360e77f618d75e1a80f87eb45f1609e57e..039c26e47fc303e7f4fbea9b7b8e676df7b92261 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -32,7 +32,9 @@ import androidx.core.view.ViewCompat; import androidx.databinding.DataBindingUtil; import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; @@ -44,7 +46,6 @@ import eu.siacs.conversations.databinding.ActivityContactDetailsBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.AbstractQuickConversationsService; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; @@ -69,6 +70,7 @@ 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 im.conversations.android.xmpp.model.stanza.Presence; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -424,11 +426,11 @@ public class ContactDetailsActivity extends OmemoActivity binding.detailsSendPresence.setOnCheckedChangeListener(null); binding.detailsReceivePresence.setOnCheckedChangeListener(null); - List statusMessages = contact.getPresences().getStatusMessages(); - if (statusMessages.size() == 0) { + Collection statusMessages = contact.getPresences().getStatusMessages(); + if (statusMessages.isEmpty()) { binding.statusMessage.setVisibility(View.GONE); } else if (statusMessages.size() == 1) { - final String message = statusMessages.get(0); + final String message = Iterables.getOnlyElement(statusMessages); binding.statusMessage.setVisibility(View.VISIBLE); final Spannable span = new SpannableString(message); if (Emoticons.isOnlyEmoji(message)) { @@ -440,16 +442,7 @@ public class ContactDetailsActivity extends OmemoActivity } binding.statusMessage.setText(span); } else { - StringBuilder builder = new StringBuilder(); - binding.statusMessage.setVisibility(View.VISIBLE); - int s = statusMessages.size(); - for (int i = 0; i < s; ++i) { - builder.append(statusMessages.get(i)); - if (i < s - 1) { - builder.append("\n"); - } - } - binding.statusMessage.setText(builder); + binding.statusMessage.setText(Joiner.on('\n').join(statusMessages)); } if (contact.getOption(Contact.Options.FROM)) { @@ -592,7 +585,7 @@ public class ContactDetailsActivity extends OmemoActivity final List tagList = contact.getTags(this); final boolean hasMetaTags = - contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE; + contact.isBlocked() || contact.getShownStatus() != Presence.Availability.OFFLINE; if ((tagList.isEmpty() && !hasMetaTags) || !this.showDynamicTags) { binding.tags.setVisibility(View.GONE); } else { @@ -628,8 +621,8 @@ public class ContactDetailsActivity extends OmemoActivity viewIdBuilder.add(id); binding.tags.addView(tv); } else { - final Presence.Status status = contact.getShownStatus(); - if (status != Presence.Status.OFFLINE) { + final Presence.Availability status = contact.getShownStatus(); + if (status != Presence.Availability.OFFLINE) { final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, binding.tags, false); UIHelper.setStatus(tv, status); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 459ef2f0be6248eab923febd321012cefe683722..bf87e21271952bc9f7b145836e91f7895e8ad84b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -81,7 +81,6 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.User; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.ReadByMarker; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; @@ -128,6 +127,7 @@ 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 im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -2938,7 +2938,7 @@ public class ConversationFragment extends XmppFragment boolean hasAttachments = mediaPreviewAdapter != null && mediaPreviewAdapter.hasAttachments(); final Conversation c = this.conversation; - final Presence.Status status; + final Presence.Availability status; final String text = this.binding.textinput == null ? "" : this.binding.textinput.getText().toString(); final SendButtonAction action; @@ -2951,17 +2951,17 @@ public class ConversationFragment extends XmppFragment if (activity != null && activity.xmppConnectionService != null && activity.xmppConnectionService.getMessageArchiveService().isCatchingUp(c)) { - status = Presence.Status.OFFLINE; + status = Presence.Availability.OFFLINE; } else if (c.getMode() == Conversation.MODE_SINGLE) { status = c.getContact().getShownStatus(); } else { status = c.getMucOptions().online() - ? Presence.Status.ONLINE - : Presence.Status.OFFLINE; + ? Presence.Availability.ONLINE + : Presence.Availability.OFFLINE; } } else { - status = Presence.Status.OFFLINE; + status = Presence.Availability.OFFLINE; } this.binding.textSendButton.setTag(action); this.binding.textSendButton.setIconResource( diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 02e3963b966be2843d45b597db96f066c4ec2e06..494e75dd257ed6e04d955b20044067c0c02b280c 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -51,7 +51,6 @@ import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.databinding.ActivityEditAccountBinding; import eu.siacs.conversations.databinding.DialogPresenceBinding; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.PresenceTemplate; import eu.siacs.conversations.services.BarcodeProvider; import eu.siacs.conversations.services.QuickConversationsService; @@ -80,6 +79,7 @@ import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.pep.Avatar; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Arrays; import java.util.List; import java.util.Set; @@ -411,7 +411,7 @@ public class EditAccountActivity extends OmemoActivity }; private static void setAvailabilityRadioButton( - Presence.Status status, DialogPresenceBinding binding) { + Presence.Availability status, DialogPresenceBinding binding) { if (status == null) { binding.online.setChecked(true); return; @@ -431,15 +431,15 @@ public class EditAccountActivity extends OmemoActivity } } - private static Presence.Status getAvailabilityRadioButton(DialogPresenceBinding binding) { + private static Presence.Availability getAvailabilityRadioButton(DialogPresenceBinding binding) { if (binding.dnd.isChecked()) { - return Presence.Status.DND; + return Presence.Availability.DND; } else if (binding.xa.isChecked()) { - return Presence.Status.XA; + return Presence.Availability.XA; } else if (binding.away.isChecked()) { - return Presence.Status.AWAY; + return Presence.Availability.AWAY; } else { - return Presence.Status.ONLINE; + return Presence.Availability.ONLINE; } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index a2ec5fe949feecda5381f086e489acc5fda5c969..9adb4e7bec4c8fb430916c548344386cd8c4352f 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -68,7 +68,6 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; @@ -84,6 +83,7 @@ import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -1152,12 +1152,12 @@ public class StartConversationActivity extends XmppActivity for (final Account account : accounts) { if (account.isEnabled()) { for (Contact contact : account.getRoster().getContacts()) { - Presence.Status s = contact.getShownStatus(); + Presence.Availability s = contact.getShownStatus(); if (contact.showInContactList() && contact.match(this, needle) && (!this.mHideOfflineContacts || (needle != null && !needle.trim().isEmpty()) - || s.compareTo(Presence.Status.OFFLINE) < 0)) { + || s.compareTo(Presence.Availability.OFFLINE) < 0)) { this.contacts.add(contact); } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 290c52079d9a39476b56d63eb1bfb29f5fc72981..577e66b42c930d9a795f1f1e74108edf6e161fc3 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -10,169 +10,170 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; - import androidx.annotation.NonNull; import androidx.constraintlayout.helper.widget.Flow; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.view.ViewCompat; import androidx.databinding.DataBindingUtil; - import com.google.android.material.color.MaterialColors; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Ints; - import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ItemContactBinding; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XEP0392Helper; import eu.siacs.conversations.xmpp.Jid; - +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.List; public class ListItemAdapter extends ArrayAdapter { - protected XmppActivity activity; - private boolean showDynamicTags = false; - private OnTagClickedListener mOnTagClickedListener = null; - private final View.OnClickListener onTagTvClick = view -> { - if (view instanceof TextView tv && mOnTagClickedListener != null) { - final String tag = tv.getText().toString(); - mOnTagClickedListener.onTagClicked(tag); - } - }; - - public ListItemAdapter(XmppActivity activity, List objects) { - super(activity, 0, objects); - this.activity = activity; - } - - - public void refreshSettings() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); - this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false); - } - - @NonNull - @Override - public View getView(int position, View view, @NonNull ViewGroup parent) { - LayoutInflater inflater = activity.getLayoutInflater(); - final ListItem item = getItem(position); - ViewHolder viewHolder; - if (view == null) { - final ItemContactBinding binding = DataBindingUtil.inflate(inflater,R.layout.item_contact,parent,false); - viewHolder = ViewHolder.get(binding); - view = binding.getRoot(); - } else { - viewHolder = (ViewHolder) view.getTag(); - } - if (view.isActivated()) { - Log.d(Config.LOGTAG,"item "+item.getDisplayName()+" is activated"); - } - //view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background)); - final List tags = item.getTags(activity); - final boolean hasMetaTags; - if (item instanceof Contact contact) { - hasMetaTags = contact.isBlocked() || contact.getShownStatus() != Presence.Status.OFFLINE; - } else { - hasMetaTags = false; - } - if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) { - viewHolder.tags.setVisibility(View.GONE); - } else { - viewHolder.tags.setVisibility(View.VISIBLE); - viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1); - final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); - for (final ListItem.Tag tag : tags) { - final String name = tag.getName(); - final TextView tv = (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); - tv.setText(name); - tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(getContext(),XEP0392Helper.rgbFromNick(name)))); - tv.setOnClickListener(this.onTagTvClick); - final int id = ViewCompat.generateViewId(); - tv.setId(id); - viewIdBuilder.add(id); - viewHolder.tags.addView(tv); - } - if (item instanceof Contact contact) { - if (contact.isBlocked()) { - final TextView tv = - (TextView) - inflater.inflate( - R.layout.item_tag, viewHolder.tags, false); - tv.setText(R.string.blocked); - tv.setBackgroundTintList(ColorStateList.valueOf(MaterialColors.harmonizeWithPrimary(tv.getContext(),ContextCompat.getColor(tv.getContext(),R.color.gray_800)))); - final int id = ViewCompat.generateViewId(); - tv.setId(id); - viewIdBuilder.add(id); - viewHolder.tags.addView(tv); - } else { - final Presence.Status status = contact.getShownStatus(); - if (status != Presence.Status.OFFLINE) { + protected XmppActivity activity; + private boolean showDynamicTags = false; + private OnTagClickedListener mOnTagClickedListener = null; + private final View.OnClickListener onTagTvClick = + view -> { + if (view instanceof TextView tv && mOnTagClickedListener != null) { + final String tag = tv.getText().toString(); + mOnTagClickedListener.onTagClicked(tag); + } + }; + + public ListItemAdapter(XmppActivity activity, List objects) { + super(activity, 0, objects); + this.activity = activity; + } + + public void refreshSettings() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity); + this.showDynamicTags = preferences.getBoolean(AppSettings.SHOW_DYNAMIC_TAGS, false); + } + + @NonNull + @Override + public View getView(int position, View view, @NonNull ViewGroup parent) { + LayoutInflater inflater = activity.getLayoutInflater(); + final ListItem item = getItem(position); + ViewHolder viewHolder; + if (view == null) { + final ItemContactBinding binding = + DataBindingUtil.inflate(inflater, R.layout.item_contact, parent, false); + viewHolder = ViewHolder.get(binding); + view = binding.getRoot(); + } else { + viewHolder = (ViewHolder) view.getTag(); + } + if (view.isActivated()) { + Log.d(Config.LOGTAG, "item " + item.getDisplayName() + " is activated"); + } + // view.setBackground(StyledAttributes.getDrawable(view.getContext(),R.attr.list_item_background)); + final List tags = item.getTags(activity); + final boolean hasMetaTags; + if (item instanceof Contact contact) { + hasMetaTags = + contact.isBlocked() + || contact.getShownStatus() != Presence.Availability.OFFLINE; + } else { + hasMetaTags = false; + } + if ((tags.isEmpty() && !hasMetaTags) || !this.showDynamicTags) { + viewHolder.tags.setVisibility(View.GONE); + } else { + viewHolder.tags.setVisibility(View.VISIBLE); + viewHolder.tags.removeViews(1, viewHolder.tags.getChildCount() - 1); + final ImmutableList.Builder viewIdBuilder = new ImmutableList.Builder<>(); + for (final ListItem.Tag tag : tags) { + final String name = tag.getName(); + final TextView tv = + (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); + tv.setText(name); + tv.setBackgroundTintList( + ColorStateList.valueOf( + MaterialColors.harmonizeWithPrimary( + getContext(), XEP0392Helper.rgbFromNick(name)))); + tv.setOnClickListener(this.onTagTvClick); + final int id = ViewCompat.generateViewId(); + tv.setId(id); + viewIdBuilder.add(id); + viewHolder.tags.addView(tv); + } + if (item instanceof Contact contact) { + if (contact.isBlocked()) { + final TextView tv = + (TextView) inflater.inflate(R.layout.item_tag, viewHolder.tags, false); + tv.setText(R.string.blocked); + tv.setBackgroundTintList( + ColorStateList.valueOf( + MaterialColors.harmonizeWithPrimary( + tv.getContext(), + ContextCompat.getColor( + tv.getContext(), R.color.gray_800)))); + final int id = ViewCompat.generateViewId(); + tv.setId(id); + viewIdBuilder.add(id); + viewHolder.tags.addView(tv); + } else { + final Presence.Availability status = contact.getShownStatus(); + if (status != Presence.Availability.OFFLINE) { final TextView tv = (TextView) - inflater.inflate( - R.layout.item_tag, viewHolder.tags, false); - UIHelper.setStatus(tv, status); - final int id = ViewCompat.generateViewId(); - tv.setId(id); - viewIdBuilder.add(id); - viewHolder.tags.addView(tv); + inflater.inflate(R.layout.item_tag, viewHolder.tags, false); + UIHelper.setStatus(tv, status); + final int id = ViewCompat.generateViewId(); + tv.setId(id); + viewIdBuilder.add(id); + viewHolder.tags.addView(tv); } } - } - viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build())); - } - final Jid jid = item.getJid(); - if (jid != null) { - viewHolder.jid.setVisibility(View.VISIBLE); - viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid)); - } else { - viewHolder.jid.setVisibility(View.GONE); - } - viewHolder.name.setText(item.getDisplayName()); - AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); - return view; - } - - public void setOnTagClickedListener(OnTagClickedListener listener) { - this.mOnTagClickedListener = listener; - } - - - public interface OnTagClickedListener { - void onTagClicked(String tag); - } - - private static class ViewHolder { - private TextView name; - private TextView jid; - private ImageView avatar; - private ConstraintLayout tags; - private Flow flowWidget; - - private ViewHolder() { - - } - - public static ViewHolder get(final ItemContactBinding binding) { - ViewHolder viewHolder = new ViewHolder(); - viewHolder.name = binding.contactDisplayName; - viewHolder.jid = binding.contactJid; - viewHolder.avatar = binding.contactPhoto; - viewHolder.tags = binding.tags; - viewHolder.flowWidget = binding.flowWidget; - binding.getRoot().setTag(viewHolder); - return viewHolder; - } - } - + } + viewHolder.flowWidget.setReferencedIds(Ints.toArray(viewIdBuilder.build())); + } + final Jid jid = item.getJid(); + if (jid != null) { + viewHolder.jid.setVisibility(View.VISIBLE); + viewHolder.jid.setText(IrregularUnicodeDetector.style(activity, jid)); + } else { + viewHolder.jid.setVisibility(View.GONE); + } + viewHolder.name.setText(item.getDisplayName()); + AvatarWorkerTask.loadAvatar(item, viewHolder.avatar, R.dimen.avatar); + return view; + } + + public void setOnTagClickedListener(OnTagClickedListener listener) { + this.mOnTagClickedListener = listener; + } + + public interface OnTagClickedListener { + void onTagClicked(String tag); + } + + private static class ViewHolder { + private TextView name; + private TextView jid; + private ImageView avatar; + private ConstraintLayout tags; + private Flow flowWidget; + + private ViewHolder() {} + + public static ViewHolder get(final ItemContactBinding binding) { + ViewHolder viewHolder = new ViewHolder(); + viewHolder.name = binding.contactDisplayName; + viewHolder.jid = binding.contactJid; + viewHolder.avatar = binding.contactPhoto; + viewHolder.tags = binding.tags; + viewHolder.flowWidget = binding.flowWidget; + binding.getRoot().setTag(viewHolder); + return viewHolder; + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java index a71300d64cab125e4e97aa6eb560bb3a6d62e9c2..344cfd1b5f50501a34010cbf5d5cd528ce99d4ea 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java +++ b/src/main/java/eu/siacs/conversations/ui/util/SendButtonTool.java @@ -31,22 +31,18 @@ package eu.siacs.conversations.ui.util; import android.app.Activity; import android.content.SharedPreferences; -import android.content.res.Configuration; import android.preference.PreferenceManager; import android.view.View; - import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.core.content.ContextCompat; - import com.google.android.material.color.MaterialColors; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.ui.Activities; import eu.siacs.conversations.ui.ConversationFragment; import eu.siacs.conversations.utils.UIHelper; +import im.conversations.android.xmpp.model.stanza.Presence; public class SendButtonTool { @@ -110,28 +106,37 @@ public class SendButtonTool { }; } - public @ColorInt static int getSendButtonColor(final View view, final Presence.Status status) { + public @ColorInt static int getSendButtonColor( + final View view, final Presence.Availability status) { final boolean nightMode = Activities.isNightMode(view.getContext()); return switch (status) { - case OFFLINE -> MaterialColors.getColor( - view, com.google.android.material.R.attr.colorOnSurface); - case ONLINE, CHAT -> MaterialColors.harmonizeWithPrimary( - view.getContext(), - ContextCompat.getColor( - view.getContext(), nightMode ? R.color.green_300 : R.color.green_800)); - case AWAY -> MaterialColors.harmonizeWithPrimary( - view.getContext(), - ContextCompat.getColor( - view.getContext(), nightMode ? R.color.amber_300 : R.color.amber_800)); - case XA -> MaterialColors.harmonizeWithPrimary( - view.getContext(), - ContextCompat.getColor( + case OFFLINE -> + MaterialColors.getColor( + view, com.google.android.material.R.attr.colorOnSurface); + case ONLINE, CHAT -> + MaterialColors.harmonizeWithPrimary( + view.getContext(), + ContextCompat.getColor( + view.getContext(), + nightMode ? R.color.green_300 : R.color.green_800)); + case AWAY -> + MaterialColors.harmonizeWithPrimary( + view.getContext(), + ContextCompat.getColor( + view.getContext(), + nightMode ? R.color.amber_300 : R.color.amber_800)); + case XA -> + MaterialColors.harmonizeWithPrimary( + view.getContext(), + ContextCompat.getColor( + view.getContext(), + nightMode ? R.color.orange_300 : R.color.orange_800)); + case DND -> + MaterialColors.harmonizeWithPrimary( view.getContext(), - nightMode ? R.color.orange_300 : R.color.orange_800)); - case DND -> MaterialColors.harmonizeWithPrimary( - view.getContext(), - ContextCompat.getColor( - view.getContext(), nightMode ? R.color.red_300 : R.color.red_800)); + ContextCompat.getColor( + view.getContext(), + nightMode ? R.color.red_300 : R.color.red_800)); }; } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 9f3aab8bc9ca937c35f530e91ec494e580c6006e..bf59a83e8fa01e7162a91bdae2ebf4caf28019a9 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -22,12 +22,12 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.RtpSessionStatus; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.worker.ExportBackupWorker; import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.stanza.Presence; import java.util.Arrays; import java.util.Calendar; import java.util.Date; @@ -523,7 +523,7 @@ public class UIHelper { return LOCATION_QUESTIONS.contains(body); } - public static void setStatus(final TextView textView, Presence.Status status) { + public static void setStatus(final TextView textView, Presence.Availability status) { final @StringRes int text; final @ColorRes int color = switch (status) { diff --git a/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java b/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java deleted file mode 100644 index cce6fc163c6b88849335d7fe048346c50f3b0ccd..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/xml/XmlElementReader.java +++ /dev/null @@ -1,20 +0,0 @@ -package eu.siacs.conversations.xml; - -import com.google.common.io.ByteSource; - -import java.io.IOException; -import java.io.InputStream; - -public class XmlElementReader { - - public static Element read(byte[] bytes) throws IOException { - return read(ByteSource.wrap(bytes).openStream()); - } - - public static Element read(InputStream inputStream) throws IOException { - final XmlReader xmlReader = new XmlReader(); - xmlReader.setInputStream(inputStream); - return xmlReader.readElement(xmlReader.readTag()); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index e8826c96aa46558a283b2e81adac42076427c1d8..987ffb1b0330f153d7179084ef2f83851c639774 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -18,10 +18,16 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.ClassToInstanceMap; +import com.google.common.collect.ImmutableClassToInstanceMap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import de.gultsch.common.Patterns; import eu.siacs.conversations.AppSettings; @@ -38,12 +44,12 @@ import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramMechanism; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.MessageParser; import eu.siacs.conversations.parser.PresenceParser; +import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.MemorizingTrustManager; import eu.siacs.conversations.services.MessageArchiveService; @@ -66,6 +72,9 @@ import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xmpp.bind.Bind2; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; +import eu.siacs.conversations.xmpp.manager.AbstractManager; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.model.AuthenticationFailure; import im.conversations.android.xmpp.model.AuthenticationRequest; import im.conversations.android.xmpp.model.AuthenticationStreamFeature; @@ -75,6 +84,7 @@ import im.conversations.android.xmpp.model.bind2.Bound; import im.conversations.android.xmpp.model.cb.SaslChannelBinding; import im.conversations.android.xmpp.model.csi.Active; import im.conversations.android.xmpp.model.csi.Inactive; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.error.Condition; import im.conversations.android.xmpp.model.fast.Fast; import im.conversations.android.xmpp.model.fast.RequestToken; @@ -126,6 +136,7 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -149,7 +160,6 @@ public class XmppConnection implements Runnable { protected final Account account; private final Features features = new Features(this); - private final HashMap disco = new HashMap<>(); private final HashMap commands = new HashMap<>(); private final SparseArray mStanzaQueue = new SparseArray<>(); private final Hashtable>> packetCallbacks = new Hashtable<>(); @@ -177,7 +187,6 @@ public class XmppConnection implements Runnable { private long lastSessionStarted = 0; private long lastDiscoStarted = 0; private boolean isMamPreferenceAlways = false; - private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0); private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true); private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false); private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); @@ -200,6 +209,7 @@ public class XmppConnection implements Runnable { private Resolver.Result seeOtherHostResolverResult; private volatile Thread mThread; private CountDownLatch mStreamCountDownLatch; + private final ClassToInstanceMap managers; public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; @@ -209,6 +219,12 @@ public class XmppConnection implements Runnable { this.unregisteredIqListener = new IqParser(service, account); this.messageListener = new MessageParser(service, account); this.bindListener = new BindProcessor(service, account); + this.managers = + new ImmutableClassToInstanceMap.Builder() + .put( + DiscoManager.class, + new DiscoManager(service.getApplicationContext(), this)) + .build(); } private static void fixResource(final Context context, final Account account) { @@ -2000,9 +2016,7 @@ public class XmppConnection implements Runnable { this.mStanzaQueue.clear(); } this.redirectionUrl = null; - synchronized (this.disco) { - disco.clear(); - } + getManager(DiscoManager.class).clear(); synchronized (this.commands) { this.commands.clear(); } @@ -2165,41 +2179,99 @@ public class XmppConnection implements Runnable { final boolean waitForDisco, final boolean carbonsEnabled) { features.carbonsEnabled = carbonsEnabled; features.blockListRequested = false; - synchronized (this.disco) { - this.disco.clear(); - } + getManager(DiscoManager.class).clear(); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": starting service discovery"); - mPendingServiceDiscoveries.set(0); mWaitForDisco.set(waitForDisco); this.lastDiscoStarted = SystemClock.elapsedRealtime(); mXmppConnectionService.scheduleWakeUpCall( Config.CONNECT_DISCO_TIMEOUT * 1000L, account.getUuid().hashCode()); - final Element caps = streamFeatures.findChild("c"); - final String hash = caps == null ? null : caps.getAttribute("hash"); - final String ver = caps == null ? null : caps.getAttribute("ver"); - ServiceDiscoveryResult discoveryResult = null; - if (hash != null && ver != null) { - discoveryResult = - mXmppConnectionService.getCachedServiceDiscoveryResult(new Pair<>(hash, ver)); - } - final boolean requestDiscoItemsFirst = - !account.isOptionSet(Account.OPTION_LOGGED_IN_SUCCESSFULLY); - if (requestDiscoItemsFirst) { - sendServiceDiscoveryItems(account.getDomain()); - } - if (discoveryResult == null) { - sendServiceDiscoveryInfo(account.getDomain()); - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server caps came from cache"); - disco.put(account.getDomain(), discoveryResult); - } + + final var nodeHash = streamFeatures.getCapabilities(); + final var serverInfoFuture = + getManager(DiscoManager.class) + .infoOrCache(Entity.discoItem(account.getDomain()), nodeHash); + final var features = getFeatures(); if (!features.bind2()) { discoverMamPreferences(); } - sendServiceDiscoveryInfo(account.getJid().asBareJid()); - if (!requestDiscoItemsFirst) { - sendServiceDiscoveryItems(account.getDomain()); + + final var accountInfoFuture = + getManager(DiscoManager.class) + .info(Entity.discoItem(account.getJid().asBareJid()), null); + + final var itemsFuture = + getManager(DiscoManager.class).itemsWithInfo(Entity.discoItem(account.getDomain())); + + final var catchingServerFuture = + Futures.catching( + serverInfoFuture, + DiscoManager.CapsHashMismatchException.class, + input -> { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": error in server caps", + input); + return null; + }, + MoreExecutors.directExecutor()); + + Futures.addCallback( + Futures.allAsList(accountInfoFuture, catchingServerFuture), + new FutureCallback<>() { + @Override + public void onSuccess(List result) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": advanced stream future done"); + enableAdvancedStreamFeatures(); + } + + @Override + public void onFailure(@Nullable Throwable throwable) { + Log.d( + Config.LOGTAG, + "could not fetch disco for advanced stream features", + throwable); + } + }, + MoreExecutors.directExecutor()); + + if (mWaitForDisco.get()) { + final ListenableFuture discoComplete = + Futures.whenAllComplete(serverInfoFuture, accountInfoFuture, itemsFuture) + .call(() -> null, MoreExecutors.directExecutor()); + Futures.addCallback( + discoComplete, + new FutureCallback<>() { + @Override + public void onSuccess(Void result) { + if (timeout(serverInfoFuture, accountInfoFuture, itemsFuture)) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": reached timeout while waiting for disco"); + return; + } + if (mWaitForDisco.compareAndSet(true, false)) { + finalizeBindOrError(); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": disco complete but bind was already" + + " finalized"); + } + } + + @Override + public void onFailure(@NonNull Throwable t) { + Log.d(Config.LOGTAG, "error in disco: ", t); + } + }, + MoreExecutors.directExecutor()); + } else { + finalizeBind(); } if (!mWaitForDisco.get()) { @@ -2208,63 +2280,19 @@ public class XmppConnection implements Runnable { this.lastSessionStarted = SystemClock.elapsedRealtime(); } - private void sendServiceDiscoveryInfo(final Jid jid) { - mPendingServiceDiscoveries.incrementAndGet(); - final Iq iq = new Iq(Iq.Type.GET); - iq.setTo(jid); - iq.query("http://jabber.org/protocol/disco#info"); - this.sendIqPacket( - iq, - (packet) -> { - if (packet.getType() == Iq.Type.RESULT) { - boolean advancedStreamFeaturesLoaded; - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); - if (jid.equals(account.getDomain())) { - mXmppConnectionService.databaseBackend.insertDiscoveryResult( - result); - } - disco.put(jid, result); - advancedStreamFeaturesLoaded = - disco.containsKey(account.getDomain()) - && disco.containsKey(account.getJid().asBareJid()); - } - if (advancedStreamFeaturesLoaded - && (jid.equals(account.getDomain()) - || jid.equals(account.getJid().asBareJid()))) { - enableAdvancedStreamFeatures(); - } - } else if (packet.getType() == Iq.Type.ERROR) { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not query disco info for " - + jid.toString()); - final boolean serverOrAccount = - jid.equals(account.getDomain()) - || jid.equals(account.getJid().asBareJid()); - final boolean advancedStreamFeaturesLoaded; - if (serverOrAccount) { - synchronized (XmppConnection.this.disco) { - disco.put(jid, ServiceDiscoveryResult.empty()); - advancedStreamFeaturesLoaded = - disco.containsKey(account.getDomain()) - && disco.containsKey(account.getJid().asBareJid()); - } - } else { - advancedStreamFeaturesLoaded = false; - } - if (advancedStreamFeaturesLoaded) { - enableAdvancedStreamFeatures(); - } - } - if (packet.getType() != Iq.Type.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } + private boolean timeout(final ListenableFuture... futures) { + for (final ListenableFuture future : futures) { + if (future.isDone()) { + try { + future.get(); + } catch (final Exception e) { + if (Throwables.getRootCause(e) instanceof TimeoutException) { + return true; } - }); + } + } + } + return false; } private void discoverMamPreferences() { @@ -2288,39 +2316,42 @@ public class XmppConnection implements Runnable { } private void discoverCommands() { - final Iq request = new Iq(Iq.Type.GET); - request.setTo(account.getDomain()); - request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); - sendIqPacket( - request, - (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final Element query = response.findChild("query", Namespace.DISCO_ITEMS); - if (query == null) { - return; - } - final HashMap commands = new HashMap<>(); - for (final Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - final String node = child.getAttribute("node"); - final Jid jid = child.getAttributeAsJid("jid"); - if (node != null && jid != null) { - commands.put(node, jid); - } - } - } - synchronized (this.commands) { - this.commands.clear(); - this.commands.putAll(commands); + final var future = + getManager(DiscoManager.class).commands(Entity.discoItem(account.getDomain())); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(Map result) { + synchronized (XmppConnection.this.commands) { + XmppConnection.this.commands.clear(); + XmppConnection.this.commands.putAll(result); } } - }); + + @Override + public void onFailure(@NonNull Throwable throwable) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": could not fetch commands", + throwable); + } + }, + MoreExecutors.directExecutor()); } public boolean isMamPreferenceAlways() { return isMamPreferenceAlways; } + private void finalizeBindOrError() { + try { + finalizeBind(); + } catch (final Exception e) { + throw new Error(e); + } + } + private void finalizeBind() { this.offlineMessagesRetrieved = false; this.bindListener.run(); @@ -2344,46 +2375,6 @@ public class XmppConnection implements Runnable { } } - private void sendServiceDiscoveryItems(final Jid server) { - mPendingServiceDiscoveries.incrementAndGet(); - final Iq iq = new Iq(Iq.Type.GET); - iq.setTo(server.getDomain()); - iq.query("http://jabber.org/protocol/disco#items"); - this.sendIqPacket( - iq, - (packet) -> { - if (packet.getType() == Iq.Type.RESULT) { - final HashSet items = new HashSet<>(); - final List elements = packet.query().getChildren(); - for (final Element element : elements) { - if (element.getName().equals("item")) { - final Jid jid = - Jid.Invalid.getNullForInvalid( - element.getAttributeAsJid("jid")); - if (jid != null && !jid.equals(account.getDomain())) { - items.add(jid); - } - } - } - for (Jid jid : items) { - sendServiceDiscoveryInfo(jid); - } - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": could not query disco items of " - + server); - } - if (packet.getType() != Iq.Type.TIMEOUT) { - if (mPendingServiceDiscoveries.decrementAndGet() == 0 - && mWaitForDisco.compareAndSet(true, false)) { - finalizeBind(); - } - } - }); - } - private void sendEnableCarbons() { final Iq iq = new Iq(Iq.Type.SET); iq.addChild("enable", Namespace.CARBONS); @@ -2726,28 +2717,23 @@ public class XmppConnection implements Runnable { this.boundStreamFeatures = null; } - private List> findDiscoItemsByFeature(final String feature) { - synchronized (this.disco) { - final List> items = new ArrayList<>(); - for (final Entry cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - items.add(cursor); - } - } - return items; - } + public M getManager(final Class clazz) { + return this.managers.getInstance(clazz); } - public Entry getServiceDiscoveryResultByFeature( - final String feature) { - synchronized (this.disco) { - for (final var cursor : this.disco.entrySet()) { - if (cursor.getValue().getFeatures().contains(feature)) { - return cursor; - } + private List> findDiscoItemsByFeature(final String feature) { + final List> items = new ArrayList<>(); + for (final Entry cursor : + getManager(DiscoManager.class).getServerItems().entrySet()) { + if (cursor.getValue().getFeatureStrings().contains(feature)) { + items.add(cursor); } - return null; } + return items; + } + + public Entry getServiceDiscoveryResultByFeature(final String feature) { + return Iterables.getFirst(findDiscoItemsByFeature(feature), null); } public Jid findDiscoItemByFeature(final String feature) { @@ -2775,15 +2761,14 @@ public class XmppConnection implements Runnable { public List getMucServers() { List servers = new ArrayList<>(); - synchronized (this.disco) { - for (final Entry cursor : disco.entrySet()) { - final ServiceDiscoveryResult value = cursor.getValue(); - if (value.getFeatures().contains("http://jabber.org/protocol/muc") - && value.hasIdentity("conference", "text") - && !value.getFeatures().contains("jabber:iq:gateway") - && !value.hasIdentity("conference", "irc")) { - servers.add(cursor.getKey().toString()); - } + for (final Entry entry : + getManager(DiscoManager.class).getServerItems().entrySet()) { + final var value = entry.getValue(); + if (value.getFeatureStrings().contains("http://jabber.org/protocol/muc") + && value.hasIdentityWithCategoryAndType("conference", "text") + && !value.getFeatureStrings().contains("jabber:iq:gateway") + && !value.hasIdentityWithCategoryAndType("conference", "irc")) { + servers.add(entry.getKey().toString()); } } return servers; @@ -2910,6 +2895,10 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.CONNECTION_TIMEOUT); } + public Account getAccount() { + return this.account; + } + private class MyKeyManager implements X509KeyManager { @Override public String chooseClientAlias(String[] strings, Principal[] principals, Socket socket) { @@ -3033,6 +3022,29 @@ public class XmppConnection implements Runnable { } } + public abstract static class Delegate { + + protected final Context context; + protected final XmppConnection connection; + + protected Delegate(final Context context, final XmppConnection connection) { + this.context = context; + this.connection = connection; + } + + protected Account getAccount() { + return connection.account; + } + + protected DatabaseBackend getDatabase() { + return DatabaseBackend.getInstance(context); + } + + protected T getManager(final Class type) { + return connection.getManager(type); + } + } + public class Features { XmppConnection connection; private boolean carbonsEnabled = false; @@ -3044,10 +3056,8 @@ public class XmppConnection implements Runnable { } private boolean hasDiscoFeature(final Jid server, final String feature) { - synchronized (XmppConnection.this.disco) { - final ServiceDiscoveryResult sdr = connection.disco.get(server); - return sdr != null && sdr.getFeatures().contains(feature); - } + final var infoQuery = getManager(DiscoManager.class).get(server); + return infoQuery != null && infoQuery.getFeatureStrings().contains(feature); } public boolean carbons() { @@ -3103,19 +3113,16 @@ public class XmppConnection implements Runnable { } public boolean pep() { - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid()); - return info != null && info.hasIdentity("pubsub", "pep"); - } + final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid()); + return infoQuery != null && infoQuery.hasIdentityWithCategoryAndType("pubsub", "pep"); } public boolean pepPersistent() { - synchronized (XmppConnection.this.disco) { - ServiceDiscoveryResult info = disco.get(account.getJid().asBareJid()); - return info != null - && info.getFeatures() - .contains("http://jabber.org/protocol/pubsub#persistent-items"); - } + final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid()); + return infoQuery != null + && infoQuery + .getFeatureStrings() + .contains("http://jabber.org/protocol/pubsub#persistent-items"); } public boolean bind2() { @@ -3150,9 +3157,9 @@ public class XmppConnection implements Runnable { return MessageArchiveService.Version.has(getAccountFeatures()); } - public List getAccountFeatures() { - ServiceDiscoveryResult result = connection.disco.get(account.getJid().asBareJid()); - return result == null ? Collections.emptyList() : result.getFeatures(); + public Collection getAccountFeatures() { + final var infoQuery = getManager(DiscoManager.class).get(account.getJid().asBareJid()); + return infoQuery == null ? Collections.emptyList() : infoQuery.getFeatureStrings(); } public boolean push() { @@ -3169,12 +3176,12 @@ public class XmppConnection implements Runnable { } public HttpUrl getServiceOutageStatus() { - final var disco = connection.disco.get(account.getDomain()); + final var disco = getManager(DiscoManager.class).get(account.getDomain()); if (disco == null) { return null; } final var address = - disco.getExtendedDiscoInformation( + disco.getServiceDiscoveryExtension( Namespace.SERVICE_OUTAGE_STATUS, "external-status-addresses"); if (Strings.isNullOrEmpty(address)) { return null; @@ -3195,7 +3202,7 @@ public class XmppConnection implements Runnable { maxSize = Long.parseLong( result.getValue() - .getExtendedDiscoInformation( + .getServiceDiscoveryExtension( Namespace.HTTP_UPLOAD, "max-file-size")); } catch (final Exception e) { return true; @@ -3224,7 +3231,7 @@ public class XmppConnection implements Runnable { try { return Long.parseLong( result.getValue() - .getExtendedDiscoInformation( + .getServiceDiscoveryExtension( Namespace.HTTP_UPLOAD, "max-file-size")); } catch (final Exception e) { return -1; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java index de325534e3a5c45f9bc6acc4aa0a3b8a985aed82..9a80ecabec409f7a9ccd053eb81b5f05a2b61a3a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/AbstractJingleConnection.java @@ -5,18 +5,16 @@ import androidx.annotation.NonNull; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; +import eu.siacs.conversations.xmpp.manager.DiscoManager; import im.conversations.android.xmpp.model.jingle.Jingle; import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Arrays; @@ -331,14 +329,15 @@ public abstract class AbstractJingleConnection { } protected boolean remoteHasFeature(final String feature) { - final Contact contact = id.getContact(); - final Presence presence = - contact.getPresences().get(Strings.nullToEmpty(id.with.getResource())); - final ServiceDiscoveryResult serviceDiscoveryResult = - presence == null ? null : presence.getServiceDiscoveryResult(); - final List features = - serviceDiscoveryResult == null ? null : serviceDiscoveryResult.getFeatures(); - return features != null && features.contains(feature); + final var connection = id.account.getXmppConnection(); + if (connection == null) { + return false; + } + final var infoQuery = connection.getManager(DiscoManager.class).get(id.with); + if (infoQuery == null) { + return false; + } + return infoQuery.hasFeature(feature); } public static class Id { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java index 484b6b5c892c7314047e42241c91d391c6648235..49ec84af131b1209148854d49b7322310bfeee6a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpCapability.java @@ -3,37 +3,34 @@ package eu.siacs.conversations.xmpp.jingle; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; - +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; -import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Presence; -import eu.siacs.conversations.entities.Presences; -import eu.siacs.conversations.entities.ServiceDiscoveryResult; -import eu.siacs.conversations.xml.Namespace; - public class RtpCapability { - private static final List BASIC_RTP_REQUIREMENTS = Arrays.asList( - Namespace.JINGLE, - Namespace.JINGLE_TRANSPORT_ICE_UDP, - Namespace.JINGLE_APPS_RTP, - Namespace.JINGLE_APPS_DTLS - ); - private static final Collection VIDEO_REQUIREMENTS = Arrays.asList( - Namespace.JINGLE_FEATURE_AUDIO, - Namespace.JINGLE_FEATURE_VIDEO - ); + private static final List BASIC_RTP_REQUIREMENTS = + Arrays.asList( + Namespace.JINGLE, + Namespace.JINGLE_TRANSPORT_ICE_UDP, + Namespace.JINGLE_APPS_RTP, + Namespace.JINGLE_APPS_DTLS); + private static final Collection VIDEO_REQUIREMENTS = + Arrays.asList(Namespace.JINGLE_FEATURE_AUDIO, Namespace.JINGLE_FEATURE_VIDEO); - public static Capability check(final Presence presence) { - final ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult(); - final Set features = disco == null ? Collections.emptySet() : ImmutableSet.copyOf(disco.getFeatures()); + public static Capability check(final InfoQuery infoQuery) { + final Set features = + infoQuery == null + ? Collections.emptySet() + : ImmutableSet.copyOf(infoQuery.getFeatureStrings()); if (features.containsAll(BASIC_RTP_REQUIREMENTS)) { if (features.containsAll(VIDEO_REQUIREMENTS)) { return Capability.VIDEO; @@ -46,15 +43,23 @@ public class RtpCapability { } public static String[] filterPresences(final Contact contact, Capability required) { + final var connection = contact.getAccount().getXmppConnection(); + if (connection == null) { + return new String[0]; + } final Presences presences = contact.getPresences(); final ArrayList resources = new ArrayList<>(); - for (final Map.Entry presence : presences.getPresencesMap().entrySet()) { - final Capability capability = check(presence.getValue()); + for (final String resource : presences.getPresencesMap().keySet()) { + final var jid = + Strings.isNullOrEmpty(resource) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(resource); + final Capability capability = check(connection.getManager(DiscoManager.class).get(jid)); if (capability == Capability.NONE) { continue; } if (required == Capability.AUDIO || capability == required) { - resources.add(presence.getKey()); + resources.add(resource); } } return resources.toArray(new String[0]); @@ -69,9 +74,17 @@ public class RtpCapability { if (presences.isEmpty() && allowFallback && contact.getAccount().isEnabled()) { return contact.getRtpCapability(); } + final var connection = contact.getAccount().getXmppConnection(); + if (connection == null) { + return Capability.NONE; + } Capability result = Capability.NONE; - for (final Presence presence : presences.getPresences()) { - Capability capability = check(presence); + for (final String resource : presences.getPresencesMap().keySet()) { + final var jid = + Strings.isNullOrEmpty(resource) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(resource); + final Capability capability = check(connection.getManager(DiscoManager.class).get(jid)); if (capability == Capability.VIDEO) { result = capability; } else if (capability == Capability.AUDIO && result == Capability.NONE) { @@ -83,18 +96,43 @@ public class RtpCapability { // do all devices that support Rtp Call also support JMI? public static boolean jmiSupport(final Contact contact) { + final var connection = contact.getAccount().getXmppConnection(); + if (connection == null) { + return false; + } return !Collections2.transform( - Collections2.filter( - contact.getPresences().getPresences(), - p -> RtpCapability.check(p) != RtpCapability.Capability.NONE), - p -> { - ServiceDiscoveryResult disco = p.getServiceDiscoveryResult(); - return disco != null && disco.getFeatures().contains(Namespace.JINGLE_MESSAGE); - }).contains(false); + Collections2.filter( + contact.getPresences().getPresencesMap().keySet(), + p -> + RtpCapability.check( + connection + .getManager(DiscoManager.class) + .get( + Strings.isNullOrEmpty(p) + ? contact.getJid() + .asBareJid() + : contact.getJid() + .withResource( + p))) + != Capability.NONE), + p -> { + final var disco = + connection + .getManager(DiscoManager.class) + .get( + Strings.isNullOrEmpty(p) + ? contact.getJid().asBareJid() + : contact.getJid().withResource(p)); + return disco != null + && disco.getFeatureStrings().contains(Namespace.JINGLE_MESSAGE); + }) + .contains(false); } public enum Capability { - NONE, AUDIO, VIDEO; + NONE, + AUDIO, + VIDEO; public static Capability of(String value) { if (Strings.isNullOrEmpty(value)) { @@ -107,5 +145,4 @@ public class RtpCapability { } } } - } diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java new file mode 100644 index 0000000000000000000000000000000000000000..b5c129e36dcb79aacb246ceffaeeadbaf5d0f924 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/AbstractManager.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import eu.siacs.conversations.xmpp.XmppConnection; + +public abstract class AbstractManager extends XmppConnection.Delegate { + + protected AbstractManager(final Context context, final XmppConnection connection) { + super(context, connection); + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java new file mode 100644 index 0000000000000000000000000000000000000000..e773d617bcd8c0f0dcab8794fc0a94788b7ad2dd --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/manager/DiscoManager.java @@ -0,0 +1,307 @@ +package eu.siacs.conversations.xmpp.manager; + +import android.content.Context; +import android.util.Log; +import com.google.common.base.Strings; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.BaseEncoding; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.xmpp.XmppConnection; +import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.EntityCapabilities; +import im.conversations.android.xmpp.EntityCapabilities2; +import im.conversations.android.xmpp.model.Hash; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.disco.items.Item; +import im.conversations.android.xmpp.model.disco.items.ItemsQuery; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public class DiscoManager extends AbstractManager { + + public static final String CAPABILITY_NODE = "http://conversations.im"; + + // this is the runtime cache that stores disco information for all entities seen during a + // session + + // a caps cache will be build in the database + + private final Map entityInformation = new HashMap<>(); + private final Map> discoItems = new HashMap<>(); + + public DiscoManager(Context context, XmppConnection connection) { + super(context, connection); + } + + public static EntityCapabilities.Hash buildHashFromNode(final String node) { + final var capsPrefix = CAPABILITY_NODE + "#"; + final var caps2Prefix = Namespace.ENTITY_CAPABILITIES_2 + "#"; + if (node.startsWith(capsPrefix)) { + final String hash = node.substring(capsPrefix.length()); + if (Strings.isNullOrEmpty(hash)) { + return null; + } + if (BaseEncoding.base64().canDecode(hash)) { + return EntityCapabilities.EntityCapsHash.of(hash); + } + } else if (node.startsWith(caps2Prefix)) { + final String caps = node.substring(caps2Prefix.length()); + if (Strings.isNullOrEmpty(caps)) { + return null; + } + final int separator = caps.lastIndexOf('.'); + if (separator < 0) { + return null; + } + final Hash.Algorithm algorithm = Hash.Algorithm.tryParse(caps.substring(0, separator)); + final String hash = caps.substring(separator + 1); + if (algorithm == null || Strings.isNullOrEmpty(hash)) { + return null; + } + if (BaseEncoding.base64().canDecode(hash)) { + return EntityCapabilities2.EntityCaps2Hash.of(algorithm, hash); + } + } + return null; + } + + public ListenableFuture infoOrCache( + final Entity entity, + final im.conversations.android.xmpp.model.capabilties.EntityCapabilities.NodeHash + nodeHash) { + if (nodeHash == null) { + return infoOrCache(entity, null, null); + } + return infoOrCache(entity, nodeHash.node, nodeHash.hash); + } + + public ListenableFuture infoOrCache( + final Entity entity, final String node, final EntityCapabilities.Hash hash) { + final var cached = getDatabase().getInfoQuery(hash); + if (cached != null) { + if (node == null || hash != null) { + this.put(entity.address, cached); + } + return Futures.immediateFuture(null); + } + return Futures.transform( + info(entity, node, hash), f -> null, MoreExecutors.directExecutor()); + } + + public ListenableFuture info( + @NonNull final Entity entity, @Nullable final String node) { + return info(entity, node, null); + } + + public ListenableFuture info( + final Entity entity, @Nullable final String node, final EntityCapabilities.Hash hash) { + final var requestNode = hash != null && node != null ? hash.capabilityNode(node) : node; + final var iqRequest = new Iq(Iq.Type.GET); + iqRequest.setTo(entity.address); + final InfoQuery infoQueryRequest = iqRequest.addExtension(new InfoQuery()); + if (requestNode != null) { + infoQueryRequest.setNode(requestNode); + } + final var future = connection.sendIqPacket(iqRequest); + return Futures.transform( + future, + iqResult -> { + final var infoQuery = iqResult.getExtension(InfoQuery.class); + if (infoQuery == null) { + throw new IllegalStateException("Response did not have query child"); + } + if (!Objects.equals(requestNode, infoQuery.getNode())) { + throw new IllegalStateException( + "Node in response did not match node in request"); + } + + if (node == null + || (hash != null + && hash.capabilityNode(node).equals(infoQuery.getNode()))) { + // only storing results w/o nodes + this.put(entity.address, infoQuery); + } + + final var caps = EntityCapabilities.hash(infoQuery); + final var caps2 = EntityCapabilities2.hash(infoQuery); + if (hash instanceof EntityCapabilities.EntityCapsHash) { + checkMatch( + (EntityCapabilities.EntityCapsHash) hash, + caps, + EntityCapabilities.EntityCapsHash.class); + } + if (hash instanceof EntityCapabilities2.EntityCaps2Hash) { + checkMatch( + (EntityCapabilities2.EntityCaps2Hash) hash, + caps2, + EntityCapabilities2.EntityCaps2Hash.class); + } + // we want to avoid caching disco info for entities that put variable data (like + // number of occupants in a MUC) into it + final boolean cache = + Objects.nonNull(hash) + || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES) + || infoQuery.hasFeature(Namespace.ENTITY_CAPABILITIES_2); + + if (cache) { + getDatabase().insertCapsCache(caps, caps2, infoQuery); + } + + return infoQuery; + }, + MoreExecutors.directExecutor()); + } + + private void checkMatch( + final H expected, final H was, final Class clazz) { + if (Arrays.equals(expected.hash, was.hash)) { + return; + } + throw new CapsHashMismatchException( + String.format( + "%s mismatch. Expected %s was %s", + clazz.getSimpleName(), + BaseEncoding.base64().encode(expected.hash), + BaseEncoding.base64().encode(was.hash))); + } + + public ListenableFuture> items(final Entity.DiscoItem entity) { + return items(entity, null); + } + + public ListenableFuture> items( + final Entity.DiscoItem entity, @Nullable final String node) { + final var requestNode = Strings.emptyToNull(node); + final var iqPacket = new Iq(Iq.Type.GET); + iqPacket.setTo(entity.address); + final ItemsQuery itemsQueryRequest = iqPacket.addExtension(new ItemsQuery()); + if (requestNode != null) { + itemsQueryRequest.setNode(requestNode); + } + final var future = connection.sendIqPacket(iqPacket); + return Futures.transform( + future, + iqResult -> { + final var itemsQuery = iqResult.getExtension(ItemsQuery.class); + if (itemsQuery == null) { + throw new IllegalStateException(); + } + if (!Objects.equals(requestNode, itemsQuery.getNode())) { + throw new IllegalStateException( + "Node in response did not match node in request"); + } + final var items = itemsQuery.getExtensions(Item.class); + + final var validItems = + Collections2.filter(items, i -> Objects.nonNull(i.getJid())); + + final var itemsAsAddresses = + ImmutableSet.copyOf(Collections2.transform(validItems, Item::getJid)); + if (node == null) { + this.discoItems.put(entity.address, itemsAsAddresses); + } + return validItems; + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture> itemsWithInfo(final Entity.DiscoItem entity) { + final var itemsFutures = items(entity); + final var filtered = + Futures.transform( + itemsFutures, + items -> + Collections2.filter( + items, + i -> + i.getNode() == null + && !entity.address.equals(i.getJid())), + MoreExecutors.directExecutor()); + return Futures.transformAsync( + filtered, + items -> { + Collection> infoFutures = + Collections2.transform( + items, i -> info(Entity.discoItem(i.getJid()), i.getNode())); + return Futures.allAsList(infoFutures); + }, + MoreExecutors.directExecutor()); + } + + public ListenableFuture> commands(final Entity.DiscoItem entity) { + final var itemsFuture = items(entity, Namespace.COMMANDS); + return Futures.transform( + itemsFuture, + items -> { + final var builder = new ImmutableMap.Builder(); + for (final var item : items) { + final var jid = item.getJid(); + final var node = item.getNode(); + if (Jid.Invalid.isValid(jid) && node != null) { + builder.put(node, jid); + } + } + return builder.buildKeepingLast(); + }, + MoreExecutors.directExecutor()); + } + + public Map getServerItems() { + final var builder = new ImmutableMap.Builder(); + final var domain = connection.getAccount().getDomain(); + final var domainInfoQuery = get(domain); + if (domainInfoQuery != null) { + builder.put(domain, domainInfoQuery); + } + final var items = this.discoItems.get(domain); + if (items == null) { + return builder.build(); + } + for (final var item : items) { + final var infoQuery = get(item); + if (infoQuery == null) { + continue; + } + builder.put(item, infoQuery); + } + return builder.buildKeepingLast(); + } + + private void put(final Jid address, final InfoQuery infoQuery) { + synchronized (this.entityInformation) { + this.entityInformation.put(address, infoQuery); + } + } + + public InfoQuery get(final Jid address) { + synchronized (this.entityInformation) { + return this.entityInformation.get(address); + } + } + + public void clear() { + synchronized (this.entityInformation) { + this.entityInformation.clear(); + } + } + + public static final class CapsHashMismatchException extends IllegalStateException { + public CapsHashMismatchException(final String message) { + super(message); + } + } +} diff --git a/src/main/java/im/conversations/android/xml/XmlElementReader.java b/src/main/java/im/conversations/android/xml/XmlElementReader.java new file mode 100644 index 0000000000000000000000000000000000000000..d7c0a0424014066e5f1473a2eac1282c06415a74 --- /dev/null +++ b/src/main/java/im/conversations/android/xml/XmlElementReader.java @@ -0,0 +1,21 @@ +package im.conversations.android.xml; + +import com.google.common.io.ByteSource; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.XmlReader; +import java.io.IOException; +import java.io.InputStream; + +public class XmlElementReader { + + public static Element read(byte[] bytes) throws IOException { + return read(ByteSource.wrap(bytes).openStream()); + } + + public static Element read(final InputStream inputStream) throws IOException { + try (final XmlReader xmlReader = new XmlReader()) { + xmlReader.setInputStream(inputStream); + return xmlReader.readElement(xmlReader.readTag()); + } + } +} diff --git a/src/main/java/im/conversations/android/xmpp/Entity.java b/src/main/java/im/conversations/android/xmpp/Entity.java index a578d250780e40c8b5a588a74f1ea43496f2c515..799d7a5eadd99032c3690fe318370540de1497cf 100644 --- a/src/main/java/im/conversations/android/xmpp/Entity.java +++ b/src/main/java/im/conversations/android/xmpp/Entity.java @@ -1,6 +1,6 @@ package im.conversations.android.xmpp; -import org.jxmpp.jid.Jid; +import eu.siacs.conversations.xmpp.Jid; public abstract class Entity { diff --git a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java index 1d8a35a68d2d5822c2d0ada756b91e473bb698e9..d891870aa41af45d96daaf6ec8ceaf89d0635689 100644 --- a/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java +++ b/src/main/java/im/conversations/android/xmpp/EntityCapabilities2.java @@ -8,7 +8,6 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.google.common.io.BaseEncoding; import com.google.common.primitives.Bytes; - import eu.siacs.conversations.xml.Namespace; import im.conversations.android.xmpp.model.Hash; import im.conversations.android.xmpp.model.data.Data; @@ -42,16 +41,12 @@ public class EntityCapabilities2 { } private static HashFunction toHashFunction(final Hash.Algorithm algorithm) { - switch (algorithm) { - case SHA_1: - return Hashing.sha1(); - case SHA_256: - return Hashing.sha256(); - case SHA_512: - return Hashing.sha512(); - default: - throw new IllegalArgumentException("Unknown hash algorithm"); - } + return switch (algorithm) { + case SHA_1 -> Hashing.sha1(); + case SHA_256 -> Hashing.sha256(); + case SHA_512 -> Hashing.sha512(); + default -> throw new IllegalArgumentException("Unknown hash algorithm"); + }; } private static String asHex(final String message) { diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Data.java b/src/main/java/im/conversations/android/xmpp/model/data/Data.java index c754ee48de9364084628b26fc596d2645741ead3..7fc03360d3d275ec27c20d7dc0df8b1987c4298a 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Data.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Data.java @@ -29,6 +29,10 @@ public class Data extends Extension { this.getExtensions(Field.class), f -> !FORM_TYPE.equals(f.getFieldName())); } + public Field getFieldByName(final String name) { + return Iterables.find(getFields(), f -> name.equals(f.getFieldName()), null); + } + private void addField(final String name, final Object value) { addField(name, value, null); } diff --git a/src/main/java/im/conversations/android/xmpp/model/data/Field.java b/src/main/java/im/conversations/android/xmpp/model/data/Field.java index f3f72fab86e7924e107674d02de4707030ebbf13..0c6b96dff7f5c8c37590fe212937560314e99a2d 100644 --- a/src/main/java/im/conversations/android/xmpp/model/data/Field.java +++ b/src/main/java/im/conversations/android/xmpp/model/data/Field.java @@ -1,6 +1,8 @@ package im.conversations.android.xmpp.model.data; -import eu.siacs.conversations.xml.Element; + import com.google.common.collect.Collections2; +import com.google.common.collect.Iterables; +import eu.siacs.conversations.xml.Element; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; import java.util.Collection; @@ -19,6 +21,10 @@ public class Field extends Extension { return Collections2.transform(getExtensions(Value.class), Element::getContent); } + public String getValue() { + return Iterables.getFirst(getValues(), null); + } + public void setFieldName(String name) { this.setAttribute("var", name); } diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java index 55f104e25bb547419e1e74ffa077ec563a354ea3..da5bc7f7bc8502371e55521dcf8df66cf816acb0 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/info/InfoQuery.java @@ -1,9 +1,12 @@ package im.conversations.android.xmpp.model.disco.info; +import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import im.conversations.android.xmpp.model.data.Data; import java.util.Collection; +import java.util.Objects; @XmlElement(name = "query") public class InfoQuery extends Extension { @@ -35,4 +38,39 @@ public class InfoQuery extends Extension { public boolean hasIdentityWithCategory(final String category) { return Iterables.any(getIdentities(), i -> category.equals(i.getCategory())); } + + public boolean hasIdentityWithCategoryAndType(final String category, final String type) { + return Iterables.any( + getIdentities(), i -> category.equals(i.getCategory()) && type.equals(i.getType())); + } + + public Collection getFeatureStrings() { + return Collections2.filter( + Collections2.transform(getFeatures(), Feature::getVar), Objects::nonNull); + } + + public Collection getServiceDiscoveryExtensions() { + return getExtensions(Data.class); + } + + public Data getServiceDiscoveryExtension(final String formType) { + return Iterables.find( + getServiceDiscoveryExtensions(), e -> formType.equals(e.getFormType()), null); + } + + public String getServiceDiscoveryExtension(final String formType, final String fieldName) { + final var extension = + Iterables.find( + getServiceDiscoveryExtensions(), + e -> formType.equals(e.getFormType()), + null); + if (extension == null) { + return null; + } + final var field = extension.getFieldByName(fieldName); + if (field == null) { + return null; + } + return field.getValue(); + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java index 129660b000cd1565956f10703afe828599eb8bf8..0887fde2c649978fec81669efd7ba2673d519e4f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java +++ b/src/main/java/im/conversations/android/xmpp/model/stanza/Presence.java @@ -1,7 +1,10 @@ package im.conversations.android.xmpp.model.stanza; +import com.google.common.base.Strings; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.capabilties.EntityCapabilities; +import im.conversations.android.xmpp.model.jabber.Show; +import im.conversations.android.xmpp.model.jabber.Status; @XmlElement public class Presence extends Stanza implements EntityCapabilities { @@ -9,4 +12,63 @@ public class Presence extends Stanza implements EntityCapabilities { public Presence() { super(Presence.class); } + + public Availability getAvailability() { + final var show = getExtension(Show.class); + if (show == null) { + return Availability.ONLINE; + } + return Availability.valueOfShown(show.getContent()); + } + + public void setAvailability(final Availability availability) { + if (availability == null || availability == Availability.ONLINE) { + return; + } + this.addExtension(new Show()).setContent(availability.toShowString()); + } + + public void setStatus(final String status) { + if (Strings.isNullOrEmpty(status)) { + return; + } + this.addExtension(new Status()).setContent(status); + } + + public String getStatus() { + final var status = getExtension(Status.class); + return status == null ? null : status.getContent(); + } + + public enum Availability { + CHAT, + ONLINE, + AWAY, + XA, + DND, + OFFLINE; + + public String toShowString() { + return switch (this) { + case CHAT -> "chat"; + case AWAY -> "away"; + case XA -> "xa"; + case DND -> "dnd"; + default -> null; + }; + } + + public static Availability valueOfShown(final String content) { + if (Strings.isNullOrEmpty(content)) { + return Availability.ONLINE; + } + return switch (content) { + case "away" -> Availability.AWAY; + case "xa" -> Availability.XA; + case "dnd" -> Availability.DND; + case "chat" -> Availability.CHAT; + default -> Availability.ONLINE; + }; + } + } } diff --git a/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java new file mode 100644 index 0000000000000000000000000000000000000000..021978dfa354e0edb4e3debbda0c89d7978d267e --- /dev/null +++ b/src/test/java/im/conversations/android/xmpp/EntityCapabilitiesTest.java @@ -0,0 +1,339 @@ +package im.conversations.android.xmpp; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNull; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.manager.DiscoManager; +import im.conversations.android.xml.XmlElementReader; +import im.conversations.android.xmpp.model.disco.info.InfoQuery; +import im.conversations.android.xmpp.model.stanza.Iq; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.ConscryptMode; + +@RunWith(RobolectricTestRunner.class) +@ConscryptMode(ConscryptMode.Mode.OFF) +public class EntityCapabilitiesTest { + + @Test + public void entityCaps() throws IOException { + final String xml = + """ + + + + + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", var); + } + + @Test + public void entityCapsComplexExample() throws IOException { + final String xml = + """ + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + ipv4 + ipv6 + + + Mac + + + 10.5.1 + + + Psi + + + 0.11 + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", var); + } + + @Test + public void entityCapsOpenFire() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Linux + + + 4.14.355-276.618.amzn2.x86_64 amd64 - Java 17.0.14 + + + Openfire + + + 5.0.0 Alpha + + + + + http://jabber.org/network/serverinfo + + + xmpp:dwd@dave.cridland.net + xmpp:akrherz@igniterealtime.org + xmpp:benjamin@igniterealtime.org + mailto:benjamin@holyarmy.org + xmpp:csh@igniterealtime.org + xmpp:dan.caseley@igniterealtime.org + mailto:dan.caseley@surevine.com + xmpp:flow@igniterealtime.org + xmpp:gdt@igniterealtime.org + mailto:greg.d.thomas@gmail.com + xmpp:guus.der.kinderen@igniterealtime.org + mailto:guus.der.kinderen@gmail.com + xmpp:lg@igniterealtime.org + xmpp:rcollier@igniterealtime.org + mailto:robincollier@hotmail.com + + + + +"""; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(Iq.class)); + final var iq = (Iq) element; + final InfoQuery info = iq.getExtension(InfoQuery.class); + final String var = EntityCapabilities.hash(info).encoded(); + Assert.assertEquals("Cd91QBSG4JGOCEvRsSz64xeJPMk=", var); + } + + @Test + public void caps2() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities2.hash(info).encoded(); + Assert.assertEquals("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=", var); + } + + @Test + public void caps2complex() throws IOException { + final String xml = + """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Tkabber + + + 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2) + + + Windows + + + XP + + + """; + final Element element = XmlElementReader.read(xml.getBytes(StandardCharsets.UTF_8)); + assertThat(element, instanceOf(InfoQuery.class)); + final InfoQuery info = (InfoQuery) element; + final String var = EntityCapabilities2.hash(info).encoded(); + Assert.assertEquals("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=", var); + } + + @Test + public void parseCaps2Node() { + final var caps = + DiscoManager.buildHashFromNode( + "urn:xmpp:caps#sha-256.u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY="); + assertThat(caps, instanceOf(EntityCapabilities2.EntityCaps2Hash.class)); + } + + @Test + public void parseCaps2NodeMissingHash() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#sha-256."); + assertNull(caps); + } + + @Test + public void parseCaps2NodeInvalid() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#-"); + assertNull(caps); + } + + @Test + public void parseCaps2NodeUnknownAlgo() { + final var caps = DiscoManager.buildHashFromNode("urn:xmpp:caps#test.test"); + assertNull(caps); + } +}