diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a991842efe4354883a8e783147f3926f687c810..d048e41491a2b6c7be8e58ad7a8d07f6375b7782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### Version 2.17.12 + +* Fix crash on file transfer in fi translation + +### Version 2.17.11 + +* minor bug fixes + ### Version 2.17.10 * Allow audio recording to be pause by tapping the timer diff --git a/build.gradle b/build.gradle index 5523135847616be46a4df960d5d2618b38e2e6a9..f926635eca3a7c7cf59d9af28eb72d93531aba8e 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation "androidx.core:core:1.10.1" - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' implementation project(':libs:annotation') annotationProcessor project(':libs:annotation-processor') @@ -84,7 +84,7 @@ dependencies { implementation 'androidx.cardview:cardview:1.0.0' implementation "androidx.preference:preference:1.2.1" implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - implementation 'com.google.android.material:material:1.13.0-alpha09' + implementation 'com.google.android.material:material:1.13.0-alpha10' implementation 'androidx.work:work-runtime:2.9.1' implementation "androidx.emoji2:emoji2:1.5.0" diff --git a/fastlane/metadata/android/en-US/changelogs/4213304.txt b/fastlane/metadata/android/en-US/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..928fa5f5fab22711cadc2fc4a99f5b0322b9df02 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4213304.txt @@ -0,0 +1 @@ +* minor bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/4213404.txt b/fastlane/metadata/android/en-US/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..607f1d08026b376edfab14f1400972a44c6ce22f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4213404.txt @@ -0,0 +1 @@ +* Fix crash on file transfer in fi translation diff --git a/fastlane/metadata/android/et/changelogs/349.txt b/fastlane/metadata/android/et/changelogs/349.txt new file mode 100644 index 0000000000000000000000000000000000000000..166160ba9cfe4d200d42eb96280667b20b38501a --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/349.txt @@ -0,0 +1,4 @@ +* Lisasime seadistuse asjatundjatele, kes soovivad teha kanalite tuvastamist search.jabber.network asemel kohalikus serveris +* Kohaletoimetamise märkeruudud on nüüd vaikimisi kasutusel ja eemaldasime vastava seadistuse +* Lülitasime „Saatmisnupp näitab olekut“ vakimisi sisse ja eemaldasime vastava seadistuse +* Tõstsime Varunduse ja Esiplaani teenuse seadistused põhivaatesse diff --git a/fastlane/metadata/android/et/changelogs/351.txt b/fastlane/metadata/android/et/changelogs/351.txt new file mode 100644 index 0000000000000000000000000000000000000000..5981723dbc1b1fa58ba2b1cced218ac4d0510a38 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/351.txt @@ -0,0 +1,3 @@ +* parandused failide edastamisel Jingle IBB abil +* parandused, kus korduvad sõnumite muutmised ummistasid andmebaasi +* võtsime kasutusele „Last Message Correction“ versiooni 1.1 diff --git a/fastlane/metadata/android/et/changelogs/353.txt b/fastlane/metadata/android/et/changelogs/353.txt new file mode 100644 index 0000000000000000000000000000000000000000..e815c62fcc48fe95e668694ce9bce51df65d93f7 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/353.txt @@ -0,0 +1,4 @@ +* kasutajad saavad nüüs ise oma hüüdnime lisada +* jälle on võimalik alla laadida faile, kasutades OMEMO krüptimist +* kanalite tunnuspildid kasutavad nüüd # sümbolit +* Quicksy kasutab vaikimisi „alati“ OMEMO krüptimist (sellega läheb peitu ka luku ikoon) diff --git a/fastlane/metadata/android/et/changelogs/360.txt b/fastlane/metadata/android/et/changelogs/360.txt new file mode 100644 index 0000000000000000000000000000000000000000..4323c8142b4cf6b1e8323dea565efb6e78c3486b --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/360.txt @@ -0,0 +1 @@ +* Uute XMPP uri parameetrite tugi: ?register ja ?register;preauth diff --git a/fastlane/metadata/android/et/changelogs/362.txt b/fastlane/metadata/android/et/changelogs/362.txt new file mode 100644 index 0000000000000000000000000000000000000000..1988e6517563ab94af14e3f5b5fb8dbe55c234bf --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/362.txt @@ -0,0 +1 @@ +* Kujunduse automaatse vahetamise tugi Android 10 puhul diff --git a/fastlane/metadata/android/et/changelogs/364.txt b/fastlane/metadata/android/et/changelogs/364.txt new file mode 100644 index 0000000000000000000000000000000000000000..171c3db84039fab4d22cce6c00d9cc86106cb832 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/364.txt @@ -0,0 +1,2 @@ +* pdf-failide eelvaade Androidi versioonis 5 ja uuemas +* kasutame OMEMO puhul 12-baidiseid IV-sid diff --git a/fastlane/metadata/android/et/changelogs/367.txt b/fastlane/metadata/android/et/changelogs/367.txt new file mode 100644 index 0000000000000000000000000000000000000000..35fcae076d078398af9b688825772b498fb7af2e --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/367.txt @@ -0,0 +1,2 @@ +* Parandasime tunnuspiltide valimise mõnedes Android 10 seadmetes +* Parandasime suuremate failide edastamise vead diff --git a/fastlane/metadata/android/et/changelogs/379.txt b/fastlane/metadata/android/et/changelogs/379.txt new file mode 100644 index 0000000000000000000000000000000000000000..495bb0be00b5eae69a9309863bafe60bd4c8770d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/379.txt @@ -0,0 +1 @@ +* Lisasime hääl- ja videokõnede võimaluse (eeldab, et server oskab kasutada STUN ja TURN servereid, mis on leitavad XEP-0215 alusel) diff --git a/fastlane/metadata/android/et/changelogs/381.txt b/fastlane/metadata/android/et/changelogs/381.txt new file mode 100644 index 0000000000000000000000000000000000000000..5e124e02998b72454ce1900766c8abb78d92585c --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/381.txt @@ -0,0 +1,2 @@ +* Kuuldav tagasiside (helistame, kõne algas, kõne lõppes) häälkõnede puhul. +* Parandsime vea, mis tekkis ebaõnnestunud videokõne kordamisel diff --git a/fastlane/metadata/android/et/changelogs/382.txt b/fastlane/metadata/android/et/changelogs/382.txt new file mode 100644 index 0000000000000000000000000000000000000000..2ce7697b623709fbe75707baaf0b4f6ff7f59fe5 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/382.txt @@ -0,0 +1,2 @@ +* Lisasime nupu kaamera vahetamiseks videokõne ajal +* Parandasime häälkõnede toimimise tahvelarvutites diff --git a/fastlane/metadata/android/et/changelogs/397.txt b/fastlane/metadata/android/et/changelogs/397.txt new file mode 100644 index 0000000000000000000000000000000000000000..1656798be6346be41f56d4f1df31a171b69ea9f8 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/397.txt @@ -0,0 +1,3 @@ +* GPX-failide tugi +* Parandasime varukoopia taastamise jõudlust +* Veaparandused diff --git a/fastlane/metadata/android/et/changelogs/404.txt b/fastlane/metadata/android/et/changelogs/404.txt new file mode 100644 index 0000000000000000000000000000000000000000..e1a8ed181d560db32d4aadcc12fc7adf2b347bbd --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/404.txt @@ -0,0 +1 @@ +* väikesed stabiilsusega seotud veaparandused audio/video funktsionaalsuses diff --git a/fastlane/metadata/android/et/changelogs/405.txt b/fastlane/metadata/android/et/changelogs/405.txt new file mode 100644 index 0000000000000000000000000000000000000000..12e754dad2c24073e934e6557352a96546c97d2a --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/405.txt @@ -0,0 +1 @@ +* Quicksy: lisandus kinnituseks saadetud SMS'ide automaatne vastuvõtmine diff --git a/fastlane/metadata/android/et/changelogs/42015.txt b/fastlane/metadata/android/et/changelogs/42015.txt new file mode 100644 index 0000000000000000000000000000000000000000..41971b0dee5971719547ddefe6a979759984cef6 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42015.txt @@ -0,0 +1 @@ +* väikesed veaparandused audio/video funktsionaalsuses diff --git a/fastlane/metadata/android/et/changelogs/42038.txt b/fastlane/metadata/android/et/changelogs/42038.txt new file mode 100644 index 0000000000000000000000000000000000000000..c8b0b4406c9db6808200485e4911b8137471933d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42038.txt @@ -0,0 +1,2 @@ +* Väikesed veaparandused +* Taastasime võimaluse helistada välja JMP ja muude teenuste abil (Playstore'i versioonis) diff --git a/fastlane/metadata/android/et/changelogs/42041.txt b/fastlane/metadata/android/et/changelogs/42041.txt new file mode 100644 index 0000000000000000000000000000000000000000..c7a8e60c87c76229be20a6e0e8eeee7ddcfc5531 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42041.txt @@ -0,0 +1,5 @@ +* Ühenduste kiirema taastamise nimel võtsime kasutusele Extensible SASL Profile, Bind 2.0 ja Fast protokollid +* Võtsime kasutusele ühenduskanalite sidumise +* Lisasime võimaluse lülituda häälkõnel ümber videokõnele +* Lisasime võimaluse oma tunnuspildi kustutamiseks +* Lisasime märkamata jäänud kõnede teavituse diff --git a/fastlane/metadata/android/et/changelogs/42042.txt b/fastlane/metadata/android/et/changelogs/42042.txt new file mode 100644 index 0000000000000000000000000000000000000000..82b99e472a3f3da9ac7018502b697e7e25680f59 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Parandasime kordussaatmise tsükli serverites, millel on vaid sm:2 tugi +* Näitame „Lülita ümber videole“ nuppu vaid siis, kui teine osapool toetab videovestlust diff --git a/fastlane/metadata/android/et/changelogs/4213304.txt b/fastlane/metadata/android/et/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..a0571dea82198717c9e2096ead3c152460df6c50 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4213304.txt @@ -0,0 +1 @@ +* väikesed veaparandused diff --git a/fastlane/metadata/android/et/changelogs/4213404.txt b/fastlane/metadata/android/et/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..55ed1677e8e1c604f4c69d681696dbd4b99eeb63 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4213404.txt @@ -0,0 +1 @@ +* Parandasime kokkujooksmise failide edastamisel soomekeelse tõlkega rakenduses diff --git a/fastlane/metadata/android/it-IT/changelogs/4212404.txt b/fastlane/metadata/android/it-IT/changelogs/4212404.txt new file mode 100644 index 0000000000000000000000000000000000000000..f553324498ea3b3f1947f3a279bd3c89c3f9a6c8 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212404.txt @@ -0,0 +1,2 @@ +* Mostra sempre il pulsante di chiamata +* Varie correzioni di errori diff --git a/fastlane/metadata/android/it-IT/changelogs/4212504.txt b/fastlane/metadata/android/it-IT/changelogs/4212504.txt new file mode 100644 index 0000000000000000000000000000000000000000..e904c43f2d2f1ca2fad8bb29e61603a8e7a148a8 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212504.txt @@ -0,0 +1 @@ +* migliorata la gestione di alcune reazioni con emoji diff --git a/fastlane/metadata/android/it-IT/changelogs/4212604.txt b/fastlane/metadata/android/it-IT/changelogs/4212604.txt new file mode 100644 index 0000000000000000000000000000000000000000..bc300b00c49fbe9dc86129e0cbd73b02a76df133 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212604.txt @@ -0,0 +1,2 @@ +* Spostati i messaggi più vicini tra di loro invece di unirli +* Aggiunta possibilità di nascondere gli avatar nelle chat quando non strettamente necessario (Impostazioni -> Interfaccia -> Messaggi di chat -> Mostra avatar) diff --git a/fastlane/metadata/android/it-IT/changelogs/4212704.txt b/fastlane/metadata/android/it-IT/changelogs/4212704.txt new file mode 100644 index 0000000000000000000000000000000000000000..3c532a0d2e2620c9c08f17fafad1fac4cacdeac8 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212704.txt @@ -0,0 +1 @@ +* Aggiunta possibilità di mostrare i messaggi allineati a sinistra diff --git a/fastlane/metadata/android/it-IT/changelogs/4212804.txt b/fastlane/metadata/android/it-IT/changelogs/4212804.txt new file mode 100644 index 0000000000000000000000000000000000000000..f8b3ac15aa1c5e3b5421677b4fb494f47fff6d2c --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212804.txt @@ -0,0 +1,3 @@ +* Accesso più facile a suoni di notifica personalizzati via Dettagli del contatto -> Menu -> Notifiche personalizzate +* Corretto destinatari delle condivisioni dirette sulle nuove versioni di Android +* Possibilità di limitare la visibilità degli avatar ai soli contatti diff --git a/fastlane/metadata/android/it-IT/changelogs/4212904.txt b/fastlane/metadata/android/it-IT/changelogs/4212904.txt new file mode 100644 index 0000000000000000000000000000000000000000..ec9bb831ee2ce91269921d094231e1dbcb8e6f06 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4212904.txt @@ -0,0 +1,2 @@ +* Corretti alcuni errori minori dell'interfaccia +* Corretti problemi di connessione con domini .onion su porte non predefinite diff --git a/fastlane/metadata/android/it-IT/changelogs/4213104.txt b/fastlane/metadata/android/it-IT/changelogs/4213104.txt new file mode 100644 index 0000000000000000000000000000000000000000..63b2ef9824fc0a3ffad7c1d92615a7c24323976c --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4213104.txt @@ -0,0 +1,2 @@ +* Utilizzo di SASL SCRAM Downgrade Protection (XEP-0474) +* Invio di reazioni a messaggi privati in gruppi al JID corretto diff --git a/fastlane/metadata/android/it-IT/changelogs/4213204.txt b/fastlane/metadata/android/it-IT/changelogs/4213204.txt new file mode 100644 index 0000000000000000000000000000000000000000..f152707bedaf558307521cd477396be3518a61dd --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4213204.txt @@ -0,0 +1,4 @@ +* Consenti di mettere in pausa registrazioni audio toccando il timer +* Corrette reazioni in messaggi privati dei gruppi +* Non accettare più 'messaggi di ripiego' per le reazioni, ricevute e display markers +* Aggiunte ulteriori icone di anteprime media diff --git a/fastlane/metadata/android/sv-SE/changelogs/42013.txt b/fastlane/metadata/android/sv-SE/changelogs/42013.txt new file mode 100644 index 0000000000000000000000000000000000000000..b496bcf7fb8551fabe83dc20384bd1cb7af6dce4 --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42013.txt @@ -0,0 +1 @@ +* Fixade problemen med 'Ingen anslutbarhet' i Android 7.1 diff --git a/fastlane/metadata/android/sv-SE/changelogs/42050.txt b/fastlane/metadata/android/sv-SE/changelogs/42050.txt new file mode 100644 index 0000000000000000000000000000000000000000..2e1bfb48446e68c364817b6642f83cc65a8ae36f --- /dev/null +++ b/fastlane/metadata/android/sv-SE/changelogs/42050.txt @@ -0,0 +1 @@ +* Öka hörnradien på profilbilder diff --git a/fastlane/metadata/android/uk/changelogs/4213204.txt b/fastlane/metadata/android/uk/changelogs/4213204.txt new file mode 100644 index 0000000000000000000000000000000000000000..02085584c6b1ce2f4da0bf6219240c1d2706fa7e --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4213204.txt @@ -0,0 +1,4 @@ +* Можливість призупиняти аудіозапис, натискаючи на таймер +* Виправлено реакції у приватних повідомлень MUC +* Припинено прийом «резервних повідомлень» для реакцій, звітів та маркерів показу +* Додано ще кілька значків попереднього перегляду медіафайлів diff --git a/fastlane/metadata/android/uk/changelogs/4213304.txt b/fastlane/metadata/android/uk/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..5055677c66cce09d060052a9941d17aecf3e6621 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4213304.txt @@ -0,0 +1 @@ +* Незначні виправлення помилок diff --git a/fastlane/metadata/android/uk/changelogs/4213404.txt b/fastlane/metadata/android/uk/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..80b025364194ad37c00235572579c092c9430b64 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4213404.txt @@ -0,0 +1 @@ +* Виправлено збій під час передавання файлів, якщо мовою додатка вибрано фінську diff --git a/fastlane/metadata/android/zh-CN/changelogs/4213304.txt b/fastlane/metadata/android/zh-CN/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..9d049011e09872903f007ac9248a9b5ab7f131e1 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4213304.txt @@ -0,0 +1 @@ +* 小错误修复 diff --git a/fastlane/metadata/android/zh-CN/changelogs/4213404.txt b/fastlane/metadata/android/zh-CN/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..d35b0abef410381b283c55fce79afc52990fe3e9 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4213404.txt @@ -0,0 +1 @@ +* 修复在芬兰语翻译中文件传输的崩溃问题 diff --git a/src/conversations/fastlane/metadata/android/sv-SE/full_description.txt b/src/conversations/fastlane/metadata/android/sv-SE/full_description.txt index c02bd4912c0f7ad4a8487a5f1360fd11f9d13c19..ca7864c971aaf60e383313a10151be2eb2ec8a95 100644 --- a/src/conversations/fastlane/metadata/android/sv-SE/full_description.txt +++ b/src/conversations/fastlane/metadata/android/sv-SE/full_description.txt @@ -28,7 +28,7 @@ Conversations fungerar med alla XMPP-servrar. Men XMPP är ett utbyggbart protok De XEP-tillägg som stöds är: -* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Används för filöverföring om båda parter är bakom en brandvägg (NAT). +* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Används för filöverföring om båda parter är bakom en brandvägg eller NAT. * XEP-0163: Personal Eventing Protocol för avatarer * XEP-0191: Blocking command låter dig svartlista spammare eller blocka kontakter utan att ta bort dem * XEP-0198: Stream Management låter XMPP att klara av mindre nätverksavbrott och förändringar i den underliggande TCP-anslutningen diff --git a/src/main/java/eu/siacs/conversations/AppSettings.java b/src/main/java/eu/siacs/conversations/AppSettings.java index 3d6f61c8e59e0f902ea8d50bf93ef246a739ff11..0ab0e265d0a01c8ec2bec81e39bb15d4d25c8362 100644 --- a/src/main/java/eu/siacs/conversations/AppSettings.java +++ b/src/main/java/eu/siacs/conversations/AppSettings.java @@ -7,6 +7,7 @@ import androidx.annotation.BoolRes; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import com.google.common.base.Strings; +import eu.siacs.conversations.services.QuickConversationsService; import java.security.SecureRandom; public class AppSettings { @@ -126,7 +127,14 @@ public class AppSettings { } public boolean isUseTor() { - return getBooleanPreference(USE_TOR, R.bool.use_tor); + return QuickConversationsService.isConversations() + && getBooleanPreference(USE_TOR, R.bool.use_tor); + } + + public boolean isExtendedConnectionOptions() { + return QuickConversationsService.isConversations() + && getBooleanPreference( + AppSettings.SHOW_CONNECTION_OPTIONS, R.bool.show_connection_options); } private boolean getBooleanPreference(@NonNull final String name, @BoolRes int res) { diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java index 6fc4b11ceed745cec500ff0e62759359d49792ba..0c8d9fb31ef92c02232e5d2898a920bee2ca312f 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java @@ -1,8 +1,9 @@ package eu.siacs.conversations.crypto.sasl; -import javax.net.ssl.SSLSocket; - +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import eu.siacs.conversations.entities.Account; +import javax.net.ssl.SSLSocket; public class Anonymous extends SaslMechanism { @@ -24,6 +25,20 @@ public class Anonymous extends SaslMechanism { @Override public String getClientFirstMessage(final SSLSocket sslSocket) { + Preconditions.checkState( + this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state"); + this.state = State.AUTH_TEXT_SENT; return ""; } + + @Override + public String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException { + checkState(State.AUTH_TEXT_SENT); + if (Strings.isNullOrEmpty(challenge)) { + this.state = State.VALID_SERVER_RESPONSE; + return null; + } + throw new AuthenticationException("Unexpected server response"); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java index b75d0883fd4a5bdbf6634a2cddf5054c40c02e26..0916b953f35a20021c39a991a5e5de239c3616a9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -1,20 +1,25 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Base64; - -import java.nio.charset.Charset; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.SSLSocket; - +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.hash.Hashing; +import com.google.common.io.BaseEncoding; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; +import java.nio.charset.Charset; +import java.util.Map; +import javax.net.ssl.SSLSocket; public class DigestMd5 extends SaslMechanism { public static final String MECHANISM = "DIGEST-MD5"; private State state = State.INITIAL; + private String precalculatedRSPAuth; public DigestMd5(final Account account) { super(account); @@ -31,84 +36,150 @@ public class DigestMd5 extends SaslMechanism { } @Override - public String getResponse(final String challenge, final SSLSocket sslSocket) + public String getClientFirstMessage(final SSLSocket sslSocket) { + Preconditions.checkState( + this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state"); + this.state = State.AUTH_TEXT_SENT; + return ""; + } + + @Override + public String getResponse(final String challenge, final SSLSocket socket) + throws AuthenticationException { + return switch (state) { + case AUTH_TEXT_SENT -> processChallenge(challenge, socket); + case RESPONSE_SENT -> validateServerResponse(challenge); + case VALID_SERVER_RESPONSE -> validateUnnecessarySuccessMessage(challenge); + default -> throw new InvalidStateException(state); + }; + } + + // ejabberd sends the RSPAuth response as a challenge and then an empty success + // technically this is allowed as per https://datatracker.ietf.org/doc/html/rfc2222#section-5.2 + // although it says to do that only if the profile of the protocol does not allow data to be put + // into success. which xmpp does allow. obviously + private String validateUnnecessarySuccessMessage(final String challenge) + throws AuthenticationException { + if (Strings.isNullOrEmpty(challenge)) { + return ""; + } + throw new AuthenticationException("Success message must be empty"); + } + + private String validateServerResponse(final String challenge) throws AuthenticationException { + Log.d(Config.LOGTAG, "DigestMd5.validateServerResponse(" + challenge + ")"); + final var attributes = messageToAttributes(challenge); + Log.d(Config.LOGTAG, "attributes: " + attributes); + final var rspauth = attributes.get("rspauth"); + if (Strings.isNullOrEmpty(rspauth)) { + throw new AuthenticationException("no rspauth in server finish message"); + } + final var expected = this.precalculatedRSPAuth; + if (Strings.isNullOrEmpty(expected) || !this.precalculatedRSPAuth.equals(rspauth)) { + throw new AuthenticationException("RSPAuth mismatch"); + } + this.state = State.VALID_SERVER_RESPONSE; + return ""; + } + + private String processChallenge(final String challenge, final SSLSocket socket) throws AuthenticationException { - switch (state) { - case INITIAL: - state = State.RESPONSE_SENT; - final String encodedResponse; - try { - final Tokenizer tokenizer = - new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); - String nonce = ""; - for (final String token : tokenizer) { - final String[] parts = token.split("=", 2); - if (parts[0].equals("nonce")) { - nonce = parts[1].replace("\"", ""); - } else if (parts[0].equals("rspauth")) { - return ""; - } - } - final String digestUri = "xmpp/" + account.getServer(); - final String nonceCount = "00000001"; - final String x = - account.getUsername() - + ":" - + account.getServer() - + ":" - + account.getPassword(); - final MessageDigest md = MessageDigest.getInstance("MD5"); - final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = CryptoHelper.random(100); - final byte[] a1 = - CryptoHelper.concatenateByteArrays( - y, - (":" + nonce + ":" + cNonce) - .getBytes(Charset.defaultCharset())); - final String a2 = "AUTHENTICATE:" + digestUri; - final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = - CryptoHelper.bytesToHex( - md.digest(a2.getBytes(Charset.defaultCharset()))); - final String kd = - ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2; - final String response = - CryptoHelper.bytesToHex( - md.digest(kd.getBytes(Charset.defaultCharset()))); - final String saslString = - "username=\"" - + account.getUsername() - + "\",realm=\"" - + account.getServer() - + "\",nonce=\"" - + nonce - + "\",cnonce=\"" - + cNonce - + "\",nc=" - + nonceCount - + ",qop=auth,digest-uri=\"" - + digestUri - + "\",response=" - + response - + ",charset=utf-8"; - encodedResponse = - Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException(e); - } - - return encodedResponse; - case RESPONSE_SENT: - state = State.VALID_SERVER_RESPONSE; - break; - case VALID_SERVER_RESPONSE: - if (challenge == null) { - return null; // everything is fine - } - default: - throw new InvalidStateException(state); + Log.d(Config.LOGTAG, "DigestMd5.processChallenge()"); + this.state = State.RESPONSE_SENT; + final var attributes = messageToAttributes(challenge); + + final var nonce = attributes.get("nonce"); + + if (Strings.isNullOrEmpty(nonce)) { + throw new AuthenticationException("Server nonce missing"); + } + final String digestUri = "xmpp/" + account.getServer(); + final String nonceCount = "00000001"; + final String x = + account.getUsername() + ":" + account.getServer() + ":" + account.getPassword(); + final byte[] y = Hashing.md5().hashBytes(x.getBytes(Charset.defaultCharset())).asBytes(); + final String cNonce = CryptoHelper.random(100); + final byte[] a1 = + CryptoHelper.concatenateByteArrays( + y, (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String a2 = "AUTHENTICATE:" + digestUri; + final String ha1 = CryptoHelper.bytesToHex(Hashing.md5().hashBytes(a1).asBytes()); + final String ha2 = + CryptoHelper.bytesToHex( + Hashing.md5().hashBytes(a2.getBytes(Charset.defaultCharset())).asBytes()); + final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2; + + final String a2ForResponse = ":" + digestUri; + final String ha2ForResponse = + CryptoHelper.bytesToHex( + Hashing.md5() + .hashBytes(a2ForResponse.getBytes(Charset.defaultCharset())) + .asBytes()); + final String kdForResponseInput = + ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2ForResponse; + + this.precalculatedRSPAuth = + CryptoHelper.bytesToHex( + Hashing.md5() + .hashBytes(kdForResponseInput.getBytes(Charset.defaultCharset())) + .asBytes()); + + final String response = + CryptoHelper.bytesToHex( + Hashing.md5().hashBytes(kd.getBytes(Charset.defaultCharset())).asBytes()); + + final String saslString = + "username=\"" + + account.getUsername() + + "\",realm=\"" + + account.getServer() + + "\",nonce=\"" + + nonce + + "\",cnonce=\"" + + cNonce + + "\",nc=" + + nonceCount + + ",qop=auth,digest-uri=\"" + + digestUri + + "\",response=" + + response + + ",charset=utf-8"; + return BaseEncoding.base64().encode(saslString.getBytes()); + } + + private static Map messageToAttributes(final String message) + throws AuthenticationException { + byte[] asBytes; + try { + asBytes = BaseEncoding.base64().decode(message); + } catch (final IllegalArgumentException e) { + throw new AuthenticationException("Unable to decode server challenge", e); + } + try { + return splitToAttributes(new String(asBytes)); + } catch (final IllegalArgumentException e) { + throw new AuthenticationException("Duplicate attributes"); + } + } + + private static Map splitToAttributes(final String message) { + final ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (final String token : Splitter.on(',').split(message)) { + final var tuple = Splitter.on('=').limit(2).splitToList(token); + if (tuple.size() == 2) { + final var value = tuple.get(1); + builder.put(tuple.get(0), trimQuotes(value)); + } + } + return builder.buildOrThrow(); + } + + public static String trimQuotes(@NonNull final String input) { + if (input.length() >= 2 + && input.charAt(0) == '"' + && input.charAt(input.length() - 1) == '"') { + return input.substring(1, input.length() - 1); } - return null; + return input; } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index 2e8adf1892d5332340e2a9fb28872223807f34df..056f022d1a091e47d5da91b0e083a7e2adf1654c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Base64; +import com.google.common.base.Preconditions; +import com.google.common.io.BaseEncoding; import eu.siacs.conversations.entities.Account; import javax.net.ssl.SSLSocket; @@ -24,7 +25,17 @@ public class External extends SaslMechanism { @Override public String getClientFirstMessage(final SSLSocket sslSocket) { - return Base64.encodeToString( - account.getJid().asBareJid().toString().getBytes(), Base64.NO_WRAP); + Preconditions.checkState( + this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state"); + this.state = State.AUTH_TEXT_SENT; + final String message = account.getJid().asBareJid().toString(); + return BaseEncoding.base64().encode(message.getBytes()); + } + + @Override + public String getResponse(String challenge, SSLSocket sslSocket) + throws AuthenticationException { + // TODO check that state is in auth text sent and move to finished + return ""; } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index 2be5d0bcb2a09482653b88df5ce4427adc6fe2ec..9729abed5f51fc39df98d8a162e4bb7cb8b75974 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -1,12 +1,10 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Base64; - -import java.nio.charset.Charset; - -import javax.net.ssl.SSLSocket; - +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; import eu.siacs.conversations.entities.Account; +import javax.net.ssl.SSLSocket; public class Plain extends SaslMechanism { @@ -16,11 +14,6 @@ public class Plain extends SaslMechanism { super(account); } - public static String getMessage(String username, String password) { - final String message = '\u0000' + username + '\u0000' + password; - return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } - @Override public int getPriority() { return 10; @@ -33,6 +26,25 @@ public class Plain extends SaslMechanism { @Override public String getClientFirstMessage(final SSLSocket sslSocket) { + Preconditions.checkState( + this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state"); + this.state = State.AUTH_TEXT_SENT; return getMessage(account.getUsername(), account.getPassword()); } + + public static String getMessage(final String username, final String password) { + final String message = '\u0000' + username + '\u0000' + password; + return BaseEncoding.base64().encode(message.getBytes()); + } + + @Override + public String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException { + checkState(State.AUTH_TEXT_SENT); + if (Strings.isNullOrEmpty(challenge)) { + this.state = State.VALID_SERVER_RESPONSE; + return null; + } + throw new AuthenticationException("Unexpected server response"); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 40f48a2392d6523ab4a6d573c1b116033b652f7a..0aa147f95908d87beee26721118b03a089d0941d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -16,6 +16,8 @@ public abstract class SaslMechanism { protected final Account account; + protected State state = State.INITIAL; + protected SaslMechanism(final Account account) { this.account = account; } @@ -39,14 +41,10 @@ public abstract class SaslMechanism { public abstract String getMechanism(); - public String getClientFirstMessage(final SSLSocket sslSocket) { - return ""; - } + public abstract String getClientFirstMessage(final SSLSocket sslSocket); - public String getResponse(final String challenge, final SSLSocket sslSocket) - throws AuthenticationException { - return ""; - } + public abstract String getResponse(final String challenge, final SSLSocket sslSocket) + throws AuthenticationException; public enum State { INITIAL, @@ -55,6 +53,17 @@ public abstract class SaslMechanism { VALID_SERVER_RESPONSE, } + protected void checkState(final State expected) throws InvalidStateException { + final var current = this.state; + if (current == null) { + throw new InvalidStateException("Current state is null. Implementation problem"); + } + if (current != expected) { + throw new InvalidStateException( + String.format("State was %s. Expected %s", current, expected)); + } + } + public enum Version { SASL, SASL_2; @@ -120,8 +129,7 @@ public abstract class SaslMechanism { return new ScramSha256(account); } else if (mechanisms.contains(ScramSha1.MECHANISM)) { return new ScramSha1(account); - } else if (mechanisms.contains(Plain.MECHANISM) - && !account.getServer().equals("nimbuzz.com")) { + } else if (mechanisms.contains(Plain.MECHANISM)) { return new Plain(account); } else if (mechanisms.contains(DigestMd5.MECHANISM)) { return new DigestMd5(account); diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 0ee9b879c40fce893ab391d1182e727c8e9a86f7..db6f717033d6549b02a21bd1c24e99acea4dc487 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -48,7 +48,6 @@ public abstract class ScramMechanism extends SaslMechanism { protected final ChannelBinding channelBinding; private final String gs2Header; private final String clientNonce; - protected State state = State.INITIAL; private final String clientFirstMessageBare; private byte[] serverSignature = null; private DowngradeProtection downgradeProtection = null; diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 07974dab3af466f77c5dce788ce8e152ac0d58c1..d7507cec87e7a803cd1a0e57fd6d23f72e99f669 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -38,6 +38,7 @@ import eu.siacs.conversations.crypto.sasl.HashedTokenSha512; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.xml.Element; @@ -129,7 +130,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable null, null, null, - 5222, + Resolver.XMPP_PORT_STARTTLS, Presence.Status.ONLINE, null, null, @@ -355,6 +356,11 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return server != null && server.endsWith(".onion"); } + public boolean isDirectToOnion() { + final var hostname = Strings.nullToEmpty(this.hostname).trim(); + return isOnion() && (hostname.isEmpty() || hostname.endsWith(".onion")); + } + public int getPort() { return this.port; } @@ -863,6 +869,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable REGISTRATION_PASSWORD_TOO_WEAK(true, false), TLS_ERROR, TLS_ERROR_DOMAIN, + CHANNEL_BINDING, INCOMPATIBLE_SERVER, INCOMPATIBLE_CLIENT, TOR_NOT_AVAILABLE, @@ -941,6 +948,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable return R.string.account_status_incompatible_server; case INCOMPATIBLE_CLIENT: return R.string.account_status_incompatible_client; + case CHANNEL_BINDING: + return R.string.account_status_channel_binding; case TOR_NOT_AVAILABLE: return R.string.account_status_tor_unavailable; case BIND_FAILURE: diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 477871fe77f39a10d1897e6a39e40c606df1eabc..53d1e25730f643c765a60db2ff04190ca1e3cf59 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -88,6 +88,8 @@ 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; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -99,7 +101,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; public class DatabaseBackend extends SQLiteOpenHelper { private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 52; + private static final int DATABASE_VERSION = 53; private static boolean requiresMessageIndexRebuild = false; private static DatabaseBackend instance = null; @@ -1251,6 +1253,37 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.REACTIONS + " TEXT"); } + if (oldVersion < 53 && newVersion >= 53) { + try (final Cursor cursor = + db.query( + Account.TABLENAME, + new String[] {Account.UUID, Account.USERNAME}, + null, + null, + null, + null, + null)) { + while (cursor != null && cursor.moveToNext()) { + final var uuid = cursor.getString(0); + final var username = cursor.getString(1); + final Localpart localpart; + try { + localpart = Localpart.fromUnescaped(username); + } catch (final XmppStringprepException e) { + Log.d(Config.LOGTAG, "unable to parse jid"); + continue; + } + final var contentValues = new ContentValues(); + contentValues.putNull(Account.ROSTERVERSION); + contentValues.put(Account.USERNAME, localpart.toString()); + db.update( + Account.TABLENAME, + contentValues, + Account.UUID + "=?", + new String[] {uuid}); + } + } + } } private void canonicalizeJids(SQLiteDatabase db) { @@ -1262,21 +1295,24 @@ public class DatabaseBackend extends SQLiteOpenHelper { String newJid; try { newJid = - Jid.of(cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))) + Jid.of( + cursor.getString( + cursor.getColumnIndexOrThrow( + Conversation.CONTACTJID))) .toString(); - } catch (IllegalArgumentException ignored) { + } catch (final IllegalArgumentException e) { Log.e( Config.LOGTAG, "Failed to migrate Conversation CONTACTJID " - + cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID)) - + ": " - + ignored - + ". Skipping..."); + + cursor.getString( + cursor.getColumnIndexOrThrow(Conversation.CONTACTJID)) + + ". Skipping...", + e); continue; } final String[] updateArgs = { - newJid, cursor.getString(cursor.getColumnIndex(Conversation.UUID)), + newJid, cursor.getString(cursor.getColumnIndexOrThrow(Conversation.UUID)), }; db.execSQL( "update " @@ -1296,12 +1332,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { while (cursor.moveToNext()) { String newJid; try { - newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Contact.JID))).toString(); + newJid = + Jid.of(cursor.getString(cursor.getColumnIndexOrThrow(Contact.JID))) + .toString(); } catch (final IllegalArgumentException e) { Log.e( Config.LOGTAG, "Failed to migrate Contact JID " - + cursor.getString(cursor.getColumnIndex(Contact.JID)) + + cursor.getString(cursor.getColumnIndexOrThrow(Contact.JID)) + ": Skipping...", e); continue; @@ -1309,8 +1347,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { final String[] updateArgs = { newJid, - cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)), - cursor.getString(cursor.getColumnIndex(Contact.JID)), + cursor.getString(cursor.getColumnIndexOrThrow(Contact.ACCOUNT)), + cursor.getString(cursor.getColumnIndexOrThrow(Contact.JID)), }; db.execSQL( "update " @@ -1335,24 +1373,25 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { newServer = Jid.of( - cursor.getString(cursor.getColumnIndex(Account.USERNAME)), - cursor.getString(cursor.getColumnIndex(Account.SERVER)), + cursor.getString( + cursor.getColumnIndexOrThrow(Account.USERNAME)), + cursor.getString( + cursor.getColumnIndexOrThrow(Account.SERVER)), null) .getDomain() .toString(); - } catch (IllegalArgumentException ignored) { + } catch (final IllegalArgumentException e) { Log.e( Config.LOGTAG, "Failed to migrate Account SERVER " - + cursor.getString(cursor.getColumnIndex(Account.SERVER)) - + ": " - + ignored - + ". Skipping..."); + + cursor.getString(cursor.getColumnIndexOrThrow(Account.SERVER)) + + ". Skipping...", + e); continue; } String[] updateArgs = { - newServer, cursor.getString(cursor.getColumnIndex(Account.UUID)), + newServer, cursor.getString(cursor.getColumnIndexOrThrow(Account.UUID)), }; db.execSQL( "update " diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 6dd42cc5f418ca7223b8f856cec83de2b6c85e88..bccd71abe82612df0a30bb1d525a2933cac69e1b 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -2127,11 +2127,15 @@ public class XmppConnectionService extends Service { } } - private void sendFileMessage(final Message message, final boolean delay, final Runnable cb) { - Log.d(Config.LOGTAG, "send file message"); - final Account account = message.getConversation().getAccount(); - if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize()) - || message.getConversation().getMode() == Conversation.MODE_MULTI) { + private void sendFileMessage( + final Message message, final boolean delay, final boolean forceP2P, final Runnable cb) { + final var account = message.getConversation().getAccount(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": send file message. forceP2P=" + forceP2P); + if ((account.httpUploadAvailable(fileBackend.getFile(message, false).getSize()) + || message.getConversation().getMode() == Conversation.MODE_MULTI) + && !forceP2P) { mHttpConnectionManager.createNewUploadConnection(message, delay, cb); } else { mJingleConnectionManager.startJingleFileTransfer(message); @@ -2140,14 +2144,24 @@ public class XmppConnectionService extends Service { } public void sendMessage(final Message message) { - sendMessage(message, false, false, false, null); + sendMessage(message, false, false, false, false, null); } public void sendMessage(final Message message, final Runnable cb) { - sendMessage(message, false, false, false, cb); + sendMessage(message, false, false, false, false, cb); } private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) { + sendMessage(message, resend, previewedLinks, delay, false, cb); + } + + private void sendMessage( + final Message message, + final boolean resend, + final boolean previewedLinks, + final boolean delay, + final boolean forceP2P, + final Runnable cb) { final Account account = message.getConversation().getAccount(); if (account.setShowErrorNotification(true)) { databaseBackend.updateAccount(account); @@ -2313,7 +2327,7 @@ public class XmppConnectionService extends Service { fileBackend.getFile(message, false).getSize()) || conversation.getMode() == Conversation.MODE_MULTI || message.fixCounterpart()) { - this.sendFileMessage(message, delay, cb); + this.sendFileMessage(message, delay, forceP2P, cb); passedCbOn = true; } else { break; @@ -2329,7 +2343,7 @@ public class XmppConnectionService extends Service { fileBackend.getFile(message, false).getSize()) || conversation.getMode() == Conversation.MODE_MULTI || message.fixCounterpart()) { - this.sendFileMessage(message, delay, cb); + this.sendFileMessage(message, delay, forceP2P, cb); passedCbOn = true; } else { break; @@ -2345,7 +2359,7 @@ public class XmppConnectionService extends Service { fileBackend.getFile(message, false).getSize()) || conversation.getMode() == Conversation.MODE_MULTI || message.fixCounterpart()) { - this.sendFileMessage(message, delay, cb); + this.sendFileMessage(message, delay, forceP2P, cb); passedCbOn = true; } else { break; @@ -2484,15 +2498,15 @@ public class XmppConnectionService extends Service { } public void resendMessage(final Message message, final boolean delay) { - sendMessage(message, true, false, delay, null); + sendMessage(message, true, false, delay, false, null); } public void resendMessage(final Message message, final boolean delay, final Runnable cb) { - sendMessage(message, true, false, delay, cb); + sendMessage(message, true, false, delay, false, cb); } public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) { - sendMessage(message, true, previewedLinks, delay, null); + sendMessage(message, true, previewedLinks, delay, false, null); } public Pair onboardingIncomplete() { @@ -3401,15 +3415,15 @@ public class XmppConnectionService extends Service { if (existing == null) { return null; } - Log.d( - Config.LOGTAG, - existing.getJid().asBareJid() - + ": restoring conversation with " - + existing.getJid() - + " from DB"); + Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB"); final Map accounts = ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid)); - existing.setAccount(accounts.get(existing.getAccountUuid())); + final var account = accounts.get(existing.getAccountUuid()); + if (account == null) { + Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid()); + return null; + } + existing.setAccount(account); final var loadMessagesFromDb = restoreFromArchive(existing); mDatabaseReaderExecutor.execute( () -> @@ -6077,10 +6091,6 @@ public class XmppConnectionService extends Service { return getBooleanPreference("use_tor", R.bool.use_tor); } - public boolean showExtendedConnectionOptions() { - return getBooleanPreference(AppSettings.SHOW_CONNECTION_OPTIONS, R.bool.show_connection_options); - } - public boolean broadcastLastActivity() { return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity); } @@ -6708,10 +6718,10 @@ public class XmppConnectionService extends Service { return this.mHttpConnectionManager; } - public void resendFailedMessages(final Message message) { + public void resendFailedMessages(final Message message, final boolean forceP2P) { message.setTime(System.currentTimeMillis()); markMessage(message, Message.STATUS_WAITING); - this.resendMessage(message, false); + this.sendMessage(message, true, false, false, forceP2P, null); if (message.getConversation() instanceof Conversation c) { c.sort(); } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 30750a3164964f66899f20802361e6634e0270ec..f63583a83297946e06eeceb6341aedc30325f816 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -662,7 +662,9 @@ public class ContactDetailsActivity extends OmemoActivity } if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) { hasKeys = true; - View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false); + View view = + inflater.inflate( + R.layout.item_device_fingerprint, binding.detailsContactKeys, false); TextView key = view.findViewById(R.id.key); TextView keyType = view.findViewById(R.id.key_type); keyType.setText(R.string.openpgp_key_id); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 2ca3b8ae84bfc9d87a68d41ab3b254d525d44f72..1079e5000e08df2cbd2f3e5545e6110f349e35fa 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1858,24 +1858,25 @@ public class ConversationFragment extends XmppFragment activity.getMenuInflater().inflate(R.menu.message_context, menu); final MenuItem reportAndBlock = menu.findItem(R.id.action_report_and_block); final MenuItem addReaction = menu.findItem(R.id.action_add_reaction); - MenuItem openWith = menu.findItem(R.id.open_with); - MenuItem copyMessage = menu.findItem(R.id.copy_message); - MenuItem quoteMessage = menu.findItem(R.id.quote_message); - MenuItem retryDecryption = menu.findItem(R.id.retry_decryption); - MenuItem correctMessage = menu.findItem(R.id.correct_message); - MenuItem retractMessage = menu.findItem(R.id.retract_message); - MenuItem moderateMessage = menu.findItem(R.id.moderate_message); - MenuItem onlyThisThread = menu.findItem(R.id.only_this_thread); - MenuItem shareWith = menu.findItem(R.id.share_with); - MenuItem sendAgain = menu.findItem(R.id.send_again); - MenuItem copyUrl = menu.findItem(R.id.copy_url); - MenuItem copyLink = menu.findItem(R.id.copy_link); - MenuItem saveAsSticker = menu.findItem(R.id.save_as_sticker); - MenuItem downloadFile = menu.findItem(R.id.download_file); - MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); - MenuItem blockMedia = menu.findItem(R.id.block_media); - MenuItem deleteFile = menu.findItem(R.id.delete_file); - MenuItem showErrorMessage = menu.findItem(R.id.show_error_message); + final MenuItem openWith = menu.findItem(R.id.open_with); + final MenuItem copyMessage = menu.findItem(R.id.copy_message); + final MenuItem quoteMessage = menu.findItem(R.id.quote_message); + final MenuItem retryDecryption = menu.findItem(R.id.retry_decryption); + final MenuItem correctMessage = menu.findItem(R.id.correct_message); + final MenuItem retractMessage = menu.findItem(R.id.retract_message); + final MenuItem moderateMessage = menu.findItem(R.id.moderate_message); + final MenuItem onlyThisThread = menu.findItem(R.id.only_this_thread); + final MenuItem shareWith = menu.findItem(R.id.share_with); + final MenuItem sendAgain = menu.findItem(R.id.send_again); + final MenuItem retryAsP2P = menu.findItem(R.id.send_again_as_p2p); + final MenuItem copyUrl = menu.findItem(R.id.copy_url); + final MenuItem copyLink = menu.findItem(R.id.copy_link); + final MenuItem saveAsSticker = menu.findItem(R.id.save_as_sticker); + final MenuItem downloadFile = menu.findItem(R.id.download_file); + final MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); + final MenuItem blockMedia = menu.findItem(R.id.block_media); + final MenuItem deleteFile = menu.findItem(R.id.delete_file); + final MenuItem showErrorMessage = menu.findItem(R.id.show_error_message); onlyThisThread.setVisible(!conversation.getLockThread() && m.getThread() != null); final boolean unInitiatedButKnownSize = MessageUtils.unInitiatedButKnownSize(m); final boolean showError = @@ -1949,6 +1950,12 @@ public class ConversationFragment extends XmppFragment } if (m.getStatus() == Message.STATUS_SEND_FAILED) { sendAgain.setVisible(true); + final var fileNotUploaded = m.isFileOrImage() && !m.hasFileOnRemoteHost(); + final var isPeerOnline = + conversational.getMode() == Conversation.MODE_SINGLE + && (conversational instanceof Conversation c) + && !c.getContact().getPresences().isEmpty(); + retryAsP2P.setVisible(fileNotUploaded && isPeerOnline); } if (m.hasFileOnRemoteHost() || m.isGeoUri() @@ -2061,7 +2068,10 @@ public class ConversationFragment extends XmppFragment quoteMessage(selectedMessage); return true; case R.id.send_again: - resendMessage(selectedMessage); + resendMessage(selectedMessage, false); + return true; + case R.id.send_again_as_p2p: + resendMessage(selectedMessage, true); return true; case R.id.copy_url: ShareUtil.copyUrlToClipboard(activity, selectedMessage); @@ -3085,12 +3095,11 @@ public class ConversationFragment extends XmppFragment builder.create().show(); } - private void resendMessage(final Message message) { + private void resendMessage(final Message message, final boolean forceP2P) { if (message.isFileOrImage()) { - if (!(message.getConversation() instanceof Conversation)) { + if (!(message.getConversation() instanceof Conversation conversation)) { return; } - final Conversation conversation = (Conversation) message.getConversation(); final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message); if ((file.exists() && file.canRead()) || message.hasFileOnRemoteHost()) { @@ -3098,14 +3107,16 @@ public class ConversationFragment extends XmppFragment if (!message.hasFileOnRemoteHost() && xmppConnection != null && conversation.getMode() == Conversational.MODE_SINGLE - && !xmppConnection - .getFeatures() - .httpUpload(message.getFileParams().getSize())) { + && (!xmppConnection + .getFeatures() + .httpUpload(message.getFileParams().getSize()) + || forceP2P)) { activity.selectPresence( conversation, () -> { message.setCounterpart(conversation.getNextCounterpart()); - activity.xmppConnectionService.resendFailedMessages(message); + activity.xmppConnectionService.resendFailedMessages( + message, forceP2P); new Handler() .post( () -> { @@ -3128,7 +3139,7 @@ public class ConversationFragment extends XmppFragment return; } } - activity.xmppConnectionService.resendFailedMessages(message); + activity.xmppConnectionService.resendFailedMessages(message, false); new Handler() .post( () -> { diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index ac7559769d4f7c0a11170095c8b38c1571410065..7ed58127c0eb7700e64fa0598c833f5f19efdd34 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -7,205 +7,241 @@ import android.view.View; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.Toast; - -import androidx.appcompat.app.AlertDialog; import androidx.databinding.DataBindingUtil; - import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; -import eu.siacs.conversations.databinding.ContactKeyBinding; +import eu.siacs.conversations.databinding.ItemDeviceFingerprintBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.XmppUri; public abstract class OmemoActivity extends XmppActivity { - private Account mSelectedAccount; - private String mSelectedFingerprint; - - protected XmppUri mPendingFingerprintVerificationUri = null; - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - Object account = v.getTag(R.id.TAG_ACCOUNT); - Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT); - Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS); - if (account instanceof Account - && fingerprint instanceof String - && fingerprintStatus instanceof FingerprintStatus) { - getMenuInflater().inflate(R.menu.omemo_key_context, menu); - MenuItem distrust = menu.findItem(R.id.distrust_key); - MenuItem verifyScan = menu.findItem(R.id.verify_scan); - if (this instanceof TrustKeysActivity) { - distrust.setVisible(false); - verifyScan.setVisible(false); - } else { - FingerprintStatus status = (FingerprintStatus) fingerprintStatus; - if (!status.isActive() || status.isVerified()) { - verifyScan.setVisible(false); - } - distrust.setVisible(status.isVerified() || (!status.isActive() && status.isTrusted())); - } - this.mSelectedAccount = (Account) account; - this.mSelectedFingerprint = (String) fingerprint; - } - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.distrust_key: - showPurgeKeyDialog(mSelectedAccount, mSelectedFingerprint); - break; - case R.id.copy_omemo_key: - copyOmemoFingerprint(mSelectedFingerprint); - break; - case R.id.verify_scan: - ScanActivity.scan(this); - break; - } - return true; - } - - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - if (requestCode == ScanActivity.REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { - String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); - XmppUri uri = new XmppUri(result == null ? "" : result); - if (xmppConnectionServiceBound) { - processFingerprintVerification(uri); - } else { - this.mPendingFingerprintVerificationUri = uri; - } - } - } - - protected abstract void processFingerprintVerification(XmppUri uri); - - protected void copyOmemoFingerprint(String fingerprint) { - if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), R.string.omemo_fingerprint)) { - Toast.makeText( - this, - R.string.toast_message_omemo_fingerprint, - Toast.LENGTH_SHORT).show(); - } - } - - protected void addFingerprintRow(LinearLayout keys, final XmppAxolotlSession session, boolean highlight) { - final Account account = session.getAccount(); - final String fingerprint = session.getFingerprint(); - addFingerprintRowWithListeners(keys, - session.getAccount(), - fingerprint, - highlight, - session.getTrust(), - true, - true, - (buttonView, isChecked) -> account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(isChecked))); - } - - protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account, - final String fingerprint, - boolean highlight, - FingerprintStatus status, - boolean showTag, - boolean undecidedNeedEnablement, - CompoundButton.OnCheckedChangeListener - onCheckedChangeListener) { - ContactKeyBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.contact_key, keys, true); - binding.tglTrust.setVisibility(View.VISIBLE); - registerForContextMenu(binding.getRoot()); - binding.getRoot().setTag(R.id.TAG_ACCOUNT, account); - binding.getRoot().setTag(R.id.TAG_FINGERPRINT, fingerprint); - binding.getRoot().setTag(R.id.TAG_FINGERPRINT_STATUS, status); - boolean x509 = Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509; - final View.OnClickListener toast; - binding.tglTrust.setChecked(status.isTrusted()); - - if (status.isActive()) { - binding.key.setTextColor(MaterialColors.getColor(binding.key, com.google.android.material.R.attr.colorOnSurface)); - binding.keyType.setTextColor(MaterialColors.getColor(binding.keyType, com.google.android.material.R.attr.colorOnSurface)); - if (status.isVerified()) { - binding.verifiedFingerprint.setVisibility(View.VISIBLE); - binding.verifiedFingerprint.setAlpha(1.0f); - binding.tglTrust.setVisibility(View.GONE); - binding.verifiedFingerprint.setOnClickListener(v -> replaceToast(getString(R.string.this_device_has_been_verified), false)); - toast = null; - } else { - binding.verifiedFingerprint.setVisibility(View.GONE); - binding.tglTrust.setVisibility(View.VISIBLE); - binding.tglTrust.setOnCheckedChangeListener(onCheckedChangeListener); - if (status.getTrust() == FingerprintStatus.Trust.UNDECIDED && undecidedNeedEnablement) { - binding.buttonEnableDevice.setVisibility(View.VISIBLE); - binding.buttonEnableDevice.setOnClickListener(v -> { - account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(false)); - binding.buttonEnableDevice.setVisibility(View.GONE); - binding.tglTrust.setVisibility(View.VISIBLE); - }); - binding.tglTrust.setVisibility(View.GONE); - } else { - binding.tglTrust.setOnClickListener(null); - binding.tglTrust.setEnabled(true); - } - toast = v -> hideToast(); - } - } else { - binding.key.setTextColor(MaterialColors.getColor(binding.key, com.google.android.material.R.attr.colorOnSurfaceVariant)); - binding.keyType.setTextColor(MaterialColors.getColor(binding.keyType, com.google.android.material.R.attr.colorOnSurfaceVariant)); - toast = v -> replaceToast(getString(R.string.this_device_is_no_longer_in_use), false); - if (status.isVerified()) { - binding.tglTrust.setVisibility(View.GONE); - binding.verifiedFingerprint.setVisibility(View.VISIBLE); - binding.verifiedFingerprint.setAlpha(0.4368f); - binding.verifiedFingerprint.setOnClickListener(toast); - } else { - binding.tglTrust.setVisibility(View.VISIBLE); - binding.verifiedFingerprint.setVisibility(View.GONE); - binding.tglTrust.setEnabled(false); - } - } - - binding.getRoot().setOnClickListener(toast); - binding.key.setOnClickListener(toast); - binding.keyType.setOnClickListener(toast); - if (showTag) { - binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); - } else { - binding.keyType.setVisibility(View.GONE); - } - if (highlight) { - binding.keyType.setTextColor(MaterialColors.getColor(binding.keyType, com.google.android.material.R.attr.colorPrimaryVariant)); - binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message)); - } else { - binding.keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); - } - - binding.key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2))); - } - - public void showPurgeKeyDialog(final Account account, final String fingerprint) { - final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle(R.string.distrust_omemo_key); - builder.setMessage(R.string.distrust_omemo_key_text); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(R.string.confirm, - (dialog, which) -> { - account.getAxolotlService().distrustFingerprint(fingerprint); - refreshUi(); - }); - builder.create().show(); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); - } + private Account mSelectedAccount; + private String mSelectedFingerprint; + + protected XmppUri mPendingFingerprintVerificationUri = null; + + @Override + public void onCreateContextMenu( + ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + Object account = v.getTag(R.id.TAG_ACCOUNT); + Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT); + Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS); + if (account instanceof Account + && fingerprint instanceof String + && fingerprintStatus instanceof FingerprintStatus) { + getMenuInflater().inflate(R.menu.omemo_key_context, menu); + MenuItem distrust = menu.findItem(R.id.distrust_key); + MenuItem verifyScan = menu.findItem(R.id.verify_scan); + if (this instanceof TrustKeysActivity) { + distrust.setVisible(false); + verifyScan.setVisible(false); + } else { + FingerprintStatus status = (FingerprintStatus) fingerprintStatus; + if (!status.isActive() || status.isVerified()) { + verifyScan.setVisible(false); + } + distrust.setVisible( + status.isVerified() || (!status.isActive() && status.isTrusted())); + } + this.mSelectedAccount = (Account) account; + this.mSelectedFingerprint = (String) fingerprint; + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.distrust_key: + showPurgeKeyDialog(mSelectedAccount, mSelectedFingerprint); + break; + case R.id.copy_omemo_key: + copyOmemoFingerprint(mSelectedFingerprint); + break; + case R.id.verify_scan: + ScanActivity.scan(this); + break; + } + return true; + } + + @Override + public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + if (requestCode == ScanActivity.REQUEST_SCAN_QR_CODE && resultCode == RESULT_OK) { + String result = intent.getStringExtra(ScanActivity.INTENT_EXTRA_RESULT); + XmppUri uri = new XmppUri(result == null ? "" : result); + if (xmppConnectionServiceBound) { + processFingerprintVerification(uri); + } else { + this.mPendingFingerprintVerificationUri = uri; + } + } + } + + protected abstract void processFingerprintVerification(XmppUri uri); + + protected void copyOmemoFingerprint(String fingerprint) { + if (copyTextToClipboard( + CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), + R.string.omemo_fingerprint)) { + Toast.makeText(this, R.string.toast_message_omemo_fingerprint, Toast.LENGTH_SHORT) + .show(); + } + } + + protected void addFingerprintRow( + LinearLayout keys, final XmppAxolotlSession session, boolean highlight) { + final Account account = session.getAccount(); + final String fingerprint = session.getFingerprint(); + addFingerprintRowWithListeners( + keys, + session.getAccount(), + fingerprint, + highlight, + session.getTrust(), + true, + true, + (buttonView, isChecked) -> + account.getAxolotlService() + .setFingerprintTrust( + fingerprint, FingerprintStatus.createActive(isChecked))); + } + + protected void addFingerprintRowWithListeners( + LinearLayout keys, + final Account account, + final String fingerprint, + boolean highlight, + FingerprintStatus status, + boolean showTag, + boolean undecidedNeedEnablement, + CompoundButton.OnCheckedChangeListener onCheckedChangeListener) { + ItemDeviceFingerprintBinding binding = + DataBindingUtil.inflate( + getLayoutInflater(), R.layout.item_device_fingerprint, keys, true); + binding.tglTrust.setVisibility(View.VISIBLE); + registerForContextMenu(binding.getRoot()); + binding.getRoot().setTag(R.id.TAG_ACCOUNT, account); + binding.getRoot().setTag(R.id.TAG_FINGERPRINT, fingerprint); + binding.getRoot().setTag(R.id.TAG_FINGERPRINT_STATUS, status); + boolean x509 = + Config.X509_VERIFICATION + && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509; + final View.OnClickListener toast; + binding.tglTrust.setChecked(status.isTrusted()); + binding.tglTrust.jumpDrawablesToCurrentState(); + + if (status.isActive()) { + binding.key.setTextColor( + MaterialColors.getColor( + binding.key, com.google.android.material.R.attr.colorOnSurface)); + binding.keyType.setTextColor( + MaterialColors.getColor( + binding.keyType, com.google.android.material.R.attr.colorOnSurface)); + if (status.isVerified()) { + binding.verifiedFingerprint.setVisibility(View.VISIBLE); + binding.verifiedFingerprint.setAlpha(1.0f); + binding.tglTrust.setVisibility(View.GONE); + binding.verifiedFingerprint.setOnClickListener( + v -> + replaceToast( + getString(R.string.this_device_has_been_verified), false)); + toast = null; + } else { + binding.verifiedFingerprint.setVisibility(View.GONE); + binding.tglTrust.setVisibility(View.VISIBLE); + binding.tglTrust.setOnCheckedChangeListener(onCheckedChangeListener); + if (status.getTrust() == FingerprintStatus.Trust.UNDECIDED + && undecidedNeedEnablement) { + binding.buttonEnableDevice.setVisibility(View.VISIBLE); + binding.buttonEnableDevice.setOnClickListener( + v -> { + account.getAxolotlService() + .setFingerprintTrust( + fingerprint, FingerprintStatus.createActive(false)); + binding.buttonEnableDevice.setVisibility(View.GONE); + binding.tglTrust.setVisibility(View.VISIBLE); + }); + binding.tglTrust.setVisibility(View.GONE); + } else { + binding.tglTrust.setOnClickListener(null); + binding.tglTrust.setEnabled(true); + } + toast = v -> hideToast(); + } + } else { + binding.key.setTextColor( + MaterialColors.getColor( + binding.key, com.google.android.material.R.attr.colorOnSurfaceVariant)); + binding.keyType.setTextColor( + MaterialColors.getColor( + binding.keyType, + com.google.android.material.R.attr.colorOnSurfaceVariant)); + toast = v -> replaceToast(getString(R.string.this_device_is_no_longer_in_use), false); + if (status.isVerified()) { + binding.tglTrust.setVisibility(View.GONE); + binding.verifiedFingerprint.setVisibility(View.VISIBLE); + binding.verifiedFingerprint.setAlpha(0.4368f); + binding.verifiedFingerprint.setOnClickListener(toast); + } else { + binding.tglTrust.setVisibility(View.VISIBLE); + binding.verifiedFingerprint.setVisibility(View.GONE); + binding.tglTrust.setEnabled(false); + } + } + + binding.getRoot().setOnClickListener(toast); + binding.key.setOnClickListener(toast); + binding.keyType.setOnClickListener(toast); + if (showTag) { + binding.keyType.setText( + getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); + } else { + binding.keyType.setVisibility(View.GONE); + } + if (highlight) { + binding.keyType.setTextColor( + MaterialColors.getColor( + binding.keyType, + com.google.android.material.R.attr.colorPrimaryVariant)); + binding.keyType.setText( + getString( + x509 + ? R.string.omemo_fingerprint_x509_selected_message + : R.string.omemo_fingerprint_selected_message)); + } else { + binding.keyType.setText( + getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); + } + + binding.key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2))); + } + + public void showPurgeKeyDialog(final Account account, final String fingerprint) { + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); + builder.setTitle(R.string.distrust_omemo_key); + builder.setMessage(R.string.distrust_omemo_key_text); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton( + R.string.confirm, + (dialog, which) -> { + account.getAxolotlService().distrustFingerprint(fingerprint); + refreshUi(); + }); + builder.create().show(); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + ScanActivity.onRequestPermissionResult(this, requestCode, grantResults); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index ac31e248f0c987fe7e4a7d561de80480594bae43..24d268b3ca6baceaac69121f56e53243073dc307 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -102,8 +102,8 @@ public class AccountAdapter extends ArrayAdapter { } viewHolder.binding.tglAccountStatus.setOnCheckedChangeListener( (compoundButton, b) -> { - if (b == isDisabled && activity instanceof OnTglAccountState) { - ((OnTglAccountState) activity).onClickTglAccountState(account, b); + if (b == isDisabled && activity instanceof OnTglAccountState tglAccountState) { + tglAccountState.onClickTglAccountState(account, b); } }); if (activity.xmppConnectionService != null && activity.xmppConnectionService.getAccounts().size() > 1) { diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/ConnectionSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/ConnectionSettingsFragment.java index 61843bd93c6e946ee327680a453feb672c57b1e3..ab8caa5c984118b18faf9d3e54f71b1105ca61a1 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/ConnectionSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/ConnectionSettingsFragment.java @@ -1,17 +1,18 @@ package eu.siacs.conversations.ui.fragment.settings; import android.os.Bundle; +import android.util.Log; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.google.common.base.Strings; - import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.utils.Resolver; +import java.util.Arrays; public class ConnectionSettingsFragment extends XmppPreferenceFragment { @@ -59,9 +60,26 @@ public class ConnectionSettingsFragment extends XmppPreferenceFragment { reconnectAccounts(); requireService().reinitializeMuclumbusService(); } - case AppSettings.SHOW_CONNECTION_OPTIONS -> { - reconnectAccounts(); + case AppSettings.SHOW_CONNECTION_OPTIONS -> reconnectAccounts(); + } + if (Arrays.asList(AppSettings.USE_TOR, AppSettings.SHOW_CONNECTION_OPTIONS).contains(key)) { + final var appSettings = new AppSettings(requireContext()); + if (appSettings.isUseTor() || appSettings.isExtendedConnectionOptions()) { + return; } + resetUserDefinedHostname(); + } + } + + private void resetUserDefinedHostname() { + final var service = requireService(); + for (final Account account : service.getAccounts()) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": resetting hostname and port to defaults"); + account.setHostname(null); + account.setPort(Resolver.XMPP_PORT_STARTTLS); + service.databaseBackend.updateAccount(account); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/Jid.java index c6b26903f32bea1deb59182cda9938112bf4bbc2..a83c762872b18f9b6f9a17a2662016ecd64c4297 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/Jid.java @@ -1,9 +1,10 @@ package eu.siacs.conversations.xmpp; import androidx.annotation.NonNull; -import com.google.common.base.CharMatcher; +import eu.siacs.conversations.utils.IP; import im.conversations.android.xmpp.model.stanza.Stanza; import java.io.Serializable; +import java.util.regex.Pattern; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Domainpart; import org.jxmpp.jid.parts.Localpart; @@ -12,6 +13,10 @@ import org.jxmpp.stringprep.XmppStringprepException; public abstract class Jid implements Comparable, Serializable, CharSequence { + private static final Pattern HOSTNAME_PATTERN = + Pattern.compile( + "^(?=.{1,253}$)(?=.{1,253}$)(?!-)(?!.*--)(?!.*-$)[A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)*$"); + public static Jid of( final CharSequence local, final CharSequence domain, final CharSequence resource) { if (local == null) { @@ -77,10 +82,14 @@ public abstract class Jid implements Comparable, Serializable, CharSequence public static Jid ofUserInput(final CharSequence input) { final var jid = of(input); - if (CharMatcher.is('@').matchesAnyOf(jid.getDomain())) { - throw new IllegalArgumentException("Domain should not contain @"); + final var domain = jid.getDomain().toString(); + if (domain.isEmpty()) { + throw new IllegalArgumentException("Domain can not be empty"); + } + if (HOSTNAME_PATTERN.matcher(domain).matches() || IP.matches(domain)) { + return jid; } - return jid; + throw new IllegalArgumentException("Invalid hostname"); } public static Jid ofOrInvalid(final String input) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6f8cc03465239a06995d74b15978e827dbff1db3..3b42121bc8c47c5fa6fb7335562cc5b4b983bae0 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -146,6 +146,7 @@ import im.conversations.android.xmpp.model.sm.StreamManagement; import im.conversations.android.xmpp.model.stanza.Iq; import im.conversations.android.xmpp.model.stanza.Presence; import im.conversations.android.xmpp.model.stanza.Stanza; +import im.conversations.android.xmpp.model.streams.Features; import im.conversations.android.xmpp.model.streams.StreamError; import im.conversations.android.xmpp.model.tls.Proceed; import im.conversations.android.xmpp.model.tls.StartTls; @@ -309,6 +310,7 @@ public class XmppConnection implements Runnable { mXmppConnectionService.resetSendingToWaiting(account); } Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting"); + this.streamFeatures = null; this.pendingResumeId.clear(); this.loginInfo = null; this.features.encryptionEnabled = false; @@ -324,8 +326,9 @@ public class XmppConnection implements Runnable { Socket localSocket; shouldAuthenticate = !account.isOptionSet(Account.OPTION_REGISTER); this.changeStatus(Account.State.CONNECTING); - final boolean useTor = mXmppConnectionService.useTorToConnect() || account.isOnion(); - final boolean extended = mXmppConnectionService.showExtendedConnectionOptions(); + final boolean useTorSetting = appSettings.isUseTor(); + final boolean extended = appSettings.isExtendedConnectionOptions(); + final boolean useTor = useTorSetting || account.isOnion(); // TODO collapse Tor usage into normal connection code path if (useTor) { final var seeOtherHost = this.seeOtherHostResolverResult; @@ -343,7 +346,15 @@ public class XmppConnection implements Runnable { Resolver.fromHardCoded( account.getServer(), Resolver.XMPP_PORT_STARTTLS)); } else { - viaTor = Iterables.getOnlyElement(Resolver.fromHardCoded(hostname, port)); + if (useTorSetting || extended) { + // if the hostname configuration is showing we can take it + viaTor = Iterables.getOnlyElement(Resolver.fromHardCoded(hostname, port)); + } else { + viaTor = + Iterables.getOnlyElement( + Resolver.fromHardCoded( + account.getServer(), Resolver.XMPP_PORT_STARTTLS)); + } this.verifiedHostname = hostname; } @@ -637,6 +648,9 @@ public class XmppConnection implements Runnable { } else if (nextTag.isStart("features", Namespace.STREAMS)) { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed", Namespace.TLS)) { + if (this.socket instanceof SSLSocket) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } switchOverToTls(nextTag); } else if (nextTag.isStart("failure", Namespace.TLS)) { throw new StateChangingException(Account.State.TLS_ERROR); @@ -797,7 +811,9 @@ public class XmppConnection implements Runnable { throws IOException, XmlPullParserException { final LoginInfo currentLoginInfo = this.loginInfo; final SaslMechanism currentSaslMechanism = LoginInfo.mechanism(currentLoginInfo); - if (currentLoginInfo == null || currentSaslMechanism == null) { + if (currentLoginInfo == null + || LoginInfo.isSuccess(currentLoginInfo) + || currentSaslMechanism == null) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } final SaslMechanism.Version version; @@ -1009,9 +1025,15 @@ public class XmppConnection implements Runnable { } catch (final IllegalArgumentException e) { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } + + final LoginInfo currentLoginInfo = this.loginInfo; + if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + Log.d(Config.LOGTAG, failure.toString()); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version); - if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { + if (SaslMechanism.hashedToken(LoginInfo.mechanism(currentLoginInfo))) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": resetting token"); account.resetFastToken(); mXmppConnectionService.databaseBackend.updateAccount(account); @@ -1048,12 +1070,13 @@ public class XmppConnection implements Runnable { } } } - if (SaslMechanism.hashedToken(LoginInfo.mechanism(this.loginInfo))) { + if (SaslMechanism.hashedToken(LoginInfo.mechanism(currentLoginInfo))) { Log.d( Config.LOGTAG, account.getJid().asBareJid() + ": fast authentication failed. falling back to regular" + " authentication"); + this.loginInfo = null; authenticate(); } else { throw new StateChangingException(Account.State.UNAUTHORIZED); @@ -1282,7 +1305,10 @@ public class XmppConnection implements Runnable { account.getJid().asBareJid() + "Not processing iq. Thread was interrupted"); return; } - if (packet.hasExtension(Jingle.class) && packet.getType() == Iq.Type.SET && isBound) { + if (packet.hasExtension(Jingle.class) + && packet.getType() == Iq.Type.SET + && isBound + && LoginInfo.isSuccess(this.loginInfo)) { if (this.jingleListener != null) { this.jingleListener.onJinglePacketReceived(account, packet); } @@ -1312,7 +1338,7 @@ public class XmppConnection implements Runnable { final boolean isRequest = stanza.getType() == Iq.Type.GET || stanza.getType() == Iq.Type.SET; if (isRequest) { - if (isBound) { + if (isBound && LoginInfo.isSuccess(this.loginInfo)) { return new Pair<>(this.unregisteredIqListener, null); } else { throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); @@ -1471,13 +1497,33 @@ public class XmppConnection implements Runnable { } private void processStreamFeatures(final Tag currentTag) throws IOException { - this.streamFeatures = + final var streamFeatures = tagReader.readElement( currentTag, im.conversations.android.xmpp.model.streams.Features.class); final boolean isSecure = isSecure(); + if (streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) { + sendStartTLS(); + return; + } + if (isSecure) { + processSecureStreamFeatures(streamFeatures); + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": STARTTLS not available " + + XmlHelper.printElementNames(streamFeatures)); + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } + } + + private void processSecureStreamFeatures( + final im.conversations.android.xmpp.model.streams.Features streamFeatures) + throws IOException { + this.streamFeatures = streamFeatures; final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); if (this.quickStartInProgress) { - if (this.streamFeatures.hasStreamFeature(Authentication.class)) { + if (streamFeatures.hasStreamFeature(Authentication.class)) { Log.d( Config.LOGTAG, account.getJid().asBareJid() @@ -1504,33 +1550,21 @@ public class XmppConnection implements Runnable { mXmppConnectionService.databaseBackend.updateAccount(account); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); } - if (this.streamFeatures.hasExtension(StartTls.class) && !features.encryptionEnabled) { - sendStartTLS(); - } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) + if (streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { - if (isSecure) { - register(); - } else { - Log.d( - Config.LOGTAG, - account.getJid().asBareJid() - + ": unable to find STARTTLS for registration process " - + XmlHelper.printElementNames(this.streamFeatures)); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); - } - } else if (!this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) + register(); + } else if (!streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) && account.isOptionSet(Account.OPTION_REGISTER)) { throw new StateChangingException(Account.State.REGISTRATION_NOT_SUPPORTED); - } else if (this.streamFeatures.hasStreamFeature(Authentication.class) + } else if (streamFeatures.hasStreamFeature(Authentication.class) && shouldAuthenticate - && isSecure) { + && this.loginInfo == null) { authenticate(SaslMechanism.Version.SASL_2); - } else if (this.streamFeatures.hasStreamFeature(Mechanisms.class) + } else if (streamFeatures.hasStreamFeature(Mechanisms.class) && shouldAuthenticate - && isSecure) { + && this.loginInfo == null) { authenticate(SaslMechanism.Version.SASL); - } else if (this.streamFeatures.streamManagement() - && isSecure + } else if (streamFeatures.streamManagement() && LoginInfo.isSuccess(loginInfo) && streamId != null && !inSmacksSession) { @@ -1547,7 +1581,6 @@ public class XmppConnection implements Runnable { this.tagWriter.writeStanzaAsync(resume); } else if (needsBinding) { if (this.streamFeatures.hasChild("bind", Namespace.BIND) - && isSecure && LoginInfo.isSuccess(loginInfo)) { sendBindRequest(); } else { @@ -1579,10 +1612,15 @@ public class XmppConnection implements Runnable { } private boolean isSecure() { - return features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); + return (features.encryptionEnabled && this.socket instanceof SSLSocket) + || Config.ALLOW_NON_TLS_CONNECTIONS + || account.isDirectToOnion(); } private void authenticate(final SaslMechanism.Version version) throws IOException { + if (this.loginInfo != null) { + throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + } final AuthenticationStreamFeature authElement; if (version == SaslMechanism.Version.SASL) { authElement = this.streamFeatures.getExtension(Mechanisms.class); @@ -1724,7 +1762,7 @@ public class XmppConnection implements Runnable { return; } Log.d(Config.LOGTAG, account.getJid() + ": server did not offer channel binding"); - throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); + throw new StateChangingException(Account.State.CHANNEL_BINDING); } } @@ -1883,8 +1921,7 @@ public class XmppConnection implements Runnable { is = null; } } else { - final boolean useTor = - mXmppConnectionService.useTorToConnect() || account.isOnion(); + final boolean useTor = this.appSettings.isUseTor() || account.isOnion(); try { final String url = data.getValue("url"); final String fallbackUrl = data.getValue("captcha-fallback-url"); @@ -2969,6 +3006,9 @@ public class XmppConnection implements Runnable { public void success(final String challenge, final SSLSocket sslSocket) throws SaslMechanism.AuthenticationException { + if (Thread.currentThread().isInterrupted()) { + throw new SaslMechanism.AuthenticationException("Race condition during auth"); + } final var response = this.saslMechanism.getResponse(challenge, sslSocket); if (!Strings.isNullOrEmpty(response)) { throw new SaslMechanism.AuthenticationException( diff --git a/src/main/res/layout/item_account.xml b/src/main/res/layout/item_account.xml index 3346c98f725f6f11cbf9cb1bda1e7c16d8c42cd1..f15ee5f271f4dfa155a587e5c50f8a9d25c785d9 100644 --- a/src/main/res/layout/item_account.xml +++ b/src/main/res/layout/item_account.xml @@ -14,8 +14,8 @@ android:layout_height="wrap_content" android:background="?selectableItemBackground" android:paddingStart="8dp" - android:paddingBottom="8dp" - android:paddingTop="8dp"> + android:paddingTop="8dp" + android:paddingBottom="8dp"> + android:layout_toStartOf="@+id/tgl_account_status" + android:layout_toEndOf="@+id/account_image" + android:orientation="vertical"> + android:textAppearance="?textAppearanceBodyLarge" + tools:text="juliet@example.com" /> - + android:focusable="false" + android:padding="16dp" /> diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/item_device_fingerprint.xml similarity index 96% rename from src/main/res/layout/contact_key.xml rename to src/main/res/layout/item_device_fingerprint.xml index 447396ab503b70de0d439d6c96864f6a539fe04d..56c148f30c4d0deaa4478d62d6e6926e4bc06190 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/item_device_fingerprint.xml @@ -36,7 +36,7 @@ - + diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml index 3758f1777a1d02b7020361e1a9b8c369b17e7afc..238e615ff196a603c319a97abfd78b2699cb8d93 100644 --- a/src/main/res/menu/message_context.xml +++ b/src/main/res/menu/message_context.xml @@ -73,6 +73,10 @@ android:id="@+id/send_again" android:title="@string/send_again" android:visible="false" /> + Möchtest du deinen Profilbild löschen? Einige Clients zeigen möglicherweise weiterhin eine zwischengespeicherte Kopie deines Profilbildes an. Nur für Kontakte anzeigen Zeitüberschreitung beim Verbinden + Erneut mit P2P versuchen \ No newline at end of file diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index c2efb3867fd45391ff7b810e29da1068b9b1af49..658f5f2d8a183d145252098360d07c82e606b164 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1123,4 +1123,5 @@ ¿Quieres eliminar tu imagen de perfil? Algunos clientes podrían seguir mostrando una copia en caché de tu avatar. Mostrar sólo a contactos Se agotó el tiempo de espera de la conexión + Reintentar con P2P \ No newline at end of file diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 6ced340fc2890d092fe33c8f2299cf883eaf7c95..c1e8c9b859b921f50df527d62d23cd2ffe68615c 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -1083,7 +1083,7 @@ Automaatne allalaadimine Välimus Hele või tume kujundus - Nõua kanaliga sidumist + Nõua edastuskanaliga sidumist Edastuskanaliga sidumine võib aidata vahendusrünnete tuvastamisel Ühendus serveriga Operatsioonisüsteem @@ -1129,4 +1129,6 @@ Kas sa sooviksid oma tunnuspildi kustutada? Palun arvesta, et mitmed klientrakendused võivad jätkata vana puhverdatud pildi kasutamist. Näita vaid kontaktidele Ühenduse on aegunud + Proovi uuesti võrdõigusvõrguga + Edastuskanaliga sidumine pole võimalik \ No newline at end of file diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index aea5ad0c52b33adb7d8df3ffbfb6e66860b744f3..0fcee0151ba72ab66bfa56c4d3072a8cff3ece69 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -26,8 +26,8 @@ minuutti sitten %d minuuttia sitten - %d lukematon keskustelu - %d lukematonta keskustelua + %d lukematon pikakeskustelu + %d lukematonta pikakeskustelua lähettää… Puretaan viestin salausta. Odota hetki… @@ -39,7 +39,7 @@ Moderaattori Osallistuja Vierailija - Poistetaanko %s yhteystiedoistasi? Keskustelujasi hänen kanssaan ei poisteta. + Poistetaanko %s yhteystiedoistasi? Pikakeskustelujasi hänen kanssaan ei poisteta. Estetäänkö %s lähettämästä viestejä sinulle? Perutaanko %s:n esto lähettää viestejä sinulle? Estetäänkö kaikki yhteydet verkkotunnuksesta %s? @@ -76,7 +76,7 @@ Valmistaudutaan lähettämään kuvat Jaetaan tiedostoja. Odota hetki… Pyyhi historia - Pyyhi keskusteluhistoria + Pyyhi pikakeskusteluhistoria Poistetaanko kaikki keskustelun viestit? \n \nVaroitus: Muilla laitteilla tai palvelimilla säilytettyjä kopioita ei poisteta. @@ -85,12 +85,12 @@ \n \nVaroitus: Muilla laitteilla tai palvelimilla olevia kopioita ei poisteta. Valitse laite - Lähetä salaamaton viesti + Lähetä selkeä tekstiviesti Lähetä viesti Lähetä viesti henkilölle %s Lähetä v\\OMEMO-salattu viesti Uusi nimimerkki on jo varattu - Lähetä salaamaton + Lähetä selkeää tekstiä Salauksen purku epäonnistui. Sinulle ei varmaan ole oikeaa salaista avainta. OpenKeychain OpenKeychainia viestien salaamiseeen ja salauksen purkamiseen, sekä julkisten avaintesi hallinointiin.

Se on GPLv3+-lisensoitu ja saatavilla F-Droidista sekä Google Playsta.

(Käynnistä %1$s uudelleen asennettuasi sovelluksen.)]]>
@@ -119,13 +119,13 @@ Rauhanaika Kuinka pitkäksi aikaa ilmoitukset hiljennetään kun jollain toisella laitteillasi tehdään jotain. Edistyneet - Vikailmoituksia lähettämällä autat kehitystyötä - Lukukuittaus + Lähettämällä pinojälkiä autat Quicksyn jatkuvaa kehitystä + Vahvista viestit Ilmoita lähettäjälle kun olet vastaanottanut ja lukenut viestin Estä kuvankaappaukset Piilota sovelluksen sisältö sovellusvaihtajassa ja estä ruutukaappaukset Käyttöliittymä - OpenKeychain-virhe + OpenKeychain tuotti virheen. Avain ei kelpaa salaamiseen. Hyväksy Virhe tapahtui @@ -171,14 +171,14 @@ Haluatko varmasti poistaa OpenPGP-avaimesi tilamainostuksistasi?\nYhteystietosi eivät voi enää lähettää sinulle OpenPGP-salattuja viestejä. OpenPGP julkinen avain julkaistu. Ota tunnus käyttöön - Haluatko varmasti poistaa tilisi? Tilin poistaminen pyyhkii koko keskusteluhistoriasi + Oletko varma, että haluat poistaa tilisi? Tilin poistaminen poistaa koko pikakeskusteluhistoriasi Nauhoita ääntä XMPP-osoite Estä XMPP-osoite käyttäjä@esimerkki.fi Salasana Tämä ei ole kunnollinen XMPP-osoite - Muisti loppui. Kuva on liian suuri. + Muisti loppu. Kuva liian iso Lisätäänkö %s osoitekirjaan? Tietoa palvelimesta XEP-0313: MAM @@ -265,10 +265,10 @@ Kirjoita salasana Pyydä yhteystietoa ensin lähettämään tilapäivityksiä.\n\nTätä käytetään sen tunnistamiseen mitä sovellusta tämä käyttää. Pyydä nyt - Ohita + Jätä huomioimatta Varoitus: Tämän lähettäminen ilman molemminpuolisia tilapäivityksiä voi aiheuttaa odottamattomia ongelmia.\n\nMene \"Yhteystiedon tietoihin\" tarkistaaksesi tilapäivitysten tilauksesi. Turvallisuus - Salli viestien korjaaminen + Viestin korjaus Mahdollistaa muiden muokata sinulle lähettämiään viestejä jälkikäteen Edistyneet asetukset Ole varovainen näiden kanssa @@ -302,8 +302,8 @@ XMPP-osoite kopioitu leikepöydälle Vikailmoitus kopioitu leikepöydälle web-osoite - Lue 2D-viivakoodi - Näytä 2D-viivakoodi + Skannaa QR-koodi + Näytä QR-koodi Näytä estolista Tilitiedot Vahvista @@ -438,7 +438,7 @@ Lataus epöonnistui: Isäntään ei saatu yhteyttä Lataus epäonnistui: Tiedoston tallennus epäonnistui Tor-verkkoa ei saavutettu - Palvelin ei vastaa tästä verkkotunnuksesta + Ei vastuussa toimialueesta Rikki Saatavuus Poissa kun laite on lukittu @@ -447,8 +447,8 @@ Näytä minut kiireisenä kun laite on äänettömänä Kohtele vain värinä -tilaa äänettömän lailla Näytä minut kiireisenä kun laite on vain värinä -tilassa - Laajemmat yhteysasetukset - Näytä isäntänimen ja portin valinta tiliä lisätessä + Isäntänimi ja portti + Näytä laajennetut yhteysasetukset tilin määrittämisen yhteydessä xmpp.esimerkki.fi Kirjaudu varmenteella Varmenteen jäsennys epäonnistui @@ -483,11 +483,8 @@ Kuvat jaettu %s:n kanssa Salli %1$s:n käyttää ulkoista tallennustilaa Salli %1$s:n käyttää kameraa - Synkronoi yhteystietojen kanssa - %1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa. -\nTämä näyttää yhteystietojesi koko nimen ja kuvan. -\n -\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle. + Yhteystietolistan integrointi + %1$s käsittelee yhteystietoluettelosi paikallisesti laitteellasi näyttääkseen sinulle nimet ja profiilikuvat vastaavista XMPP-yhteystiedoista.\n\nMikään yhteystietoluettelo ei koskaan poistu laitteestasi! Ilmoita kaikista uusista viesteistä Ilmoita vain kun minut mainitaan Ilmoitukset pois käytöstä @@ -531,7 +528,7 @@ Lyhyt Keskipitkä Pitkä - Kertoo yhteystiedoillesi milloin käytät Conversationsia + Anna yhteyshenkilöillesi nähdä, milloin viimeksi käytit sovellusta Yksityisyys Teema Valitse väripaletti @@ -578,7 +575,7 @@ Luota uusiin laitteisiin varmistamattomilta yhteystiedoilta, mutta vaadi varmistettujen yhteystietojen uusien laitteiden manuaalinen hyväksyminen. OMEMO-avaimiin luotetaan sokeasti, eli ne voivat olla jonkun muun tai joku voi salakuunnella. Ei luotettu - Viallinen 2D-viivakoodi + Virheellinen QR-koodi Tyhjennä välimuisti (kamerasovelluksen käyttämä) Tyhjennä välimuisti Siivoa yksityinen tallennustila @@ -660,7 +657,7 @@ Poista käytöstä nyt Luonnos: OMEMO-salaus - Uusissa keskusteluissa OMEMO otetaan oletuksena käyttöön. + Uusissa pikakeskusteluissa OMEMO otetaan oletuksena käyttöön. Luo pikakuvake Käytössä oletuksena Oletuksena pois käytöstä @@ -677,13 +674,13 @@ Odota hetki… Salli %1$s:n käyttää mikrofonia GIF - Näytä keskustelu + Näytä pikakeskustelu Sijainnin jako -lisäosa Käytä lisäosaa sisäänrakennetun kartan sijaan Kopioi web-osoite Kopioi XMPP-osoite Tiedostonjako HTTP:llä S3:een - \'Aloita keskustelu\' -näytöllä avaa näppäimistö ja siirrä kursori hakukenttään + \'Uusi pikakeskustelu\' -näytön esiintymisen yhteydessä, avaa näppäimistö ja aseta kohdistin hakukenttään Ryhmän kuvake Isäntäpalvelin ei tue ryhmäkeskustelun kuvakkeita Vain omistaja voi vaihtaa kuvakkeen @@ -725,20 +722,20 @@ Varmista %s %s.]]> Lähetimme sinulle uuden viestin 6-numeroisella koodilla. - Kirjoita 6-numeroinen PIN-koodi alapuolelle. + Syötä kuusinumeroinen PIN-koodi alle. Lähetä uusi tekstiviesti Lähetä uusi tekstivieti (%s) Odota (%s) - takaisin + Takaisin Mahdollinen PIN-koodi liitettiin leikepöydältä automaattisesti. - Syötä 6-numeroinen PIN-koodisi. + Syötä kuusinumeroinen PIN-koodisi. Haluatko varmasti perua rekisteröintiprosessin? Kyllä Ei Varmistetaan… Pyydetään tekstiviestiä… Syöttämäsi PIN-koodi on väärä. - Lähettämämme PIN-koodi on vanhentunut. + Sinulle lähettämämme PIN-koodi on vanhentunut. Tuntematon verkkovirhe. Tuntematon vastaus palvelimelta. Palvelimeen ei saatu yhteyttä. @@ -762,7 +759,7 @@ e-kirja Alkuperäinen (pakkaamaton) Avaa sovelluksella… - Conversations-profiilikuva + Keskustelun profiilikuva Valitse tili Palauta varmuuskopiosta Palauta @@ -861,8 +858,8 @@ Irrota GPX-reitti Viestin korjaaminen epäonnistui - Kaikki keskustelut - Tämä keskustelu + Kaikki pikakeskustelut + Tämä pikakeskustelu Profiilikuvasi %s:n profiilikuva OMEMO-salattu @@ -893,7 +890,7 @@ Perustekstiasiakirja OMEMO:a käytetään aina kaikissa yksityisissä keskusteluissa. Etsi yhteystiedoista - OMEMO täytyy ottaa käyttöön käsin uusissa keskusteluissa. + OMEMO on otettava nimenomaisesti käyttöön uusille pikakeskusteluille. Ryhmäkeskustelut Etsi ryhmäkeskusteluista Suoraan hakuun @@ -908,7 +905,7 @@ Synkronoi kirjanmerkit Lähtevä puhelu (%s) · %s Lataus epäonnistui: Kelvoton tiedosto - Julkaise käyttö + Viimeksi nähty Jatka Kirjautunut ulos @@ -929,4 +926,180 @@ Äänikirja Hiljaiset viestit Vaihdetaanko videopuheluun\? + Kanavahaku käyttää kolmannen osapuolen palvelua nimeltä <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Tämän ominaisuuden käyttäminen lähettää IP-osoitteesi ja hakutermit kyseiseen palveluun. Katso lisätietoja niiden <a href=https://search.jabber.network/privacy>tietosuojakäytännöstään</a>. + Opas on laadittu tilin luomista varten conversations.imissa.\nKun valitset palveluntarjoajaksi conversations.im, voit kommunikoida muiden palveluntarjoajien käyttäjien kanssa antamalla heille koko XMPP-osoitteesi. + Lähetä kaatumisraportit + Yhteyshenkilö pyytää läsnäolotilausta + Viivakoodi ei sisällä sormenjälkiä tälle pikakeskustelulle. + Vastaavat pikakeskustelut arkistoitu. + Viestien ilmoitusasetukset + Sinuun sovelletaan käyttörajoitus + Lisätäänkö lisää säikeitä? + Ulkoasu + Vaalea/tumma tila + Yhteyden aikakatkaisu + Käytännön rikkomus + Yhteensopimaton asiakas + Virtavirhe + Virran avaamisvirhe + Aseta \"autojoin\" -lippu, kun tulet MUC:hen tai poistut siitä ja reagoi muiden asiakkaiden tekemiin muutoksiin. + Poistuit tästä ryhmäpikakeskustelusta teknisistä syistä + %s:n sidossuhdetta ei voitu muuttaa + multimediatiedosto + Sidonnan epäonnistuminen + Laitteesi käyttää raskasta akun optimointia kohteelle %1$s, mikä voi johtaa viivästyneisiin ilmoituksiin tai jopa viestien katoamiseen.\n\nSinua pyydetään nyt poistamaan ne käytöstä. + Liity Conversationiin + Erilliset taustavärit lähetetyille ja vastaanotetuille viesteille + Epäluotettava laite + Viestejä ei noudeta paikallisen säilytysajan vuoksi. + Järjestelmän värit (Material You) + Ei kiinteä asento + Tätä ilmoitusluokkaa käytetään näyttämään pysyvä ilmoitus, joka ilmaisee, että %1$s on käynnissä. + Virheellinen käyttäjän syöte + Älä yritä palauttaa varmuuskopioita, joita et ole itse luonut! + Etsi osallistujat + Yhteyshenkilö ei ole saatavilla + Vaihda pikakeskusteluun + Tilin rekisteröintiä ei tueta + Poista avatar + Hylkää siirtyminen videopyyntöön + Siirry videoon + Työntö-palvelin + Ei mitään (poistettu käytöstä) + Hylkää + Poista tili palvelimelta + Tiliä ei voitu poistaa palvelimelta + Piilota ilmoitus + Kirjaudu ulos + Yhteystietosi käyttää vahvistamattomia laitteita. Skannaa heidän QR-koodinsa suorittaaksesi vahvistuksen ja estääksesi aktiiviset MITM-hyökkäykset. + Käytät vahvistamattomia laitteita. Skannaa QR-koodi muilla laitteillasi vahvistaaksesi ja estääksesi aktiiviset MITM-hyökkäykset. + Ilmoita roskapostista + Ilmoita roskapostista ja estä roskapostittaja + Puheluintegraatio ei ole saatavilla! + Poista ja arkistoi pikakeskustelu + Aloita pikakeskustelu + Käyttöliittymä + Teema, värit, kuvakaappaukset, syöttö + Turvallisuus + E2E-salaus, sokea luottamus ennen vahvistusta, MITM-tunnistus + Ilmoitusvälitys UnifiedPush-yhteensopiville kolmannen osapuolen sovelluksille + Ilmoitukset + Salli kuvakaappaukset + Päästä päähän -salaus + Varmenneviranomaiset + Luota järjestelmän CA-varmenteisiin + Edellyttää kanavan sidontaa + Kanavan sidonta voi havaita joitain koneen välissä olevia hyökkäyksiä + Palvelinyhteys + Kirjoitusilmoitukset, viimeksi nähty, saatavuus + Isäntänimi ja portti, Tor + Sitoutumisilmoitukset + Haluatko poistaa kirjanmerkin kohteesta %s? + Haluatko poistaa %s:n kirjanmerkin ja arkistoida pikakeskustelun? + Keskustelujen varmuuskopio + Yrität tuoda vanhentunutta varmuuskopiotiedostomuotoa + Piilota ei-aktiivinen + Laitteesi käyttää raskasta akun optimointia kohteelle %1$s, mikä voi johtaa viivästyneisiin ilmoituksiin tai jopa viestien katoamiseen.\nOn suositeltavaa poistaa ne käytöstä. + Ei lupaa puhelun soittamiseen + Tätä ilmoitusryhmää käytetään näyttämään ilmoituksia, joiden ei pitäisi laukaista ääntä. Esimerkiksi ollessasi aktiivinen toisella laitteella (armoaika). + Syötä nimesi, jotta ihmiset, joiden osoitekirjassa ei ole sinua, tietävät kuka olet. + Videota ei voitu poistaa käytöstä. + Quicksy pyytää suostumustasi tietojesi käyttöön + Uusi pikakeskustelu + Tälle yhteyshenkilölle ei ole käytettävissä avaimia.\nVarmista, että teillä molemmilla on läsnäolotilaus. + Tämä ei ole kelvollinen käyttäjänimi + Näytä ei-aktiivinen + Turvavirhe: Virheellinen tiedoston käyttöoikeus! + Tervetuloa Quicksyyn! + Dynaamiset värit + Isäntänimi ja portti, Tor, kanavan löytö + Näytä sovelluksen sisältö sovelluksen vaihtajassa ja salli kuvakaappausten ottaminen + Oletko varma, että haluat poistaa tämän laitteen vahvistuksen?\nTämä laite ja sen viestit merkitään \"Epäluotettava\":ksi. + Puhelut on poistettu käytöstä Toria käytettäessä + Asiakasvarmennetta ei ole valittu! + Tietosuojakäytäntö + Etsi maita + Yhteystietolistan integrointi ei ole saatavilla + Ei sidossuhdetta + Teksti jaettu %s:n kanssa + Huomioi ilmoitukset + Näytä huomioidut ilmoitukset + Kiinteä asento + Tallentamista ei voitu aloittaa + Tätä ilmoitusluokkaa käytetään näyttämään ilmoitus, jos tiliin yhdistämisessä on ongelma. + Käynnissä olevat puhelut + Tiedosto jätetty pois tietoturvarikkomuksen vuoksi. + Kirjaudu sisään + Jaa kanssa… + Värikkäät pikakeskustelukuplat + Arkistoi pikakeskustelu + Jaa kanssa… + Poista pikakeskustelu jälkeenpäin + Lähetä salattu viesti + Suorita toiminto kanssa + Vastaanotetaan + Automaattinen lataus + Käyttöjärjestelmä + Näppäimistö + Tiedoston koko, kuvan pakkaus, videon laatu + Armonaika, soittoääni, värinä, muukalaiset + Lähetetään + XMPP-osoitetta ei löytynyt + Väliaikainen todennuksen epäonnistuminen + XMPP-tili + Tili, jonka kautta työntöviestit vastaanotetaan. + Pikakeskustelu arkistoitu + Sinun avatarisi. Napauta valitaksesi uuden avatarin galleriasta. + XEP-0388: laajennettava SASL-profiili + Tälle yhteyshenkilölle ei ole käytettävissä avaimia.\nUusia avaimia ei voitu noutaa palvelimelta. Ehkä yhteystietosi palvelimessa on jotain vikaa? + Yhdistä uudelleen toiseen isäntään + Olet vahvistamassa oman tilisi OMEMO-avaimet. Tämä on turvallista vain, jos seurasit tätä linkkiä luotettavasta lähteestä, jossa vain sinä olisit voinut julkaista tämän linkin. + Sovellus + Vuorovaikutus + Laitteella + Luo yksikertainen, ajoita toistuva + Luo yksikertainen varmuuskopio + Puhelussa käytetään langallisia kuulokkeita + Puhelussa käytetään kaiutinta. Napauta vaihtaaksesi kuulokkeisiin. + Puhelussa käytetään kaiutinta. + Puhelussa käytetään bluetoothia. + Käännä kamera + Video on kytketty päälle. Poista käytöstä napauttamalla. + Video on poistettu käytöstä. Ota käyttöön napauttamalla. + Sisäänkirjautumismekanismi + Taustaväri, kirjasinkoko, avatarit + Haluatko poistaa avatarisi? Jotkut asiakkaat saattavat edelleen näyttää avatarisi välimuistissa olevan kopion. + Suurenna viestikuplien kirjasinkokoa + Otetaanko mukautetut ilmoitusasetukset (tärkeys, ääni, värinä) käyttöön tälle keskustelulle? + Puhelua ei voitu muokata + Hyväksy kutsut ryhmäpikakeskusteluihin tuntemattomilta + Kutsut tuntemattomilta + Salli yksityisviestit + Suuri fontti + Toimintoa ei tueta + Muokkaa lempinimeä + Poista OpenPGP-avain + Muokkaa nimeä ja aihetta + Muuta kokoonpanoa + Muuta ilmoitusasetuksia + Puhelussa käytetään kuuloketta. Napauta vaihtaaksesi kaiuttimeen. + Toistuva varmuuskopio + Koko näyttö -ilmoitukset + Salli tämän sovelluksen näyttää saapuvien puhelujen ilmoitukset, jotka täyttävät koko näytön, kun laite on lukittuna. + Puhelussa käytetään kuulokkeita. + Yhteyshenkilösi XMPP-asiakasohjelma ei ehkä tue audio-/videopuheluita. + Reaktiota ei voitu lisätä + Lisää reaktio… + Puheluintegraatio + Tämän sovelluksen puhelut ovat vuorovaikutuksessa tavallisten puheluiden kanssa, kuten puhelun lopettaminen toisen alkaessa. + Vasemmalle tasatut viestit + Näytä kaikki viestit, mukaan lukien lähetetyt, vasemmalla puolella yhtenäisen pikakeskustelu-asettelun saavuttamiseksi. + Mukautetut ilmoitukset + Näytä vain yhteyshenkilöille + Lisää reaktio + Lisää reaktioita + Näytä avatarit viestillesi ja kahdenkeskisissä pikakeskusteluissa ryhmäpikakeskustelujen lisäksi. + Pikakeskustelukuplat + Näytä avatarit + Pikakeskustelukuplat diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 848b04d529de740149f5d0b1ae2802e0419a1d7f..ed57de38b653b1eabaeb7a73e7a7c8cc051a7a88 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -334,7 +334,7 @@ Eliminar %s ficheiro Abrir %s - enviando (%1$d %% completado) + enviando (%1$d%% completado) Preparándose para compartir o ficheiro Ofreceuse %s para descargar Cancelar a transmisión @@ -1107,4 +1107,6 @@ Queres eliminar o teu avatar? Algúns clientes poderían continuar mostrando unha copia almacenada do teu avatar. Mostrar só aos contactos Caducidade da conexión + Reintentar con P2P + Non está dispoñible a vinculación de canles diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 6495c3a4ae7cd6fa0a21568fc00fc0c4a939c9f5..73556f89d69479119f95a66422e19c1a83eeee30 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -460,8 +460,8 @@ Scaricamento fallito: scrittura del file impossibile Scaricamento fallito: file non valido Rete Tor non disponibile - Bind fallito - Il server non è responsabile per questo dominio + Associazione fallita + Non responsabile per il dominio Rotto Disponibilità \"Non disponibile\" a dispositivo bloccato @@ -1107,4 +1107,22 @@ Aggiungi reazione… Aggiungi reazione Altre reazioni + Connessione scaduta + Mostra solo ai contatti + Riprova con P2P + Impossibile modificare la chiamata + Il client XMPP del tuo contatto potrebbe non supportare le chiamate audio/video. + Integrazione di chiamate + Le chiamate da questa app interagiscono con le normali chiamate telefoniche, come terminare una chiamata quando un\'altra inizia. + Messaggi allineati a sinistra + Mostra gli avatar + Mostra gli avatar per i tuoi messaggi e nelle chat 1:1, in aggiunta alle chat di gruppo. + Attivare le impostazioni di notifica personalizzate (importanza, suono, vibrazione) per questa conversazione? + Mostra tutti i messaggi, inclusi quelli inviati, sul lato sinistro per una disposizione uniforme della chat. + Notifiche personalizzate + Vuoi eliminare il tuo avatar? Alcuni client potrebbero continuare a mostrare una copia in cache del tuo avatar. + Messaggi di chat + Colore di sfondo, dimensione caratteri, avatar + Messaggi di chat + Associazione dei canali non disponibile diff --git a/src/main/res/values-night/themes.xml b/src/main/res/values-night/themes.xml index 18591c59eb87bdea65a4dad8caa15cb63fd0b23f..c6a2926e8ff2443987133d14d2f26e69e5141a9e 100644 --- a/src/main/res/values-night/themes.xml +++ b/src/main/res/values-night/themes.xml @@ -28,6 +28,7 @@ @color/md_theme_dark_inverseOnSurface @color/md_theme_dark_inverseSurface @color/md_theme_dark_inversePrimary + @style/MaterialPreferenceThemeOverlay + + + + diff --git a/src/quicksy/fastlane/metadata/android/fi-FI/full_description.txt b/src/quicksy/fastlane/metadata/android/fi-FI/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..5030837b499c0cf1c524106486c7410c9d6ba908 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/fi-FI/full_description.txt @@ -0,0 +1,14 @@ +Quicksy on suositun Jabber/XMPP-asiakaskeskustelujen spin off, jossa on automaattinen yhteystietojen etsintä. + +Rekisteröidyt puhelinnumerollasi ja Quicksy ehdottaa sinulle automaattisesti mahdollisia yhteystietoja osoitekirjassasi olevien puhelinnumeroiden perusteella. + +Kotelon alla Quicksy on täysimittainen Jabber-asiakasohjelma, jonka avulla voit kommunikoida minkä tahansa käyttäjän kanssa millä tahansa julkisesti liitetyllä palvelimella. Samoin Quicksyn käyttäjiin voidaan ottaa yhteyttä ulkopuolelta yksinkertaisesti lisäämällä +puhelinnumero@quicksy.im yhteystietoluetteloosi. + +Yhteystietojen synkronoinnin lisäksi käyttöliittymä on tarkoituksella mahdollisimman lähellä keskusteluja. Tämän ansiosta käyttäjät voivat lopulta siirtyä Quicksystä Conversationsiin ilman, että heidän tarvitsee opetella uudelleen sovelluksen toimintaa. + +Ehdotetut yhteystiedot koostuvat muista Quicksy-käyttäjistä ja tavallisista Jabber/XMPP-käyttäjistä, jotka ovat syöttäneet Jabber-tunnuksensa Quicksy-hakemistoon (https://quicksy.im/#get-listed). + +Huomautus: Voit kirjoittaa (https://quicksy.im/enter/) Jabber-tunnuksesi Quicksyyn +Hakemistoon vaaditaan kertaluonteinen rekisteröintimaksu. + +Lue lisää tietosuojakäytännöstä (https://quicksy.im/#privacy). diff --git a/src/quicksy/fastlane/metadata/android/fi-FI/short_description.txt b/src/quicksy/fastlane/metadata/android/fi-FI/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..9482dfa8200858b1c73a4d960d2943bb9d2ffdde --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/fi-FI/short_description.txt @@ -0,0 +1 @@ +Jabber/XMPP helpolla syötyksellä ja helpolla löytämisellä diff --git a/src/quicksy/res/values-fi/strings.xml b/src/quicksy/res/values-fi/strings.xml index 9a988a15db678f4e9df6f9968f143c15690945b1..505494d3cbfbd8bda9f0cc8eae19f4af8ca7cdf4 100644 --- a/src/quicksy/res/values-fi/strings.xml +++ b/src/quicksy/res/values-fi/strings.xml @@ -1,7 +1,7 @@ Kuinka kauan Quicksy pysyy hiljaa nähtyään toisella laitteellasi toimintaa - Lähettämällä virheenkorjaustietoja autat Quicksyn kehittäjiä + Lähettämällä pinojälkiä autat Quicksyn jatkuvaa kehitystä Kerro kaikille yhteystiedoillesi kun käytät Quicksya Saadaksesi ilmoituksia silloinkin kun näyttö on sammutettu, Quicksy pitää lisätä suojattujen sovellusten luetteloon. Quicksy-profiilikuva @@ -9,4 +9,4 @@ Palvelimen identiteetin varmennus epäonnistui. Tuntematon turvallisuusvirhe. Palvelimeen yhdistäminen aikakatkaistiin. - + \ No newline at end of file