From 2596d2ee2d397cfdd2eff77469b013e432d7c4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 25 Feb 2025 19:26:50 +0000 Subject: [PATCH 001/169] Translated using Weblate (Estonian) Currently translated at 82.1% (69 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/et/ --- fastlane/metadata/android/et/changelogs/383.txt | 3 +++ fastlane/metadata/android/et/changelogs/387.txt | 2 ++ fastlane/metadata/android/et/changelogs/388.txt | 3 +++ fastlane/metadata/android/et/changelogs/390.txt | 1 + fastlane/metadata/android/et/changelogs/393.txt | 3 +++ fastlane/metadata/android/et/changelogs/394.txt | 2 ++ fastlane/metadata/android/et/changelogs/395.txt | 3 +++ 7 files changed, 17 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/383.txt create mode 100644 fastlane/metadata/android/et/changelogs/387.txt create mode 100644 fastlane/metadata/android/et/changelogs/388.txt create mode 100644 fastlane/metadata/android/et/changelogs/390.txt create mode 100644 fastlane/metadata/android/et/changelogs/393.txt create mode 100644 fastlane/metadata/android/et/changelogs/394.txt create mode 100644 fastlane/metadata/android/et/changelogs/395.txt diff --git a/fastlane/metadata/android/et/changelogs/383.txt b/fastlane/metadata/android/et/changelogs/383.txt new file mode 100644 index 0000000000000000000000000000000000000000..51fd0a959fe613c2b68c8401c0125564985ae85c --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/383.txt @@ -0,0 +1,3 @@ +* Selleks, et muud tööriistariba ikoonid oleks järjepidevalt samas kohas, nihutasime kõne ikooni vasakule +* Häälkõnede ajal näitame kõne kestust +* Lisasime lahenduse samaaegsete hääl- ja videokõnede jaoks (olukord, kus kaks inimest helistavad samal hetkel üksteisele) diff --git a/fastlane/metadata/android/et/changelogs/387.txt b/fastlane/metadata/android/et/changelogs/387.txt new file mode 100644 index 0000000000000000000000000000000000000000..4de9661e52f539de894843ed1409931a9e89cd00 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/387.txt @@ -0,0 +1,2 @@ +* Muutsime sertifikaadiga sisselogimise kasutajaliidese paremaks +* Lisasime võimaluse tõsta vestlused ülal esile (ehk lisada lemmikuks) diff --git a/fastlane/metadata/android/et/changelogs/388.txt b/fastlane/metadata/android/et/changelogs/388.txt new file mode 100644 index 0000000000000000000000000000000000000000..ec70e5981dc327579003ab00df4aa428adc4727b --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/388.txt @@ -0,0 +1,3 @@ +* Vähendasime mõnedes seadmetes kaja kõne keskel +* Parandasime sisselogimisvea, kui salasõnas oli erilisi tähemärke +* Videokõnede ajal esitame nüüd valjuhääldis valimistooni ja kinnist tooni diff --git a/fastlane/metadata/android/et/changelogs/390.txt b/fastlane/metadata/android/et/changelogs/390.txt new file mode 100644 index 0000000000000000000000000000000000000000..197153e6c2c8f3fd112ee19786306e4cf0ebf229 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/390.txt @@ -0,0 +1 @@ +* Kui teine osapool on hõivatud, siis pakume võimalust häälsõnumi jätmiseks diff --git a/fastlane/metadata/android/et/changelogs/393.txt b/fastlane/metadata/android/et/changelogs/393.txt new file mode 100644 index 0000000000000000000000000000000000000000..de0347ee28d0760d012106150db9f93c73f22ca9 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/393.txt @@ -0,0 +1,3 @@ +* Kui hääl- või videokõne ei õnnestu, siis näitame abiteabe nuppu +* Parandasime mõned tüütud kokkujooksmised +* Parandasime Jingle'i-põhiste ühenduste (failide teisaldamine + kõned) toimimise ainult JID alusel diff --git a/fastlane/metadata/android/et/changelogs/394.txt b/fastlane/metadata/android/et/changelogs/394.txt new file mode 100644 index 0000000000000000000000000000000000000000..86b2a077cd2d807929825b61be23fb6d130c7f5d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/394.txt @@ -0,0 +1,2 @@ +* Parandasime olukorra, kus teavitusi polnud mõnel spetsiifilisel juhul näha +* Parandasime hääl- ja videokõnede ühilduvusvigu ja kokkujooksmisi diff --git a/fastlane/metadata/android/et/changelogs/395.txt b/fastlane/metadata/android/et/changelogs/395.txt new file mode 100644 index 0000000000000000000000000000000000000000..f960cbb59c2271101fc16ce76a139f16ccc09261 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/395.txt @@ -0,0 +1,3 @@ +* Lisasime kõne vaatesse nupu „Tagasi vestluse juurde“ +* Parandasime kiirklahvide toimimist +* Muud veaparandused From f8e73a2411415c433b8af1ffe0034e4961a2f44f Mon Sep 17 00:00:00 2001 From: ghose Date: Fri, 28 Feb 2025 05:00:02 +0000 Subject: [PATCH 002/169] Translated using Weblate (Galician) Currently translated at 66.6% (56 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4213204.txt | 4 ++++ fastlane/metadata/android/gl-ES/changelogs/4213304.txt | 1 + fastlane/metadata/android/gl-ES/changelogs/4213404.txt | 1 + 3 files changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4213204.txt create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4213404.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4213204.txt b/fastlane/metadata/android/gl-ES/changelogs/4213204.txt new file mode 100644 index 0000000000000000000000000000000000000000..772b36c69c11fa6d4655ab1b1aeebcfc3e86ef44 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4213204.txt @@ -0,0 +1,4 @@ +* Permitir pausar a gravación de audio tocando no temporizador +* Arranxo das reaccións nas MUC PMs +* Deixar de aceptar 'fallback messages' para reaccións, acuses de recibo e marcadores de posición +* Engadir máis iconas de previsualización multimedia diff --git a/fastlane/metadata/android/gl-ES/changelogs/4213304.txt b/fastlane/metadata/android/gl-ES/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..5f1ebaa32aa97f9ea118a526bac19148f77d3d2b --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4213304.txt @@ -0,0 +1 @@ +* arranxos menores diff --git a/fastlane/metadata/android/gl-ES/changelogs/4213404.txt b/fastlane/metadata/android/gl-ES/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..224beb2288ff04033837bb92bd34b03121daeb12 --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4213404.txt @@ -0,0 +1 @@ +* Arranxo do problema ao transferir ficheiros na tradución ao fi From 1a1848fe506d6188b5b3239916edc59679719ef7 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Sat, 1 Mar 2025 17:20:26 +0000 Subject: [PATCH 003/169] Translated using Weblate (German) Currently translated at 100.0% (1061 of 1061 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 2e349cafb2f3d8214c01349e635e28c4f38290a0..7bb01c4fec2ef23d742b871b699901afc1e5e5e3 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1110,4 +1110,5 @@ Nur für Kontakte anzeigen Zeitüberschreitung beim Verbinden Erneut mit P2P versuchen - \ No newline at end of file + Kanalbindung nicht verfügbar + From eef95f94bbf0719ffff8cf3c48f920ef42acfbfe Mon Sep 17 00:00:00 2001 From: nautilusx Date: Sat, 1 Mar 2025 17:17:33 +0000 Subject: [PATCH 004/169] Translated using Weblate (German) Currently translated at 100.0% (84 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4213304.txt | 1 + fastlane/metadata/android/de-DE/changelogs/4213404.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/4213404.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4213304.txt b/fastlane/metadata/android/de-DE/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..a00810e4f6e08b6d44a747d22e22fe4cd4ea8f83 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4213304.txt @@ -0,0 +1 @@ +* Kleinere Fehlerbehebungen diff --git a/fastlane/metadata/android/de-DE/changelogs/4213404.txt b/fastlane/metadata/android/de-DE/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..63c602f92528df3eba4e4627bd90bbec8bb5866a --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4213404.txt @@ -0,0 +1 @@ +* Behebung eines Absturzes bei der Dateiübertragung in der finnischen Übersetzung From 65ffd58a9a618d07ee8dca355e2125efdb336e9c Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Sun, 2 Mar 2025 14:03:39 +0000 Subject: [PATCH 005/169] Translated using Weblate (Polish) Currently translated at 52.3% (44 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/changelogs/42059.txt | 2 ++ fastlane/metadata/android/pl-PL/changelogs/4213304.txt | 1 + fastlane/metadata/android/pl-PL/changelogs/4213404.txt | 1 + 3 files changed, 4 insertions(+) create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42059.txt create mode 100644 fastlane/metadata/android/pl-PL/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/pl-PL/changelogs/4213404.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/42059.txt b/fastlane/metadata/android/pl-PL/changelogs/42059.txt new file mode 100644 index 0000000000000000000000000000000000000000..3f3df51fc1c4e021d0122970ce5ffc99ac95771e --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42059.txt @@ -0,0 +1,2 @@ +* Ponowne podniesienie docelowego SDK do 33 +* Poprawienie błędów na serwerach obsługujących SASL2 bez Inline Stream Management diff --git a/fastlane/metadata/android/pl-PL/changelogs/4213304.txt b/fastlane/metadata/android/pl-PL/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..2f9a6e84501c3df605cbf3d649485b071f507f42 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4213304.txt @@ -0,0 +1 @@ +* Mniejsze poprawki błędów diff --git a/fastlane/metadata/android/pl-PL/changelogs/4213404.txt b/fastlane/metadata/android/pl-PL/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..d82bfa4b03f27f53bda5351f3822df7767bd36ac --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4213404.txt @@ -0,0 +1 @@ +* Naprawienie awarii podczas przesyłania plików w fińskiej wersji językowej From 3dcd9958ac06e9e583fd337054b2ac22da8bcf99 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 3 Mar 2025 05:45:32 +0000 Subject: [PATCH 006/169] Translated using Weblate (Russian) Currently translated at 35.7% (30 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/ru/ --- fastlane/metadata/android/ru-RU/changelogs/4213304.txt | 1 + fastlane/metadata/android/ru-RU/changelogs/4213404.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4213404.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/4213304.txt b/fastlane/metadata/android/ru-RU/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..b89a78e7bc1612eea2357456fb7e1ea35ecb6358 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4213304.txt @@ -0,0 +1 @@ +* исправление мелких ошибок diff --git a/fastlane/metadata/android/ru-RU/changelogs/4213404.txt b/fastlane/metadata/android/ru-RU/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..e48860e869251818a884940a1618a583fd770f3c --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4213404.txt @@ -0,0 +1 @@ +* Исправлен сбой передачи файлов при использовании финского перевода From 6707e4c02289475518e7a26f5e6bb373c721eb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 3 Mar 2025 10:51:34 +0000 Subject: [PATCH 007/169] Translated using Weblate (Estonian) Currently translated at 85.7% (72 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/et/ --- fastlane/metadata/android/et/changelogs/398.txt | 4 ++++ fastlane/metadata/android/et/changelogs/401.txt | 2 ++ fastlane/metadata/android/et/changelogs/402.txt | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/398.txt create mode 100644 fastlane/metadata/android/et/changelogs/401.txt create mode 100644 fastlane/metadata/android/et/changelogs/402.txt diff --git a/fastlane/metadata/android/et/changelogs/398.txt b/fastlane/metadata/android/et/changelogs/398.txt new file mode 100644 index 0000000000000000000000000000000000000000..23d66d0b7148e237198982c5cafdbc52105a95e9 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/398.txt @@ -0,0 +1,4 @@ +* Üksikute vestluste otsing +* Kasutaja teavitus, kui sõnumi edastamine ei õnnestu +* Jätame meelde Quicksy kasutajate kuvatavad nimed (hüüdnimed) ka peale rakenduse taaskäivitamist +* Lisasime nupu Orboti (Tor) teavitusest käivitamiseks (kui peaks vaja olema) diff --git a/fastlane/metadata/android/et/changelogs/401.txt b/fastlane/metadata/android/et/changelogs/401.txt new file mode 100644 index 0000000000000000000000000000000000000000..038afc2c219a2562c106cd38336e8fb978ef2b6c --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/401.txt @@ -0,0 +1,2 @@ +* Parandasime otsinguvea Android <= 5 puhul +* Optimeerisime mälukasutust diff --git a/fastlane/metadata/android/et/changelogs/402.txt b/fastlane/metadata/android/et/changelogs/402.txt new file mode 100644 index 0000000000000000000000000000000000000000..4f03c5317ed8d7533b33221cfca506b69de7c321 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/402.txt @@ -0,0 +1,3 @@ +* Pakume lihtkutse (Easy Invite) loomist, kui server seda toetab +* Kuvame Movimist saadetud GIF-faile +* Salvestame tunnuspildid puhverdatud andmetena From fa41640bcc90d1e30b4d5d41adecd096e172227f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 3 Mar 2025 16:48:48 +0100 Subject: [PATCH 008/169] use 'word document' instead of full mime type in download button --- .../ui/adapter/MediaAdapter.java | 15 +- .../siacs/conversations/utils/MimeUtils.java | 6 + .../siacs/conversations/utils/UIHelper.java | 195 ++++++++++-------- src/main/res/values/strings.xml | 1 + 4 files changed, 125 insertions(+), 92 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java index de4b1f4626723de5c4e281daaeecdc39e6f8814d..411dadf584295480e2eb8e29290df6a0ecdafdf3 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -18,11 +18,13 @@ import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.color.MaterialColors; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ItemMediaBinding; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.ui.util.ViewUtil; +import eu.siacs.conversations.utils.MimeUtils; import eu.siacs.conversations.worker.ExportBackupWorker; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -33,13 +35,12 @@ import java.util.concurrent.RejectedExecutionException; public class MediaAdapter extends RecyclerView.Adapter { public static final List DOCUMENT_MIMES = - Arrays.asList( - "application/pdf", - "application/vnd.oasis.opendocument.text", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "text/x-tex", - "text/plain"); + new ImmutableList.Builder() + .add("application/pdf") + .add("text/x-tex") + .add("text/plain") + .addAll(MimeUtils.WORD_DOCUMENT_MIMES) + .build(); public static final List SPREAD_SHEET_MIMES = Arrays.asList( "text/comma-separated-values", diff --git a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java index 00ccf81243ac0e475dfb1432e51078cb7fc971e6..de23f3bba0ac561e9b7a00f7f8041af6e51424d2 100644 --- a/src/main/java/eu/siacs/conversations/utils/MimeUtils.java +++ b/src/main/java/eu/siacs/conversations/utils/MimeUtils.java @@ -48,6 +48,12 @@ public final class MimeUtils { "video/3gpp", // .3gp files can contain audio, video or both "video/3gpp2"); + public static final List WORD_DOCUMENT_MIMES = + Arrays.asList( + "application/vnd.oasis.opendocument.text", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); + private static final Map mimeTypeToExtensionMap = new HashMap<>(); private static final Map extensionToMimeTypeMap = new HashMap<>(); diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 245f196a66a70b16bbfc02d567370e947a9c9010..9f3aab8bc9ca937c35f530e91ec494e580c6006e 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -7,21 +7,12 @@ import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Pair; import android.widget.TextView; - import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; - import com.google.android.material.color.MaterialColors; import com.google.common.base.Strings; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -37,35 +28,42 @@ import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.worker.ExportBackupWorker; import eu.siacs.conversations.xmpp.Jid; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; public class UIHelper { - private static final List LOCATION_QUESTIONS = Arrays.asList( - "where are you", //en - "where are you now", //en - "where are you right now", //en - "whats your 20", //en - "what is your 20", //en - "what's your 20", //en - "whats your twenty", //en - "what is your twenty", //en - "what's your twenty", //en - "wo bist du", //de - "wo bist du jetzt", //de - "wo bist du gerade", //de - "wo seid ihr", //de - "wo seid ihr jetzt", //de - "wo seid ihr gerade", //de - "dónde estás", //es - "donde estas" //es - ); - - private static final List PUNCTIONATION = Arrays.asList('.', ',', '?', '!', ';', ':'); - - private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE - | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; - private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME - | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; + private static final List LOCATION_QUESTIONS = + Arrays.asList( + "where are you", // en + "where are you now", // en + "where are you right now", // en + "whats your 20", // en + "what is your 20", // en + "what's your 20", // en + "whats your twenty", // en + "what is your twenty", // en + "what's your twenty", // en + "wo bist du", // de + "wo bist du jetzt", // de + "wo bist du gerade", // de + "wo seid ihr", // de + "wo seid ihr jetzt", // de + "wo seid ihr gerade", // de + "dónde estás", // es + "donde estas" // es + ); + + private static final List PUNCTIONATION = + Arrays.asList('.', ',', '?', '!', ';', ':'); + + private static final int SHORT_DATE_FLAGS = + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; + private static final int FULL_DATE_FLAGS = + DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE; public static String readableTimeDifference(Context context, long time) { return readableTimeDifference(context, time, false); @@ -75,8 +73,7 @@ public class UIHelper { return readableTimeDifference(context, time, true); } - private static String readableTimeDifference(Context context, long time, - boolean fullDate) { + private static String readableTimeDifference(Context context, long time, boolean fullDate) { if (time == 0) { return context.getString(R.string.just_now); } @@ -93,11 +90,9 @@ public class UIHelper { return df.format(date); } else { if (fullDate) { - return DateUtils.formatDateTime(context, date.getTime(), - FULL_DATE_FLAGS); + return DateUtils.formatDateTime(context, date.getTime(), FULL_DATE_FLAGS); } else { - return DateUtils.formatDateTime(context, date.getTime(), - SHORT_DATE_FLAGS); + return DateUtils.formatDateTime(context, date.getTime(), SHORT_DATE_FLAGS); } } } @@ -116,8 +111,7 @@ public class UIHelper { cal1.add(Calendar.DAY_OF_YEAR, -1); cal2.setTime(new Date(date)); return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) - && cal1.get(Calendar.DAY_OF_YEAR) == cal2 - .get(Calendar.DAY_OF_YEAR); + && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); } public static boolean sameDay(long a, long b) { @@ -130,8 +124,7 @@ public class UIHelper { cal1.setTime(a); cal2.setTime(b); return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) - && cal1.get(Calendar.DAY_OF_YEAR) == cal2 - .get(Calendar.DAY_OF_YEAR); + && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR); } public static String lastseen(Context context, boolean active, long time) { @@ -147,52 +140,68 @@ public class UIHelper { } else if (difference < 60 * 60 * 2) { return context.getString(R.string.last_seen_hour); } else if (difference < 60 * 60 * 24) { - return context.getString(R.string.last_seen_hours, - Math.round(difference / (60.0 * 60.0))); + return context.getString( + R.string.last_seen_hours, Math.round(difference / (60.0 * 60.0))); } else if (difference < 60 * 60 * 48) { return context.getString(R.string.last_seen_day); } else { - return context.getString(R.string.last_seen_days, - Math.round(difference / (60.0 * 60.0 * 24.0))); + return context.getString( + R.string.last_seen_days, Math.round(difference / (60.0 * 60.0 * 24.0))); } } - public static int getColorForName(final String name) { return XEP0392Helper.rgbFromNick(name); } - - public static Pair getMessagePreview(final Context context, final Message message) { + public static Pair getMessagePreview( + final Context context, final Message message) { return getMessagePreview(context, message, 0); } - public static Pair getMessagePreview(final Context context, final Message message, @ColorInt int textColor) { + public static Pair getMessagePreview( + final Context context, final Message message, @ColorInt int textColor) { final Transferable d = message.getTransferable(); if (d != null) { switch (d.getStatus()) { case Transferable.STATUS_CHECKING: - return new Pair<>(context.getString(R.string.checking_x, - getFileDescriptionString(context, message)), true); + return new Pair<>( + context.getString( + R.string.checking_x, + getFileDescriptionString(context, message)), + true); case Transferable.STATUS_DOWNLOADING: - return new Pair<>(context.getString(R.string.receiving_x_file, - getFileDescriptionString(context, message), - d.getProgress()), true); + return new Pair<>( + context.getString( + R.string.receiving_x_file, + getFileDescriptionString(context, message), + d.getProgress()), + true); case Transferable.STATUS_OFFER: case Transferable.STATUS_OFFER_CHECK_FILESIZE: - return new Pair<>(context.getString(R.string.x_file_offered_for_download, - getFileDescriptionString(context, message)), true); + return new Pair<>( + context.getString( + R.string.x_file_offered_for_download, + getFileDescriptionString(context, message)), + true); case Transferable.STATUS_FAILED: return new Pair<>(context.getString(R.string.file_transmission_failed), true); case Transferable.STATUS_CANCELLED: - return new Pair<>(context.getString(R.string.file_transmission_cancelled), true); + return new Pair<>( + context.getString(R.string.file_transmission_cancelled), true); case Transferable.STATUS_UPLOADING: if (message.getStatus() == Message.STATUS_OFFERED) { - return new Pair<>(context.getString(R.string.offering_x_file, - getFileDescriptionString(context, message)), true); + return new Pair<>( + context.getString( + R.string.offering_x_file, + getFileDescriptionString(context, message)), + true); } else { - return new Pair<>(context.getString(R.string.sending_x_file, - getFileDescriptionString(context, message)), true); + return new Pair<>( + context.getString( + R.string.sending_x_file, + getFileDescriptionString(context, message)), + true); } default: return new Pair<>("", false); @@ -215,18 +224,28 @@ public class UIHelper { if (!rtpSessionStatus.successful && received) { return new Pair<>(context.getString(R.string.missed_call), true); } else { - return new Pair<>(context.getString(received ? R.string.incoming_call : R.string.outgoing_call), true); + return new Pair<>( + context.getString( + received ? R.string.incoming_call : R.string.outgoing_call), + true); } } else { final String body = MessageUtils.filterLtrRtl(message.getBody()); if (body.startsWith(Message.ME_COMMAND)) { - return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND, - UIHelper.getMessageDisplayName(message) + " "), false); + return new Pair<>( + body.replaceAll( + "^" + Message.ME_COMMAND, + UIHelper.getMessageDisplayName(message) + " "), + false); } else if (message.isGeoUri()) { return new Pair<>(context.getString(R.string.location), true); - } else if (message.treatAsDownloadable() || MessageUtils.unInitiatedButKnownSize(message)) { - return new Pair<>(context.getString(R.string.x_file_offered_for_download, - getFileDescriptionString(context, message)), true); + } else if (message.treatAsDownloadable() + || MessageUtils.unInitiatedButKnownSize(message)) { + return new Pair<>( + context.getString( + R.string.x_file_offered_for_download, + getFileDescriptionString(context, message)), + true); } else { SpannableStringBuilder styledBody = new SpannableStringBuilder(body); if (textColor != 0) { @@ -283,18 +302,18 @@ public class UIHelper { return input.length() > 256 ? StylingHelper.subSequence(input, 0, 256) : input; } - public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos){ + public static boolean isPositionPrecededByBodyStart(CharSequence body, int pos) { // true if not a single linebreak before current position - for (int i = pos - 1; i >= 0; i--){ - if (body.charAt(i) != ' '){ + for (int i = pos - 1; i >= 0; i--) { + if (body.charAt(i) != ' ') { return false; } } return true; } - public static boolean isPositionPrecededByLineStart(CharSequence body, int pos){ - if (isPositionPrecededByBodyStart(body, pos)){ + public static boolean isPositionPrecededByLineStart(CharSequence body, int pos) { + if (isPositionPrecededByBodyStart(body, pos)) { return true; } return body.charAt(pos - 1) == '\n'; @@ -342,7 +361,8 @@ public class UIHelper { final char c = body.charAt(i); if (Character.isWhitespace(c)) { return false; - } else if (QuoteHelper.isPositionQuoteCharacter(body, pos) || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) { + } else if (QuoteHelper.isPositionQuoteCharacter(body, pos) + || QuoteHelper.isPositionQuoteEndCharacter(body, pos)) { return body.length() == i + 1 || Character.isWhitespace(body.charAt(i + 1)); } } @@ -405,6 +425,8 @@ public class UIHelper { return context.getString(R.string.image); } else if (mime.contains("pdf")) { return context.getString(R.string.pdf_document); + } else if (MimeUtils.WORD_DOCUMENT_MIMES.contains(mime)) { + return context.getString(R.string.word_document); } else if (mime.equals("application/vnd.android.package-archive")) { return context.getString(R.string.apk); } else if (mime.equals(ExportBackupWorker.MIME_TYPE)) { @@ -413,7 +435,8 @@ public class UIHelper { return context.getString(R.string.vcard); } else if (mime.equals("text/x-vcalendar") || mime.equals("text/calendar")) { return context.getString(R.string.event); - } else if (mime.equals("application/epub+zip") || mime.equals("application/vnd.amazon.mobi8-ebook")) { + } else if (mime.equals("application/epub+zip") + || mime.equals("application/vnd.amazon.mobi8-ebook")) { return context.getString(R.string.ebook); } else if (mime.equals("application/gpx+xml")) { return context.getString(R.string.gpx_track); @@ -438,7 +461,8 @@ public class UIHelper { return contact != null ? contact.getDisplayName() : ""; } } else { - if (conversation instanceof Conversation && conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation instanceof Conversation + && conversation.getMode() == Conversation.MODE_MULTI) { return ((Conversation) conversation).getMucOptions().getSelf().getName(); } else { final Account account = conversation.getAccount(); @@ -449,12 +473,11 @@ public class UIHelper { } else { return displayName; } - } } } - public static String getMessageHint(final Context context,final Conversation conversation) { + public static String getMessageHint(final Context context, final Conversation conversation) { return switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_NONE -> { if (Config.multipleEncryptionChoices()) { @@ -491,10 +514,12 @@ public class UIHelper { || message.getType() != Message.TYPE_TEXT) { return false; } - final String body = Strings.nullToEmpty(message.getBody()) - .trim() - .toLowerCase(Locale.getDefault()) - .replace("?", "").replace("¿", ""); + final String body = + Strings.nullToEmpty(message.getBody()) + .trim() + .toLowerCase(Locale.getDefault()) + .replace("?", "") + .replace("¿", ""); return LOCATION_QUESTIONS.contains(body); } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e143b167cc19d9189fd545eebf32737505493109..6bf79ca139943c41aa2d439f1203b19e73bdcccd 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -422,6 +422,7 @@ vector graphic multimedia file PDF document + Word document Android App Audiobook Contact From b72af5703f9871c176fa5c5ecdc2ffc20b966628 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 7 Mar 2025 11:46:18 +0100 Subject: [PATCH 009/169] minor jingle code clean up --- build.gradle | 6 +- .../conversations/xmpp/jingle/IceServers.java | 138 +++++++++--------- .../xmpp/jingle/JingleRtpConnection.java | 10 +- .../xmpp/jingle/RtpContentMap.java | 9 +- .../xmpp/jingle/SessionDescription.java | 12 +- .../xmpp/jingle/WebRTCWrapper.java | 41 +++--- .../jingle/stanzas/IceUdpTransportInfo.java | 14 +- .../xmpp/jingle/stanzas/RtpDescription.java | 25 ++-- .../WebRTCDataChannelTransport.java | 31 ++-- 9 files changed, 135 insertions(+), 151 deletions(-) diff --git a/build.gradle b/build.gradle index c014e6e74df3965b2fea41a7a7232ccd82eb9d22..2c41cea2112ef5e70dd0faf48957668bebb61da3 100644 --- a/build.gradle +++ b/build.gradle @@ -96,8 +96,8 @@ dependencies { implementation "com.squareup.retrofit2:converter-gson:2.11.0" implementation "com.squareup.okhttp3:okhttp:4.12.0" - implementation 'com.google.guava:guava:32.1.3-android' - quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.35' + implementation 'com.google.guava:guava:33.4.0-android' + quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.52' implementation 'im.conversations.webrtc:webrtc-android:129.0.0' } @@ -113,7 +113,7 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42134 + versionCode 42135 versionName "2.17.12" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java index 5149470f09bca5fe0ef6eb9c494a407fac5a30cc..3ef291c97b59ec86abca1a96b342711eb557ae43 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java @@ -1,98 +1,94 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; - import com.google.common.base.Strings; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; - import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.IP; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import im.conversations.android.xmpp.model.stanza.Iq; - -import org.webrtc.PeerConnection; - import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; +import org.webrtc.PeerConnection; public final class IceServers { - public static List parse(final Iq response) { - ImmutableList.Builder listBuilder = new ImmutableList.Builder<>(); + public static Set parse(final Iq response) { if (response.getType() == Iq.Type.RESULT) { - final Element services = - response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - final List children = - services == null ? Collections.emptyList() : services.getChildren(); - for (final Element child : children) { - if ("service".equals(child.getName())) { - final String type = child.getAttribute("type"); - final String host = child.getAttribute("host"); - final String sport = child.getAttribute("port"); - final Integer port = sport == null ? null : Ints.tryParse(sport); - final String transport = child.getAttribute("transport"); - final String username = child.getAttribute("username"); - final String password = child.getAttribute("password"); - if (Strings.isNullOrEmpty(host) || port == null) { - continue; - } - if (port < 0 || port > 65535) { + return Collections.emptySet(); + } + final var builder = new ImmutableSet.Builder(); + final Element services = + response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + final List children = + services == null ? Collections.emptyList() : services.getChildren(); + for (final Element child : children) { + if ("service".equals(child.getName())) { + final String type = child.getAttribute("type"); + final String host = child.getAttribute("host"); + final String sport = child.getAttribute("port"); + final Integer port = sport == null ? null : Ints.tryParse(sport); + final String transport = child.getAttribute("transport"); + final String username = child.getAttribute("username"); + final String password = child.getAttribute("password"); + if (Strings.isNullOrEmpty(host) || port == null) { + continue; + } + if (port < 0 || port > 65535) { + continue; + } + + if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) + && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) { + Log.w( + Config.LOGTAG, + "skipping invalid combination of udp/tls in external services"); continue; } - if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) - && Arrays.asList("udp", "tcp").contains(transport)) { - if (Arrays.asList("stuns", "turns").contains(type) - && "udp".equals(transport)) { - Log.w( - Config.LOGTAG, - "skipping invalid combination of udp/tls in external services"); - continue; - } - - // STUN URLs do not support a query section since M110 - final String uri; - if (Arrays.asList("stun", "stuns").contains(type)) { - uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port); - } else { - uri = - String.format( - "%s:%s:%s?transport=%s", - type, IP.wrapIPv6(host), port, transport); - } + // STUN URLs do not support a query section since M110 + final String uri; + if (Arrays.asList("stun", "stuns").contains(type)) { + uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port); + } else { + uri = + String.format( + "%s:%s:%s?transport=%s", + type, IP.wrapIPv6(host), port, transport); + } - final PeerConnection.IceServer.Builder iceServerBuilder = - PeerConnection.IceServer.builder(uri); - iceServerBuilder.setTlsCertPolicy( - PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); - if (username != null && password != null) { - iceServerBuilder.setUsername(username); - iceServerBuilder.setPassword(password); - } else if (Arrays.asList("turn", "turns").contains(type)) { - // The WebRTC spec requires throwing an - // InvalidAccessError when username (from libwebrtc - // source coder) - // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc - Log.w( - Config.LOGTAG, - "skipping " - + type - + "/" - + transport - + " without username and password"); - continue; - } - final PeerConnection.IceServer iceServer = - iceServerBuilder.createIceServer(); - Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer); - listBuilder.add(iceServer); + final PeerConnection.IceServer.Builder iceServerBuilder = + PeerConnection.IceServer.builder(uri); + iceServerBuilder.setTlsCertPolicy( + PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); + if (username != null && password != null) { + iceServerBuilder.setUsername(username); + iceServerBuilder.setPassword(password); + } else if (Arrays.asList("turn", "turns").contains(type)) { + // The WebRTC spec requires throwing an + // InvalidAccessError when username (from libwebrtc + // source coder) + // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc + Log.w( + Config.LOGTAG, + "skipping " + + type + + "/" + + transport + + " without username and password"); + continue; } + final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); + Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer); + builder.add(iceServer); } } } - return listBuilder.build(); + return builder.build(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 10a6d4ba6b6c9ef78667db03de71481a56dead47..cd35d163b8bafcaf1a8644209c5a0a04bc05d0e9 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1339,7 +1339,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private synchronized void sendSessionAccept( final Set media, final SessionDescription offer, - final List iceServers) { + final Set iceServers) { if (isTerminated()) { Log.w( Config.LOGTAG, @@ -1823,7 +1823,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private synchronized void sendSessionInitiate( final Set media, final State targetState, - final List iceServers) { + final Set iceServers) { if (isTerminated()) { Log.w( Config.LOGTAG, @@ -2320,7 +2320,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void setupWebRTC( final Set media, - final List iceServers, + final Set iceServers, final boolean trickle) throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); @@ -2843,7 +2843,7 @@ public class JingleRtpConnection extends AbstractJingleConnection Log.w( Config.LOGTAG, id.account.getJid().asBareJid() + ": has no external service discovery"); - onIceServersDiscovered.onIceServersDiscovered(Collections.emptyList()); + onIceServersDiscovered.onIceServersDiscovered(Collections.emptySet()); } } @@ -2956,6 +2956,6 @@ public class JingleRtpConnection extends AbstractJingleConnection } private interface OnIceServersDiscovered { - void onIceServersDiscovered(List iceServers); + void onIceServersDiscovered(Set iceServers); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java index 5036b2b858038e6064751e49e1b264c4b230ed92..e8079fe4663e71c482cd527345b987673ae33810 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/RtpContentMap.java @@ -12,7 +12,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Sets; - import eu.siacs.conversations.xmpp.jingle.stanzas.Content; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.GenericTransportInfo; @@ -21,13 +20,11 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.OmemoVerifiedIceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import im.conversations.android.xmpp.model.jingle.Jingle; - import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; - import javax.annotation.Nonnull; public class RtpContentMap extends AbstractContentMap { @@ -99,7 +96,7 @@ public class RtpContentMap extends AbstractContentMap> entry : @@ -118,7 +115,8 @@ public class RtpContentMap extends AbstractContentMap descriptionTransport = diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 025a7acc917fd0defb95953556b9b15f99cb271a..b981472d535cce394102ff39708ae9d577e784dc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -2,9 +2,7 @@ package eu.siacs.conversations.xmpp.jingle; import android.util.Log; import android.util.Pair; - import androidx.annotation.NonNull; - import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Strings; @@ -12,7 +10,6 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; - import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription; @@ -21,7 +18,6 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Group; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; - import java.util.Collection; import java.util.Collections; import java.util.List; @@ -206,6 +202,10 @@ public class SessionDescription { entry : contentMap.contents.entrySet()) { final String name = entry.getKey(); checkNoWhitespace(name, "content name must not contain any whitespace"); + // https://groups.google.com/g/discuss-webrtc/c/VG406JMTBI4/m/MrSex_q7AgAJ + if (name.length() > 16) { + throw new IllegalArgumentException("mid should not be longer than 16 chars"); + } final DescriptionTransport descriptionTransport = entry.getValue(); final RtpDescription description = descriptionTransport.description; @@ -226,7 +226,7 @@ public class SessionDescription { if (parameters.size() == 1) { mediaAttributes.put( "fmtp", RtpDescription.Parameter.toSdpString(id, parameters.get(0))); - } else if (parameters.size() > 0) { + } else if (!parameters.isEmpty()) { mediaAttributes.put( "fmtp", RtpDescription.Parameter.toSdpString(id, parameters)); } @@ -306,7 +306,7 @@ public class SessionDescription { "A SSRC group is missing semantics attribute"); } checkNoWhitespace(semantics, "source group semantics must not contain whitespace"); - if (groups.size() == 0) { + if (groups.isEmpty()) { throw new IllegalArgumentException("A SSRC group is missing SSRC ids"); } for (final String source : groups) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index 36433790f104b337d77d8d1060c3f8ed4c18db2f..d5c3be53fe411a6c68550d04ed6285e93457b12c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -3,18 +3,26 @@ package eu.siacs.conversations.xmpp.jingle; import android.content.Context; import android.os.Build; import android.util.Log; - import com.google.common.base.Optional; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; - import eu.siacs.conversations.Config; import eu.siacs.conversations.services.XmppConnectionService; - +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.webrtc.AudioSource; import org.webrtc.AudioTrack; import org.webrtc.CandidatePairChangeEvent; @@ -35,19 +43,6 @@ import org.webrtc.SessionDescription; import org.webrtc.VideoTrack; import org.webrtc.audio.JavaAudioDeviceModule; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - public class WebRTCWrapper { private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName(); @@ -235,13 +230,13 @@ public class WebRTCWrapper { synchronized void initializePeerConnection( final Set media, - final List iceServers, + final Set iceServers, final boolean trickle) throws InitializationException { Preconditions.checkState(this.eglBase != null); Preconditions.checkNotNull(media); Preconditions.checkArgument( - media.size() > 0, "media can not be empty when initializing peer connection"); + !media.isEmpty(), "media can not be empty when initializing peer connection"); final boolean setUseHardwareAcousticEchoCanceler = !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL); Log.d( @@ -371,16 +366,16 @@ public class WebRTCWrapper { if (videoSourceWrapper != null) { try { videoSourceWrapper.stopCapture(); - } catch (InterruptedException e) { - e.printStackTrace(); + } catch (final InterruptedException e) { + Log.e(Config.LOGTAG, "could not stop capturing video source", e); } } } public static PeerConnection.RTCConfiguration buildConfiguration( - final List iceServers, final boolean trickle) { + final Set iceServers, final boolean trickle) { final PeerConnection.RTCConfiguration rtcConfig = - new PeerConnection.RTCConfiguration(iceServers); + new PeerConnection.RTCConfiguration(ImmutableList.copyOf(iceServers)); rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp if (trickle) { @@ -397,7 +392,7 @@ public class WebRTCWrapper { } void reconfigurePeerConnection( - final List iceServers, final boolean trickle) { + final Set iceServers, final boolean trickle) { requirePeerConnection().setConfiguration(buildConfiguration(iceServers, trickle)); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 8d90b1982d28bfa8e86e82f2b277a22f022e2be5..85ee06cb6b30441a60f8dd9ff3323fee3ae54112 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,28 +1,23 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import android.util.Log; - import androidx.annotation.NonNull; - import com.google.common.base.Joiner; import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.base.Strings; -import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; - import eu.siacs.conversations.Config; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.jingle.SessionDescription; import eu.siacs.conversations.xmpp.jingle.transports.Transport; - import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -101,7 +96,9 @@ public class IceUdpTransportInfo extends GenericTransportInfo { for (final Element child : this.children) { if (Namespace.JINGLE_TRANSPORT_ICE_OPTION.equals(child.getNamespace()) && IceOption.WELL_KNOWN.contains(child.getName())) { - optionBuilder.add(child.getName()); + optionBuilder.add( + SessionDescription.checkNoWhitespace( + child.getName(), "Ice options should not contain whitespace")); } } return optionBuilder.build(); @@ -159,7 +156,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { final IceUdpTransportInfo transportInfo = new IceUdpTransportInfo(); transportInfo.setAttributes(new Hashtable<>(getAttributes())); transportInfo.setChildren(this.getChildren()); - for(final Candidate candidate : candidates) { + for (final Candidate candidate : candidates) { transportInfo.addChild(candidate); } return transportInfo; @@ -220,7 +217,8 @@ public class IceUdpTransportInfo extends GenericTransportInfo { return null; } - public static Candidate fromSdpAttributeValue(final String value, final String currentUfrag) { + public static Candidate fromSdpAttributeValue( + final String value, final String currentUfrag) { final String[] segments = value.split(" "); if (segments.length < 6) { return null; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index a7b62363c4cb5040e924f336f12b9363f99fcbcc..0eb6cdbf9270a96d44508a1a336da51fc62c38dc 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java @@ -1,25 +1,22 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import android.util.Pair; - import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; - +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.jingle.Media; +import eu.siacs.conversations.xmpp.jingle.SessionDescription; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; -import eu.siacs.conversations.xmpp.jingle.Media; -import eu.siacs.conversations.xmpp.jingle.SessionDescription; - public class RtpDescription extends GenericDescription { private RtpDescription(final String media) { @@ -291,7 +288,7 @@ public class RtpDescription extends GenericDescription { final String channels = this.getAttribute("channels"); if (channels == null) { return 1; // The number of channels; if omitted, it MUST be assumed to contain one - // channel + // channel } try { return Integer.parseInt(channels); @@ -544,13 +541,17 @@ public class RtpDescription extends GenericDescription { } public List getSsrcs() { - ImmutableList.Builder builder = new ImmutableList.Builder<>(); - for (Element child : this.children) { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (final Element child : this.children) { if ("source".equals(child.getName())) { final String ssrc = child.getAttribute("ssrc"); - if (ssrc != null) { - builder.add(ssrc); + if (Strings.isNullOrEmpty(ssrc)) { + continue; } + builder.add( + SessionDescription.checkNoNewline( + ssrc, + "Source Specific media attributes can not contain newline")); } } return builder.build(); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java index 95b15438460b50ac229ed522162e714f820a48d6..134c0be69bf6cf7b8a22f9ede6dafe2060c06eee 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java @@ -5,7 +5,6 @@ import static eu.siacs.conversations.xmpp.jingle.WebRTCWrapper.logDescription; import android.content.Context; import android.util.Log; - import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.io.Closeables; @@ -13,7 +12,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; - import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.Namespace; @@ -22,17 +20,7 @@ import eu.siacs.conversations.xmpp.jingle.IceServers; import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; - import im.conversations.android.xmpp.model.stanza.Iq; - -import org.webrtc.CandidatePairChangeEvent; -import org.webrtc.DataChannel; -import org.webrtc.IceCandidate; -import org.webrtc.MediaStream; -import org.webrtc.PeerConnection; -import org.webrtc.PeerConnectionFactory; -import org.webrtc.SessionDescription; - import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; @@ -46,14 +34,21 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; +import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; - import javax.annotation.Nonnull; +import org.webrtc.CandidatePairChangeEvent; +import org.webrtc.DataChannel; +import org.webrtc.IceCandidate; +import org.webrtc.MediaStream; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.SessionDescription; public class WebRTCDataChannelTransport implements Transport { @@ -229,12 +224,12 @@ public class WebRTCDataChannelTransport implements Transport { } } - private ListenableFuture> getIceServers() { + private ListenableFuture> getIceServers() { if (Config.DISABLE_PROXY_LOOKUP) { - return Futures.immediateFuture(Collections.emptyList()); + return Futures.immediateFuture(Collections.emptySet()); } if (xmppConnection.getFeatures().externalServiceDiscovery()) { - final SettableFuture> iceServerFuture = + final SettableFuture> iceServerFuture = SettableFuture.create(); final Iq request = new Iq(Iq.Type.GET); request.setTo(this.account.getDomain()); @@ -254,12 +249,12 @@ public class WebRTCDataChannelTransport implements Transport { }); return iceServerFuture; } else { - return Futures.immediateFuture(Collections.emptyList()); + return Futures.immediateFuture(Collections.emptySet()); } } private PeerConnection createPeerConnection( - final List iceServers, final boolean trickle) { + final Set iceServers, final boolean trickle) { final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle); final PeerConnection peerConnection = requirePeerConnectionFactory() From 6745b6cfbbc01ba5051c92a2916b1a808dc2b3e6 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 3 Mar 2025 16:48:31 +0000 Subject: [PATCH 010/169] Translated using Weblate (Arabic) Currently translated at 55.3% (588 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ar/ --- src/main/res/values-ar/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ar/strings.xml b/src/main/res/values-ar/strings.xml index 82691c3d78e06222e036ae9e3f212444969a2325..0da63828da9cc7a95a2a5436be63fe1c7e337f95 100644 --- a/src/main/res/values-ar/strings.xml +++ b/src/main/res/values-ar/strings.xml @@ -650,4 +650,4 @@ رمز التسجيل غير صالح مفتاح تعمية خاطئ. هذا ليس عنوان XMPP صالح - \ No newline at end of file + From ff51bd7139e9e328842b2616d58a3e1e7089eec3 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 3 Mar 2025 16:49:00 +0000 Subject: [PATCH 011/169] Translated using Weblate (French) Currently translated at 97.5% (1036 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 71be6a990780423010509840bacc3eaebed9a8f1..965e650ac9bcf886945568510a3caa34da30dd2e 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -1110,4 +1110,4 @@ L\'appel passe par le haut-parleur. Tapotez pour passer sur les écouteurs. L\'appel passe par le haut-parleur. Mécanisme de connexion - \ No newline at end of file + From 0a67d8004aa31a0511a7cef9b9a7332851635e0c Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 3 Mar 2025 16:49:07 +0000 Subject: [PATCH 012/169] Translated using Weblate (Hungarian) Currently translated at 98.6% (1048 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/hu/ --- src/main/res/values-hu/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-hu/strings.xml b/src/main/res/values-hu/strings.xml index c6dd67432e3fb0624593fcbd2ba076679c3638f4..310470291983ebd4db812e833bc736e47066c751 100644 --- a/src/main/res/values-hu/strings.xml +++ b/src/main/res/values-hu/strings.xml @@ -1097,4 +1097,4 @@ Csevegőbuborékok Háttérszín, betűméret, avatárok Csevegőbuborékok - \ No newline at end of file + From 755e215cd2078fda8c47ff85cfb2f1c4384bea19 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 3 Mar 2025 16:49:30 +0000 Subject: [PATCH 013/169] Translated using Weblate (Russian) Currently translated at 99.6% (1058 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index d5283e4be491cf0c167f44bcdcd60af48d877338..31a34c0ddc19e34c800ae11298bab02c618946eb 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -1158,4 +1158,4 @@ Истекло время ожидания подключения Повторить через P2P Привязка канала недоступна - \ No newline at end of file + From 8f8628a3422caa90b3393a60611290e7f3780774 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 3 Mar 2025 16:48:56 +0000 Subject: [PATCH 014/169] Translated using Weblate (Persian) Currently translated at 87.3% (928 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fa/ --- src/main/res/values-fa-rIR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-fa-rIR/strings.xml b/src/main/res/values-fa-rIR/strings.xml index 63727938f6d752dfa3e6fc28d6c1f4028a872b81..1d5e136ae79f1bf1f5c29bfd7a7559252e8adaad 100644 --- a/src/main/res/values-fa-rIR/strings.xml +++ b/src/main/res/values-fa-rIR/strings.xml @@ -1027,4 +1027,4 @@ این برنامه برای به‌کاربردن داده‌های شما نیازمند موافقت شماست یکپارچه‌سازی تماس تلفنی در دسترس نیست! اجازهٔ تماس تلفنی وجود ندارد - \ No newline at end of file + From 58c469ac4c271de87483259915396692e3558051 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Mon, 3 Mar 2025 16:51:42 +0000 Subject: [PATCH 015/169] Translated using Weblate (Albanian) Currently translated at 99.0% (1052 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index b8c64aba80be703b19ccb5b5d8ca0941584fbd73..dd78479be0e04d293014df66b01672ef6bd365ce 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -1121,4 +1121,5 @@ Doni të fshihet avatari jua? Disa klientë mund të vazhdojnë të shfaqin një kopje të ruajtur në fshehtinat e tyre të avatarit tuaj. Mbarim kohe për lidhjen Riprovo me P2P - \ No newline at end of file + Dokument Word + From 4cc0d5d2718e624bde50139143beaed49ea020cf Mon Sep 17 00:00:00 2001 From: Dirk Date: Mon, 3 Mar 2025 21:26:11 +0000 Subject: [PATCH 016/169] Translated using Weblate (German) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 7bb01c4fec2ef23d742b871b699901afc1e5e5e3..74b0b9c7e2a5c294979646c2e6716009dea7ddbe 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1111,4 +1111,5 @@ Zeitüberschreitung beim Verbinden Erneut mit P2P versuchen Kanalbindung nicht verfügbar + Worddatei From 663110d4cddc147db90400affae45c3332abf699 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 3 Mar 2025 17:47:37 +0000 Subject: [PATCH 017/169] Translated using Weblate (Russian) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 31a34c0ddc19e34c800ae11298bab02c618946eb..9d7b1ca73c4f7512a5b10a48ff5c531987b68793 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -91,7 +91,7 @@ Отправить сообщение без шифрования Сообщение Сообщение для %s - Зашифрованное vOMEMO сообщение + Зашифрованное v\\OMEMO сообщение Используется новое имя Отправить в незашифрованном виде Расшифровка невозможна. Вероятно, у вас нет надлежащего ключа. @@ -218,9 +218,9 @@ Найдены новые зашифрованные OpenPGP сообщения ID ключа OpenPGP Отпечаток OMEMO - Отпечаток vOMEMO + Отпечаток v\\OMEMO Отпечаток OMEMO (выбранного сообщения) - Отпечаток vOMEMO (выбранного сообщения) + Отпечаток v\\OMEMO (выбранного сообщения) Другие устройства Доверенные отпечатки OMEMO Получение ключей… @@ -422,7 +422,7 @@ видео изображение векторная графика - PDF-документ + Документ PDF Приложение Android Контакт Аватар загружен! @@ -1158,4 +1158,5 @@ Истекло время ожидания подключения Повторить через P2P Привязка канала недоступна + Документ Word From b34292a3306101a0232d3178a599ca754e4ddfb1 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Mon, 3 Mar 2025 20:12:43 +0000 Subject: [PATCH 018/169] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index e4ad25d4678d71592b86af7b919b5ed03af52f5c..861f3996018e50971eb23a054e98c0b21e68f64c 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1159,4 +1159,5 @@ Час очікування з\'єднання вичерпано Повторити спробу з P2P Прив\'язка каналу недоступна - \ No newline at end of file + документ Word + From fcbba18104e01999287a203bbc83b980b71ac80f Mon Sep 17 00:00:00 2001 From: DrManhattanMR Date: Tue, 4 Mar 2025 00:27:23 +0000 Subject: [PATCH 019/169] Translated using Weblate (Spanish) Currently translated at 100.0% (84 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/es/ --- fastlane/metadata/android/es-ES/changelogs/351.txt | 2 +- fastlane/metadata/android/es-ES/changelogs/394.txt | 2 +- fastlane/metadata/android/es-ES/changelogs/407.txt | 2 +- fastlane/metadata/android/es-ES/changelogs/42037.txt | 2 +- fastlane/metadata/android/es-ES/changelogs/4213204.txt | 4 ++++ fastlane/metadata/android/es-ES/changelogs/4213304.txt | 1 + fastlane/metadata/android/es-ES/changelogs/4213404.txt | 1 + 7 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 fastlane/metadata/android/es-ES/changelogs/4213204.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/4213404.txt diff --git a/fastlane/metadata/android/es-ES/changelogs/351.txt b/fastlane/metadata/android/es-ES/changelogs/351.txt index a89b01aee6505af54f92c76e7183b287ff047058..70eae3e3ab87e5cb685f778508c48383d640c3ec 100644 --- a/fastlane/metadata/android/es-ES/changelogs/351.txt +++ b/fastlane/metadata/android/es-ES/changelogs/351.txt @@ -1,3 +1,3 @@ * Corrección de la transferencia de archivos Jingle IBB -* Corrección de correcciones repetidas que llenaban la base de datos. +* Corrección de correcciones repetidas que llenaban la base de datos * Transición a Last Message Correction v1.1 diff --git a/fastlane/metadata/android/es-ES/changelogs/394.txt b/fastlane/metadata/android/es-ES/changelogs/394.txt index 72b50cdfb4264f4c4a291a2aa09510d812c35a9c..46fdb8622bc8ef5a020392cc4b9bece487fe4eba 100644 --- a/fastlane/metadata/android/es-ES/changelogs/394.txt +++ b/fastlane/metadata/android/es-ES/changelogs/394.txt @@ -1,2 +1,2 @@ -* Se ha corregido el problema de las notificaciones que no aparecían en determinadas circunstancias. +* Se ha corregido el problema de las notificaciones que no aparecían en determinadas circunstancias * Se han solucionado problemas de compatibilidad y bloqueos relacionados con las llamadas A/V diff --git a/fastlane/metadata/android/es-ES/changelogs/407.txt b/fastlane/metadata/android/es-ES/changelogs/407.txt index 3958fd6efe69ad6e01b37a467fbe282aeae89fa9..7ef9bf11a49567627dce3fe3c4051e8d4a69323a 100644 --- a/fastlane/metadata/android/es-ES/changelogs/407.txt +++ b/fastlane/metadata/android/es-ES/changelogs/407.txt @@ -1,3 +1,3 @@ * Mostrar botón de llamada para contactos desconectados si previamente anunciaron soporte -* El botón Atrás ya no finaliza la llamada cuando está conectada. +* El botón Atrás ya no finaliza la llamada cuando está conectada * Corrección de errores diff --git a/fastlane/metadata/android/es-ES/changelogs/42037.txt b/fastlane/metadata/android/es-ES/changelogs/42037.txt index b10095829a6ea900b935c5dd5e1046ddd9ebc865..d2f93ba2839987f10a343a13d79980c31794d0dc 100644 --- a/fastlane/metadata/android/es-ES/changelogs/42037.txt +++ b/fastlane/metadata/android/es-ES/changelogs/42037.txt @@ -1,5 +1,5 @@ Versión 2.10.9 -* Pedir permisos Bluetooth al hacer llamadas A/V (Puede rechazar esto si no utiliza auriculares Bluetooth). +* Pedir permisos Bluetooth al hacer llamadas A/V (Puede rechazar esto si no utiliza auriculares Bluetooth) * Corrección de error al llamar a Movim * Corregir avatar incorrecto que se muestra para los chats de grupo * Preguntar siempre por las optimizaciones de batería diff --git a/fastlane/metadata/android/es-ES/changelogs/4213204.txt b/fastlane/metadata/android/es-ES/changelogs/4213204.txt new file mode 100644 index 0000000000000000000000000000000000000000..806a2b0d9a2ccd03570f32d3d8846fd85126aef1 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4213204.txt @@ -0,0 +1,4 @@ +* Permitir pausar la grabación de audio tocando el temporizador +* Corregir las reacciones en mensajes privados de salas de chat (MUC PMs) +* Dejar de aceptar 'mensajes de respaldo' para reacciones, confirmaciones de lectura y marcadores de visualización +* Agregar más íconos de vista previa de medios diff --git a/fastlane/metadata/android/es-ES/changelogs/4213304.txt b/fastlane/metadata/android/es-ES/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..1fa40f774e1adac06a92084111c54ad8cb252b34 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4213304.txt @@ -0,0 +1 @@ +* Correccion de errores menores diff --git a/fastlane/metadata/android/es-ES/changelogs/4213404.txt b/fastlane/metadata/android/es-ES/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..370e3510031b3a17d03181e0467d6ac357567c1a --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/4213404.txt @@ -0,0 +1 @@ +Solucionar fallo al transferir archivos en la traducción al finés From 53ffcc2df77a4cb124dd18ad7206553a7491a495 Mon Sep 17 00:00:00 2001 From: ghose Date: Tue, 4 Mar 2025 08:02:07 +0000 Subject: [PATCH 020/169] Translated using Weblate (Galician) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 1d31d8f311a26b64d8d9995b7902694c06b6f7cf..47d5cd7810c498fd2ce7dafcfe44b1c632a27a42 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1111,4 +1111,5 @@ Caducidade da conexión Reintentar con P2P Non está dispoñible a vinculación de canles - \ No newline at end of file + Documento de Word + From 9f76b2f724fe7c4ca337b271d7629228b719b594 Mon Sep 17 00:00:00 2001 From: random_r Date: Tue, 4 Mar 2025 13:56:54 +0000 Subject: [PATCH 021/169] Translated using Weblate (Italian) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 236920610056ef78b75f1d1d9d6c31fa883f0987..3bd44179ec94fcc7635668399b1fa4ebb27acc10 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -1125,4 +1125,5 @@ Colore di sfondo, dimensione caratteri, avatar Messaggi di chat Associazione dei canali non disponibile - \ No newline at end of file + Documento Word + From 99665f23f5efa5767a9bb36d23d14599620bc2f5 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Tue, 4 Mar 2025 07:14:16 +0000 Subject: [PATCH 022/169] Translated using Weblate (Polish) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index f875158e509dbe96ef9295392aba157c764c1931..3cb55cc8f9f49f9702f10928f56b4fc5de204386 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -415,7 +415,7 @@ obraz grafika wektorowa plik multimediów - Dokument PDF + dokument PDF Aplikacja Androida Kontakt Avatar został pomyślnie opublikowany! @@ -464,7 +464,7 @@ Pobieranie niepowiodło się: brak możliwości zapisu pliku Pobieranie nieudane: Nieprawidłowy plik Sieć TOR jest niedostepna - Błąd połączenia (zasób) + Błąd przywiązania kanału (zasobu) Nie odpowiada za domenę Zepsute Dostępność @@ -1142,4 +1142,6 @@ Pokazuj wyłącznie kontaktom Limit czasu połączenia Spróbuj ponownie używając P2P - \ No newline at end of file + dokument Microsoft Word + Przywiązywanie kanału niedostępne + From 8f509045ae450be8ec0264379f8e960b46728792 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Tue, 4 Mar 2025 08:40:59 +0000 Subject: [PATCH 023/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index caade01e552aa02b6c5fb6a7364509df943a818a..f6507ca959f57c3bae68dcbef53a9799dcccebc1 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1130,4 +1130,5 @@ Timp limită de conectare expirat Reîncearcă cu P2P Channel binding indisponibil - \ No newline at end of file + Document Word + From c95df84c9ec0033059f96efffb2112eb611a23be Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Tue, 4 Mar 2025 04:03:43 +0000 Subject: [PATCH 024/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 851fa8bc37ece158463cf9c87c9efdc274f9c190..e9250d775b75d5b4df6c2a4ac055de511e358a3f 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -1084,7 +1084,7 @@ 正在使用听筒进行通话,点按即可切换到扬声器。 登录机制 XEP-0386:绑定 2 - XEP-0388:可扩展 SASL 配置文件 + XEP-0388:可扩展 SASL 配置 无法添加回应 添加回应… 更多回应 @@ -1107,4 +1107,5 @@ 连接超时 使用 P2P 重试 不支持通道绑定 - \ No newline at end of file + Word 文档 + From eeedfeafd905091f2d619ff3c97b36779ae8bcbd Mon Sep 17 00:00:00 2001 From: random_r Date: Tue, 4 Mar 2025 13:57:27 +0000 Subject: [PATCH 025/169] Translated using Weblate (Italian) Currently translated at 100.0% (84 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/4213304.txt | 1 + fastlane/metadata/android/it-IT/changelogs/4213404.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/4213404.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4213304.txt b/fastlane/metadata/android/it-IT/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..96f539343c5bea9398d477f671a4610671f9e589 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4213304.txt @@ -0,0 +1 @@ +* correzione di errori minori diff --git a/fastlane/metadata/android/it-IT/changelogs/4213404.txt b/fastlane/metadata/android/it-IT/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..6d3581a35ce1f29073f99c2d5565cb84cb09ab0e --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4213404.txt @@ -0,0 +1 @@ +* Corretto crash su trasferimento file in traduzione fi From f31ac26515ce73a5c03dbac4c323271e290adb13 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Tue, 4 Mar 2025 07:13:34 +0000 Subject: [PATCH 026/169] Translated using Weblate (Polish) Currently translated at 53.5% (45 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/changelogs/42050.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42050.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/42050.txt b/fastlane/metadata/android/pl-PL/changelogs/42050.txt new file mode 100644 index 0000000000000000000000000000000000000000..709665a1946955e751d0a33bec077bf2776dc2d2 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42050.txt @@ -0,0 +1 @@ +* Zwiększenie promienia narożnika zdjęć profilowych From 73474aa440878cbede375e7fca24900b6fd7b37f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 4 Mar 2025 07:50:13 +0000 Subject: [PATCH 027/169] Translated using Weblate (Estonian) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/et/ --- src/main/res/values-et/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index c1e8c9b859b921f50df527d62d23cd2ffe68615c..27b1bf45802c8e70fc288ce524ca8d4b075a0849 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -1131,4 +1131,5 @@ Ühenduse on aegunud Proovi uuesti võrdõigusvõrguga Edastuskanaliga sidumine pole võimalik - \ No newline at end of file + Wordi-dokument + From 21cc1fc75a1fa097994531a27213bb228f4e7a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 4 Mar 2025 09:37:28 +0000 Subject: [PATCH 028/169] Translated using Weblate (Estonian) Currently translated at 98.8% (83 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/et/ --- fastlane/metadata/android/et/changelogs/403.txt | 3 +++ fastlane/metadata/android/et/changelogs/407.txt | 3 +++ fastlane/metadata/android/et/changelogs/42000.txt | 4 ++++ fastlane/metadata/android/et/changelogs/42006.txt | 2 ++ fastlane/metadata/android/et/changelogs/42010.txt | 2 ++ fastlane/metadata/android/et/changelogs/42012.txt | 1 + fastlane/metadata/android/et/changelogs/42013.txt | 1 + fastlane/metadata/android/et/changelogs/42014.txt | 2 ++ fastlane/metadata/android/et/changelogs/42018.txt | 3 +++ fastlane/metadata/android/et/changelogs/42022.txt | 2 ++ fastlane/metadata/android/et/changelogs/42023.txt | 2 ++ 11 files changed, 25 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/403.txt create mode 100644 fastlane/metadata/android/et/changelogs/407.txt create mode 100644 fastlane/metadata/android/et/changelogs/42000.txt create mode 100644 fastlane/metadata/android/et/changelogs/42006.txt create mode 100644 fastlane/metadata/android/et/changelogs/42010.txt create mode 100644 fastlane/metadata/android/et/changelogs/42012.txt create mode 100644 fastlane/metadata/android/et/changelogs/42013.txt create mode 100644 fastlane/metadata/android/et/changelogs/42014.txt create mode 100644 fastlane/metadata/android/et/changelogs/42018.txt create mode 100644 fastlane/metadata/android/et/changelogs/42022.txt create mode 100644 fastlane/metadata/android/et/changelogs/42023.txt diff --git a/fastlane/metadata/android/et/changelogs/403.txt b/fastlane/metadata/android/et/changelogs/403.txt new file mode 100644 index 0000000000000000000000000000000000000000..18b941a51ea2df6c75d2b25b560e774b56578846 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/403.txt @@ -0,0 +1,3 @@ +* Parandasime ühendusvyse vead, kui erinevad kasutajakontod kasutasid erinevaid SCRAMi meetodeid +* Lisasime SCRAM-SHA-512 toe +* Failide teisaldamine Jingle'i protokolliga võrdõigusvõrgus (P2P) iseenda kontoga diff --git a/fastlane/metadata/android/et/changelogs/407.txt b/fastlane/metadata/android/et/changelogs/407.txt new file mode 100644 index 0000000000000000000000000000000000000000..c8bf51fc73461dbddf1301601670b929095f061b --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/407.txt @@ -0,0 +1,3 @@ +* Kui kasutajate kõnetugi on varem tuvastatud, siis kuvame kõne nupu ka siis, kui kasutaja pole võrgus +* Tagasi-nupp ühendatud kõne puhul enam ei lõpeta kõnet +* Veaparandused diff --git a/fastlane/metadata/android/et/changelogs/42000.txt b/fastlane/metadata/android/et/changelogs/42000.txt new file mode 100644 index 0000000000000000000000000000000000000000..c67efc2830958dbbb78d0ad16075306ed48601e3 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42000.txt @@ -0,0 +1,4 @@ +* Võimalus valida helinaid +* Parandasime OpenPGP võtmete tuvastuse OpenKeychain 5.6+puhul +* Mitte-ASCII tähtedes TLS sertifikaatide korrektne verifitseerimine +* Stabiilsem RTP-sessioonide loomine (helistamisel) diff --git a/fastlane/metadata/android/et/changelogs/42006.txt b/fastlane/metadata/android/et/changelogs/42006.txt new file mode 100644 index 0000000000000000000000000000000000000000..03f73be295e78454d18c1e1edc5dfc2114cdcbac --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42006.txt @@ -0,0 +1,2 @@ +* Hääl- ja videokõnede verifitserimine olemasoleva OMEMO sessiooni kontekstis +* Parem ühilduvus muude, kui libwebrtc teekidega loodud WebRTC-ühendustega diff --git a/fastlane/metadata/android/et/changelogs/42010.txt b/fastlane/metadata/android/et/changelogs/42010.txt new file mode 100644 index 0000000000000000000000000000000000000000..3a9e336a7f86233b75e71a43230ad70129734dfb --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42010.txt @@ -0,0 +1,2 @@ +* Erinevad veaparandused Tor toe kontekstis +* Parem kõnede ühilduvus Dinoga diff --git a/fastlane/metadata/android/et/changelogs/42012.txt b/fastlane/metadata/android/et/changelogs/42012.txt new file mode 100644 index 0000000000000000000000000000000000000000..fd58d7c7e188758a60fd85efbb4baa8c98c38326 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42012.txt @@ -0,0 +1 @@ +* Failide üles- ja allalaadimise vea parandus, kui kasutaja ei usalda operatsioonisüsteemis näidatud sertifiktseerimiskeskusi diff --git a/fastlane/metadata/android/et/changelogs/42013.txt b/fastlane/metadata/android/et/changelogs/42013.txt new file mode 100644 index 0000000000000000000000000000000000000000..3e5d0bae396b27c8ce6201f899f78d75f682f369 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42013.txt @@ -0,0 +1 @@ +* „Võrguühendus puudub“ vea parandus Android 7.1 puhul diff --git a/fastlane/metadata/android/et/changelogs/42014.txt b/fastlane/metadata/android/et/changelogs/42014.txt new file mode 100644 index 0000000000000000000000000000000000000000..46c3f42203731339f73fd82784f659058609195a --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42014.txt @@ -0,0 +1,2 @@ +* Domeen kuulub alati verifitseerimisele ja seda kasutaja muuta ei saa +* Serveripõhise kontaktnimestiku eelautentimine diff --git a/fastlane/metadata/android/et/changelogs/42018.txt b/fastlane/metadata/android/et/changelogs/42018.txt new file mode 100644 index 0000000000000000000000000000000000000000..75590bb9a21c05d0d3466bc754b824fae8b7cf95 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42018.txt @@ -0,0 +1,3 @@ +* Kui video puhul kaugseadme ekraani küljesuhe ei klapi, siis lisame pildile mustad ääres +* Parem otsingujõudlus +* Seadistus ekraanitõmmiste tegemise keelamiseks diff --git a/fastlane/metadata/android/et/changelogs/42022.txt b/fastlane/metadata/android/et/changelogs/42022.txt new file mode 100644 index 0000000000000000000000000000000000000000..3ba31cc7a24c8ea3e7d26747c1e1600ca90dbf01 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42022.txt @@ -0,0 +1,2 @@ +* Parandasime vea, kuis mõned videod olid pakkimata +* Parandasime harvaesineva kokkujooksmise teavituse avamisel diff --git a/fastlane/metadata/android/et/changelogs/42023.txt b/fastlane/metadata/android/et/changelogs/42023.txt new file mode 100644 index 0000000000000000000000000000000000000000..d40606cbb35b2828609d682efaefc448c288e256 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42023.txt @@ -0,0 +1,2 @@ +* Parandsime kokkujooksmise mõnede tsitaatide kuvamisel +* Parandsime tervitusekraani kokkujooksmise From 6eb03498436af7ac8c8c9ba9f5471036e8058644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 5 Mar 2025 12:25:23 +0000 Subject: [PATCH 029/169] Translated using Weblate (Estonian) Currently translated at 100.0% (84 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/et/ --- fastlane/metadata/android/et/changelogs/42037.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/42037.txt diff --git a/fastlane/metadata/android/et/changelogs/42037.txt b/fastlane/metadata/android/et/changelogs/42037.txt new file mode 100644 index 0000000000000000000000000000000000000000..859c829eeb052d1826e62bea7014aff1c3b2e7e2 --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/42037.txt @@ -0,0 +1,11 @@ +Versioon 2.10.9 +* Kõnede puhul BT loa küsimine (kui BT kõrvaklappe pole, siis keeldu) +* Movimile helistamise vea parandus +* Rühmavestluste avatariviga +* Küsime akukasutuse optimeerimisest loobumist +* „x kontot ühendatud“ arvestab vaid kohalikke ühendusi +* Parandasime liidestuse Google Maps asukoha jagamise lisamooduliga +* Serveritasu ääremärkus on eemaldatud +* Failide salvestamine vastavalt Android 11 reeglitele +* Kõne taastamine peale võrguvahetust +* Helistaja ja konto JID saabuva kõne vaates From 75782007bd0f81febf60522d44f3a0a09016252f Mon Sep 17 00:00:00 2001 From: "lucasmz.dev" Date: Fri, 7 Mar 2025 01:07:36 +0000 Subject: [PATCH 030/169] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 20ef84f34425ddce5f4a0a20e6d82f2b5eb2326a..24fac1e6892a0de84fc2c86168d2452813a16e82 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -1129,4 +1129,5 @@ Conexão demorou muito Tentar novamente com P2P Vínculo de canal indisponível - \ No newline at end of file + Documento do Word + From e469209fb413e8369928b480aaa4be0b2a82407e Mon Sep 17 00:00:00 2001 From: hsfg6 Date: Fri, 7 Mar 2025 03:28:42 +0000 Subject: [PATCH 031/169] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hant/ --- src/main/res/values-zh-rTW/strings.xml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml index c97a5f96b4a80d7b6b1801f14f63c5de47b096dc..f0814eca90c5db7ebb492a4935827649c04af804 100644 --- a/src/main/res/values-zh-rTW/strings.xml +++ b/src/main/res/values-zh-rTW/strings.xml @@ -432,7 +432,7 @@ 下載失敗:無效的檔案 Tor 網路無法使用 繫結失敗 - 伺服器無法回應此網域 + 無法回應網域 已損毀 可用性 裝置上鎖時離開 @@ -1099,4 +1099,22 @@ 呼叫正在使用揚聲器。 無法添加回應 添加回應… - \ No newline at end of file + 連接超時 + Word 文件 + 自訂通知 + 使用 P2P 重試 + 通道綁定不可用 + 為此對話啟用自定義通知設定(重要性、聲音、振動)設定? + 您聯絡人的 XMPP 用戶端可能不支援音訊/視訊通話。 + 左對齊訊息 + 在左側顯示所有訊息,包括已傳送的訊息,以實現統一的聊天佈局。 + 呼叫集成 + 來自此應用程式的呼叫與常規電話交互,例如在另一個呼叫開始時結束一個呼叫。 + 是否要刪除您的大頭貼?某些用戶端可能會繼續顯示您的大頭貼的緩存副本。 + 僅向聯絡人顯示 + 聊天氣泡 + 背景顏色、字體大小、大頭貼 + 聊天氣泡 + 顯示大頭貼 + 除了群聊之外,還可以顯示您的訊息和 1 對 1 聊天的大頭貼。 + From 255f7d8dbb83f276e5e43eba585dd8bbd5783e30 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 7 Mar 2025 11:47:42 +0100 Subject: [PATCH 032/169] fix typo in previous commit --- .../java/eu/siacs/conversations/xmpp/jingle/IceServers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java index 3ef291c97b59ec86abca1a96b342711eb557ae43..82026096d0581694e6272ed4e04d71160305c987 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java @@ -18,7 +18,7 @@ import org.webrtc.PeerConnection; public final class IceServers { public static Set parse(final Iq response) { - if (response.getType() == Iq.Type.RESULT) { + if (response.getType() != Iq.Type.RESULT) { return Collections.emptySet(); } final var builder = new ImmutableSet.Builder(); From 363211c086eb555e4ca6899118eb7953204b34b8 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 7 Mar 2025 12:07:10 +0100 Subject: [PATCH 033/169] use modern api to retrieve ice servers --- .../conversations/xmpp/jingle/IceServers.java | 83 ++----------------- .../xmpp/jingle/JingleRtpConnection.java | 3 +- .../WebRTCDataChannelTransport.java | 4 +- .../xmpp/model/disco/external/Services.java | 80 ++++++++++++++++++ 4 files changed, 89 insertions(+), 81 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java index 82026096d0581694e6272ed4e04d71160305c987..d852bd5f96017f6e3c65e14a1fa309a8ff4341b8 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java @@ -1,17 +1,8 @@ package eu.siacs.conversations.xmpp.jingle; -import android.util.Log; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet; -import com.google.common.primitives.Ints; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.IP; -import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.xmpp.model.disco.external.Services; import im.conversations.android.xmpp.model.stanza.Iq; -import java.util.Arrays; import java.util.Collections; -import java.util.List; import java.util.Set; import org.webrtc.PeerConnection; @@ -21,74 +12,10 @@ public final class IceServers { if (response.getType() != Iq.Type.RESULT) { return Collections.emptySet(); } - final var builder = new ImmutableSet.Builder(); - final Element services = - response.findChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); - final List children = - services == null ? Collections.emptyList() : services.getChildren(); - for (final Element child : children) { - if ("service".equals(child.getName())) { - final String type = child.getAttribute("type"); - final String host = child.getAttribute("host"); - final String sport = child.getAttribute("port"); - final Integer port = sport == null ? null : Ints.tryParse(sport); - final String transport = child.getAttribute("transport"); - final String username = child.getAttribute("username"); - final String password = child.getAttribute("password"); - if (Strings.isNullOrEmpty(host) || port == null) { - continue; - } - if (port < 0 || port > 65535) { - continue; - } - - if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) - && Arrays.asList("udp", "tcp").contains(transport)) { - if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) { - Log.w( - Config.LOGTAG, - "skipping invalid combination of udp/tls in external services"); - continue; - } - - // STUN URLs do not support a query section since M110 - final String uri; - if (Arrays.asList("stun", "stuns").contains(type)) { - uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port); - } else { - uri = - String.format( - "%s:%s:%s?transport=%s", - type, IP.wrapIPv6(host), port, transport); - } - - final PeerConnection.IceServer.Builder iceServerBuilder = - PeerConnection.IceServer.builder(uri); - iceServerBuilder.setTlsCertPolicy( - PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); - if (username != null && password != null) { - iceServerBuilder.setUsername(username); - iceServerBuilder.setPassword(password); - } else if (Arrays.asList("turn", "turns").contains(type)) { - // The WebRTC spec requires throwing an - // InvalidAccessError when username (from libwebrtc - // source coder) - // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc - Log.w( - Config.LOGTAG, - "skipping " - + type - + "/" - + transport - + " without username and password"); - continue; - } - final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); - Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer); - builder.add(iceServer); - } - } + final var services = response.getExtension(Services.class); + if (services == null) { + return Collections.emptySet(); } - return builder.build(); + return services.getIceServers(); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index cd35d163b8bafcaf1a8644209c5a0a04bc05d0e9..895cadcf8087d3b08a209a698720efcf49009314 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -44,6 +44,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Proceed; import eu.siacs.conversations.xmpp.jingle.stanzas.Propose; import eu.siacs.conversations.xmpp.jingle.stanzas.Reason; import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription; +import im.conversations.android.xmpp.model.disco.external.Services; import im.conversations.android.xmpp.model.jingle.Jingle; import im.conversations.android.xmpp.model.stanza.Iq; import java.util.Arrays; @@ -2824,7 +2825,7 @@ public class JingleRtpConnection extends AbstractJingleConnection if (id.account.getXmppConnection().getFeatures().externalServiceDiscovery()) { final Iq request = new Iq(Iq.Type.GET); request.setTo(id.account.getDomain()); - request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + request.addExtension(new Services()); xmppConnectionService.sendIqPacket( id.account, request, diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java index 134c0be69bf6cf7b8a22f9ede6dafe2060c06eee..62d06f796510d53fb5c691d7af012a1f1d944105 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java @@ -14,12 +14,12 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jingle.IceServers; import eu.siacs.conversations.xmpp.jingle.WebRTCWrapper; import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo; import eu.siacs.conversations.xmpp.jingle.stanzas.WebRTCDataChannelTransportInfo; +import im.conversations.android.xmpp.model.disco.external.Services; import im.conversations.android.xmpp.model.stanza.Iq; import java.io.IOException; import java.io.InputStream; @@ -233,7 +233,7 @@ public class WebRTCDataChannelTransport implements Transport { SettableFuture.create(); final Iq request = new Iq(Iq.Type.GET); request.setTo(this.account.getDomain()); - request.addChild("services", Namespace.EXTERNAL_SERVICE_DISCOVERY); + request.addExtension(new Services()); xmppConnection.sendIqPacket( request, (response) -> { diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java index 36338083da5d03530ee42027f618225e8c248632..61045a2b8c8dd939d288499c053f3cd5e5f45a37 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java @@ -1,7 +1,17 @@ package im.conversations.android.xmpp.model.disco.external; +import android.util.Log; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.common.primitives.Ints; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.IP; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import org.webrtc.PeerConnection; @XmlElement public class Services extends Extension { @@ -9,4 +19,74 @@ public class Services extends Extension { public Services() { super(Services.class); } + + public Collection getServices() { + return this.getExtensions(Service.class); + } + + public Set getIceServers() { + final var builder = new ImmutableSet.Builder(); + for (final var service : this.getServices()) { + final String type = service.getAttribute("type"); + final String host = service.getAttribute("host"); + final String sport = service.getAttribute("port"); + final Integer port = sport == null ? null : Ints.tryParse(sport); + final String transport = service.getAttribute("transport"); + final String username = service.getAttribute("username"); + final String password = service.getAttribute("password"); + if (Strings.isNullOrEmpty(host) || port == null) { + continue; + } + if (port < 0 || port > 65535) { + continue; + } + + if (Arrays.asList("stun", "stuns", "turn", "turns").contains(type) + && Arrays.asList("udp", "tcp").contains(transport)) { + if (Arrays.asList("stuns", "turns").contains(type) && "udp".equals(transport)) { + Log.w( + Config.LOGTAG, + "skipping invalid combination of udp/tls in external services"); + continue; + } + + // STUN URLs do not support a query section since M110 + final String uri; + if (Arrays.asList("stun", "stuns").contains(type)) { + uri = String.format("%s:%s:%s", type, IP.wrapIPv6(host), port); + } else { + uri = + String.format( + "%s:%s:%s?transport=%s", + type, IP.wrapIPv6(host), port, transport); + } + + final PeerConnection.IceServer.Builder iceServerBuilder = + PeerConnection.IceServer.builder(uri); + iceServerBuilder.setTlsCertPolicy( + PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK); + if (username != null && password != null) { + iceServerBuilder.setUsername(username); + iceServerBuilder.setPassword(password); + } else if (Arrays.asList("turn", "turns").contains(type)) { + // The WebRTC spec requires throwing an + // InvalidAccessError when username (from libwebrtc + // source coder) + // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc + Log.w( + Config.LOGTAG, + "skipping " + + type + + "/" + + transport + + " without username and password"); + continue; + } + final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); + Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer); + builder.add(iceServer); + } + } + return builder.build(); + } } From 7f77454ac12f137eb0683cb2a7a1bd7cff57179a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 10 Mar 2025 13:44:20 +0100 Subject: [PATCH 034/169] add log line for when id can't be found for reaction --- .../siacs/conversations/services/XmppConnectionService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a27886f5e3851da778eaa84407adf9e77989558e..098be18599be98d0010dd783d0daafaff6d32fb2 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -5721,13 +5721,13 @@ public class XmppConnectionService extends Service { if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) { final var mucOptions = conversation.getMucOptions(); if (!mucOptions.participating()) { - Log.d(Config.LOGTAG, "not participating in MUC"); + Log.e(Config.LOGTAG, "not participating in MUC"); return false; } final var self = mucOptions.getSelf(); final String occupantId = self.getOccupantId(); if (Strings.isNullOrEmpty(occupantId)) { - Log.d(Config.LOGTAG, "occupant id not found for reaction in MUC"); + Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC"); return false; } final var existingRaw = @@ -5771,6 +5771,7 @@ public class XmppConnectionService extends Service { conversation.getAccount().getJid()); } if (reactTo == null || Strings.isNullOrEmpty(reactToId)) { + Log.e(Config.LOGTAG, "could not find id to react to"); return false; } final var reactionMessage = From a512c659a50a6b02a166933d2395a0fd1fd2e58b Mon Sep 17 00:00:00 2001 From: nautilusx Date: Sat, 8 Mar 2025 08:28:21 +0000 Subject: [PATCH 035/169] Translated using Weblate (German) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 74b0b9c7e2a5c294979646c2e6716009dea7ddbe..1b7830371c193a18867fe690c01a03a53a2db422 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -1111,5 +1111,5 @@ Zeitüberschreitung beim Verbinden Erneut mit P2P versuchen Kanalbindung nicht verfügbar - Worddatei + Word-Dokument From df7cbb2fbef105e132a5c70a2b0008c7478de466 Mon Sep 17 00:00:00 2001 From: user11 Date: Sat, 8 Mar 2025 10:44:44 +0000 Subject: [PATCH 036/169] Translated using Weblate (Serbian) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sr/ --- src/main/res/values-sr/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 4558a2aa2c3c5e973bacaaf7b66198b3f6029b85..f885961afddb736259ab0a147aa856877e414281 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -1145,4 +1145,5 @@ Истекла веза Покушај поново са P2P Везивање канала недоступно - \ No newline at end of file + Word документ + From a65f2be1e2389e6c7f36e8195cce22bc150f551a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 12 Mar 2025 15:25:46 +0100 Subject: [PATCH 037/169] resend muc presence after gaining occupant-id feature --- .../conversations/parser/PresenceParser.java | 7 ++++++ .../services/XmppConnectionService.java | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index eef615de358c79ba745f5695913b476d8def213a..038537dfe28d8051b0fd5aa8021553b06178180a 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -88,6 +88,13 @@ public class PresenceParser extends AbstractParser && jid.equals( Jid.Invalid.getNullForInvalid( item.getAttributeAsJid("jid"))))) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": got self-presence from " + + user.getFullJid() + + ". occupant-id=" + + occupantId); if (mucOptions.setOnline()) { mXmppConnectionService.getAvatarService().clear(mucOptions); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 098be18599be98d0010dd783d0daafaff6d32fb2..ce482490e10f26ed49e3ed29aea70d472cb4ef99 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -3615,7 +3615,7 @@ public class XmppConnectionService extends Service { } private void joinMuc( - Conversation conversation, + final Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) { final Account account = conversation.getAccount(); @@ -4327,6 +4327,7 @@ public class XmppConnectionService extends Service { bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName()); + final var hadOccupantId = mucOptions.occupantId(); if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) { Log.d( Config.LOGTAG, @@ -4336,6 +4337,25 @@ public class XmppConnectionService extends Service { updateConversation(conversation); } + final var hasOccupantId = mucOptions.occupantId(); + + if (!hadOccupantId && hasOccupantId && mucOptions.online()) { + final var me = mucOptions.getSelf().getFullJid(); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": gained support for occupant-id in " + + me + + ". resending presence"); + final var packet = + mPresenceGenerator.selfPresence( + account, + Presence.Status.ONLINE, + mucOptions.nonanonymous()); + packet.setTo(me); + sendPresencePacket(account, packet); + } + if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) { if (bookmark.setBookmarkName( From e721b957cb251d74b533626f46637e23c79a931e Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 13 Mar 2025 10:20:31 +0100 Subject: [PATCH 038/169] use custom equals method for ice server set --- .../conversations/xmpp/jingle/IceServers.java | 4 +- .../xmpp/jingle/JingleRtpConnection.java | 8 ++-- .../xmpp/jingle/WebRTCWrapper.java | 7 +-- .../WebRTCDataChannelTransport.java | 8 ++-- .../xmpp/model/disco/external/Services.java | 48 ++++++++++++++++--- 5 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java index d852bd5f96017f6e3c65e14a1fa309a8ff4341b8..cc383be716bab5e16a06fbfd98a13b2c3a72c77e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/IceServers.java @@ -2,13 +2,13 @@ package eu.siacs.conversations.xmpp.jingle; import im.conversations.android.xmpp.model.disco.external.Services; import im.conversations.android.xmpp.model.stanza.Iq; +import java.util.Collection; import java.util.Collections; -import java.util.Set; import org.webrtc.PeerConnection; public final class IceServers { - public static Set parse(final Iq response) { + public static Collection parse(final Iq response) { if (response.getType() != Iq.Type.RESULT) { return Collections.emptySet(); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 895cadcf8087d3b08a209a698720efcf49009314..13ecb8d2c60e840764df7de04692a007755df304 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -1340,7 +1340,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private synchronized void sendSessionAccept( final Set media, final SessionDescription offer, - final Set iceServers) { + final Collection iceServers) { if (isTerminated()) { Log.w( Config.LOGTAG, @@ -1824,7 +1824,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private synchronized void sendSessionInitiate( final Set media, final State targetState, - final Set iceServers) { + final Collection iceServers) { if (isTerminated()) { Log.w( Config.LOGTAG, @@ -2321,7 +2321,7 @@ public class JingleRtpConnection extends AbstractJingleConnection private void setupWebRTC( final Set media, - final Set iceServers, + final Collection iceServers, final boolean trickle) throws WebRTCWrapper.InitializationException { this.jingleConnectionManager.ensureConnectionIsRegistered(this); @@ -2957,6 +2957,6 @@ public class JingleRtpConnection extends AbstractJingleConnection } private interface OnIceServersDiscovered { - void onIceServersDiscovered(Set iceServers); + void onIceServersDiscovered(Collection iceServers); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java index d5c3be53fe411a6c68550d04ed6285e93457b12c..09e95a7cd89e5d055e068fa48597aed5c700f03e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java @@ -13,6 +13,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; import eu.siacs.conversations.Config; import eu.siacs.conversations.services.XmppConnectionService; +import java.util.Collection; import java.util.LinkedList; import java.util.Queue; import java.util.Set; @@ -230,7 +231,7 @@ public class WebRTCWrapper { synchronized void initializePeerConnection( final Set media, - final Set iceServers, + final Collection iceServers, final boolean trickle) throws InitializationException { Preconditions.checkState(this.eglBase != null); @@ -373,7 +374,7 @@ public class WebRTCWrapper { } public static PeerConnection.RTCConfiguration buildConfiguration( - final Set iceServers, final boolean trickle) { + final Collection iceServers, final boolean trickle) { final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(ImmutableList.copyOf(iceServers)); rtcConfig.tcpCandidatePolicy = @@ -749,7 +750,7 @@ public class WebRTCWrapper { } } - static class InitializationException extends Exception { + public static class InitializationException extends Exception { private InitializationException(final String message, final Throwable throwable) { super(message, throwable); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java index 62d06f796510d53fb5c691d7af012a1f1d944105..80b0322f4727cf418d7f37de58d9cb6feb1ddbf6 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/transports/WebRTCDataChannelTransport.java @@ -30,11 +30,11 @@ import java.io.PipedOutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; +import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; -import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -224,12 +224,12 @@ public class WebRTCDataChannelTransport implements Transport { } } - private ListenableFuture> getIceServers() { + private ListenableFuture> getIceServers() { if (Config.DISABLE_PROXY_LOOKUP) { return Futures.immediateFuture(Collections.emptySet()); } if (xmppConnection.getFeatures().externalServiceDiscovery()) { - final SettableFuture> iceServerFuture = + final SettableFuture> iceServerFuture = SettableFuture.create(); final Iq request = new Iq(Iq.Type.GET); request.setTo(this.account.getDomain()); @@ -254,7 +254,7 @@ public class WebRTCDataChannelTransport implements Transport { } private PeerConnection createPeerConnection( - final Set iceServers, final boolean trickle) { + final Collection iceServers, final boolean trickle) { final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle); final PeerConnection peerConnection = requirePeerConnectionFactory() diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java index 61045a2b8c8dd939d288499c053f3cd5e5f45a37..ecf24606cc97bd60ff71b86f71b5674443ea705e 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java @@ -1,7 +1,12 @@ package im.conversations.android.xmpp.model.disco.external; import android.util.Log; + +import androidx.annotation.NonNull; + +import com.google.common.base.Objects; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Ints; import eu.siacs.conversations.Config; @@ -10,7 +15,6 @@ import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; import java.util.Arrays; import java.util.Collection; -import java.util.Set; import org.webrtc.PeerConnection; @XmlElement @@ -24,8 +28,8 @@ public class Services extends Extension { return this.getExtensions(Service.class); } - public Set getIceServers() { - final var builder = new ImmutableSet.Builder(); + public Collection getIceServers() { + final var builder = new ImmutableSet.Builder(); for (final var service : this.getServices()) { final String type = service.getAttribute("type"); final String host = service.getAttribute("host"); @@ -70,8 +74,7 @@ public class Services extends Extension { iceServerBuilder.setPassword(password); } else if (Arrays.asList("turn", "turns").contains(type)) { // The WebRTC spec requires throwing an - // InvalidAccessError when username (from libwebrtc - // source coder) + // InvalidAccessError on empty username or password // https://chromium.googlesource.com/external/webrtc/+/master/pc/ice_server_parsing.cc Log.w( Config.LOGTAG, @@ -82,11 +85,42 @@ public class Services extends Extension { + " without username and password"); continue; } - final PeerConnection.IceServer iceServer = iceServerBuilder.createIceServer(); + final var iceServer = new IceServerWrapper(iceServerBuilder.createIceServer()); Log.w(Config.LOGTAG, "discovered ICE Server: " + iceServer); builder.add(iceServer); } } - return builder.build(); + final var set = builder.build(); + Log.d(Config.LOGTAG, "discovered " + set.size() + " ice servers"); + return Collections2.transform(set, i -> i.iceServer); + } + + private static class IceServerWrapper { + + private final PeerConnection.IceServer iceServer; + + private IceServerWrapper(final PeerConnection.IceServer iceServer) { + this.iceServer = iceServer; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof IceServerWrapper that)) return false; + return Objects.equal(iceServer.urls, that.iceServer.urls) + && Objects.equal(iceServer.username, that.iceServer.username) + && Objects.equal(iceServer.password, that.iceServer.password); + } + + @Override + public int hashCode() { + return Objects.hashCode(iceServer.urls, iceServer.urls, iceServer.password); + } + + @Override + @NonNull + public String toString() { + return this.iceServer.toString(); + } } } From e2557bb9f5743ef01ea8a2b1b95530aa46f5af1a Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 13 Mar 2025 12:24:43 +0100 Subject: [PATCH 039/169] remove unused code from DownloadableFile --- .../entities/DownloadableFile.java | 163 +++++++++--------- .../jingle/JingleFileTransferConnection.java | 5 + .../xmpp/model/disco/external/Services.java | 2 - 3 files changed, 83 insertions(+), 87 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index 072b4fd065248948a6c473d053b6e08fcc6fd439..841bcee515c22dc11605881495efeaace0ab0eb9 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -1,94 +1,87 @@ package eu.siacs.conversations.entities; import android.util.Log; - -import java.io.File; - import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.MimeUtils; +import java.io.File; public class DownloadableFile extends File { - private static final long serialVersionUID = 2247012619505115863L; - - private long expectedSize = 0; - private byte[] sha1sum; - private byte[] aeskey; - private byte[] iv; - - public DownloadableFile(final File parent, final String file) { - super(parent, file); - } - - public DownloadableFile(String path) { - super(path); - } - - public long getSize() { - return super.length(); - } - - public long getExpectedSize() { - return this.expectedSize; - } - - public String getMimeType() { - String path = this.getAbsolutePath(); - int start = path.lastIndexOf('.') + 1; - if (start < path.length()) { - String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start)); - return mime == null ? "" : mime; - } else { - return ""; - } - } - - public void setExpectedSize(long size) { - this.expectedSize = size; - } - - public byte[] getSha1Sum() { - return this.sha1sum; - } - - public void setSha1Sum(byte[] sum) { - this.sha1sum = sum; - } - - public void setKeyAndIv(byte[] keyIvCombo) { - // originally, we used a 16 byte IV, then found for aes-gcm a 12 byte IV is ideal - // this code supports reading either length, with sending 12 byte IV to be done in future - if (keyIvCombo.length == 48) { - this.aeskey = new byte[32]; - this.iv = new byte[16]; - System.arraycopy(keyIvCombo, 0, this.iv, 0, 16); - System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32); - } else if (keyIvCombo.length == 44) { - this.aeskey = new byte[32]; - this.iv = new byte[12]; - System.arraycopy(keyIvCombo, 0, this.iv, 0, 12); - System.arraycopy(keyIvCombo, 12, this.aeskey, 0, 32); - } else if (keyIvCombo.length >= 32) { - this.aeskey = new byte[32]; - this.iv = new byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; - System.arraycopy(keyIvCombo, 0, aeskey, 0, 32); - } - Log.d(Config.LOGTAG,"using "+this.iv.length+"-byte IV for file transmission"); - } - - public void setKey(byte[] key) { - this.aeskey = key; - } - - public void setIv(byte[] iv) { - this.iv = iv; - } - - public byte[] getKey() { - return this.aeskey; - } - - public byte[] getIv() { - return this.iv; - } + private static final long serialVersionUID = 2247012619505115863L; + + private long expectedSize = 0; + private byte[] aeskey; + private byte[] iv; + + public DownloadableFile(final File parent, final String file) { + super(parent, file); + } + + public DownloadableFile(String path) { + super(path); + } + + public long getSize() { + return super.length(); + } + + public long getExpectedSize() { + return this.expectedSize; + } + + public String getMimeType() { + String path = this.getAbsolutePath(); + int start = path.lastIndexOf('.') + 1; + if (start < path.length()) { + String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start)); + return mime == null ? "" : mime; + } else { + return ""; + } + } + + public void setExpectedSize(long size) { + this.expectedSize = size; + } + + public void setKeyAndIv(byte[] keyIvCombo) { + // originally, we used a 16 byte IV, then found for aes-gcm a 12 byte IV is ideal + // this code supports reading either length, with sending 12 byte IV to be done in future + if (keyIvCombo.length == 48) { + this.aeskey = new byte[32]; + this.iv = new byte[16]; + System.arraycopy(keyIvCombo, 0, this.iv, 0, 16); + System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32); + } else if (keyIvCombo.length == 44) { + this.aeskey = new byte[32]; + this.iv = new byte[12]; + System.arraycopy(keyIvCombo, 0, this.iv, 0, 12); + System.arraycopy(keyIvCombo, 12, this.aeskey, 0, 32); + } else if (keyIvCombo.length >= 32) { + this.aeskey = new byte[32]; + this.iv = + new byte[] { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0xf + }; + System.arraycopy(keyIvCombo, 0, aeskey, 0, 32); + } + Log.d(Config.LOGTAG, "using " + this.iv.length + "-byte IV for file transmission"); + } + + public void setKey(byte[] key) { + this.aeskey = key; + } + + public void setIv(byte[] iv) { + this.iv = iv; + } + + public byte[] getKey() { + return this.aeskey; + } + + public byte[] getIv() { + return this.iv; + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java index ca035d5dfc7a7ab00c165c14e6b6db78d95a9f35..fbaf7ffe3e7a52d5a9148cc780cb0d6135448912 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java @@ -355,6 +355,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection Log.d( Config.LOGTAG, "got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage)); + // TODO store hashes if there are any setFileOffer(file); if (keyTransportMessage != null) { this.transportSecurity = @@ -548,10 +549,13 @@ public class JingleFileTransferConnection extends AbstractJingleConnection private void receiveSessionInfoChecksum(final FileTransferDescription.Checksum checksum) { Log.d(Config.LOGTAG, "received checksum " + checksum); + // TODO check that we are receiver + // TODO store hashes } private void receiveSessionInfoReceived(final FileTransferDescription.Received received) { Log.d(Config.LOGTAG, "peer confirmed received " + received); + // TODO check that we are sender } private synchronized void receiveSessionTerminate(final Iq jinglePacket, final Jingle jingle) { @@ -902,6 +906,7 @@ public class JingleFileTransferConnection extends AbstractJingleConnection sendSessionInfoChecksum(hashes); } else { Log.d(Config.LOGTAG, "file transfer complete " + hashes); + // TODO compare with stored file hashes sendFileSessionInfoReceived(); terminateTransport(); messageReceivedSuccess(); diff --git a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java index ecf24606cc97bd60ff71b86f71b5674443ea705e..b3d64f803c4aa18a1e84d8bf8c540f15e741aa3f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java +++ b/src/main/java/im/conversations/android/xmpp/model/disco/external/Services.java @@ -1,9 +1,7 @@ package im.conversations.android.xmpp.model.disco.external; import android.util.Log; - import androidx.annotation.NonNull; - import com.google.common.base.Objects; import com.google.common.base.Strings; import com.google.common.collect.Collections2; From ae8cf0d7e4b5d96478e51966fb6d257986f71c1b Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 14 Mar 2025 10:12:35 +0100 Subject: [PATCH 040/169] bump guava in annotation-processor --- libs/annotation-processor/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/annotation-processor/build.gradle b/libs/annotation-processor/build.gradle index 6232f33c6e557d384e4e63db790d24e9ea0864c6..3f537ec28c6f1c7f1fb8071f80cd1e749842c579 100644 --- a/libs/annotation-processor/build.gradle +++ b/libs/annotation-processor/build.gradle @@ -15,6 +15,6 @@ dependencies { annotationProcessor 'com.google.auto.service:auto-service:1.0.1' api 'com.google.auto.service:auto-service-annotations:1.0.1' - implementation 'com.google.guava:guava:31.1-jre' + implementation 'com.google.guava:guava:33.4.0-jre' } \ No newline at end of file From 16ce3353a5f299163e5863ce00fb7f93aeb34aaf Mon Sep 17 00:00:00 2001 From: codimp Date: Wed, 12 Mar 2025 01:12:01 +0000 Subject: [PATCH 041/169] Translated using Weblate (French) Currently translated at 100.0% (1062 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fr/ --- src/main/res/values-fr/strings.xml | 85 +++++++++++++++++++----------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index 965e650ac9bcf886945568510a3caa34da30dd2e..cc0e711f1dafe51f85a427f9d63659e6ae467c99 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -66,10 +66,10 @@ %1$s a planté En utilisant votre compte XMPP pour envoyer des rapports de crash, vous aidez le développement de %1$s. Envoyer - Ne plus me demander - Impossible de se connecter au compte. - Impossible de se connecter aux comptes. - Appuyez pour gérer vos comptes. + Ne plus demander + Impossible de se connecter au compte + Impossible de se connecter à plusieurs comptes + Appuyez pour gérer vos comptes Joindre un fichier Ajouter ce contact manquant à votre liste de contact ? Ajouter un contact @@ -87,15 +87,15 @@ \n \nAvertissement : Cela ne supprimera pas les copies de ce fichier qui sont stockées sur d\'autres appareils ou serveurs. Choisir l\'appareil - Envoyer un message en clair + Envoyer un message non-chiffré Envoyer le message Envoyer un message à %s - Envoyer un message chiffré avec \\OMEMO - Votre identifiant a été changé - Envoyer en clair - Échec du déchiffrement. Avez-vous la bonne clé privée ? + Envoyer un message chiffré avec v\\OMEMO + Votre pseudo a été changé + Envoyer non-chiffré + Échec du déchiffrement. Peut-être que vous n\'avez pas la bonne clé privée. OpenKeychain - %1$s utilise <b>OpenKeychain</b> pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.<br><br>OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.<br><br><small>(Veuillez redémarrer %1$s après l\'installation de l\'application.)</small> + OpenKeychain pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.

OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.

(Veuillez redémarrer %1$s après l'installation de l'application.)]]>
Redémarrer Installer Veuillez installer OpenKeychain @@ -132,9 +132,9 @@ Une erreur s\'est produite Erreur Votre compte - Envoyer mes màj de disponibilité - Recevoir ses màj de disponibilité - Demander les màj de disponibilité + Envoyer mes màj de statut + Recevoir ses màj de statut + Demander les màj de statut Choisir une image Prendre une photo Accepter par avance les demandes de publication @@ -169,14 +169,14 @@ OTR OpenPGP OMEMO - Supprimer + Supprimer le compte Désactiver temporairement Publier une image de profil Publier la clé publique OpenPGP Supprimer la clé publique OpenPGP Êtes-vous sûr de vouloir supprimer votre clé publique OpenPGP de votre annonce de présence ?\nVos contacts ne pourront plus vous envoyer de message chiffrés avec OpenPGP. Clé publique OpenPGP publiée. - Activer le compter + Activer le compte Êtes-vous sûr de vouloir supprimer votre compte ? Supprimer votre compte effacera l\'historique de vos conversations Enregistrer un son Adresse XMPP @@ -191,10 +191,10 @@ XEP-0280 : Copies carbone XEP-0352 : Indication statut client XEP-0191 : Commande de blocage - XEP-0237 : Révision contacts + XEP-0237 : Versioning des contacts XEP-0198 : Gestion des flux XEP-0215 : Découverte de service externe - XEP-0163 : PEP (Avatars / OMEMO) + XEP-0163 : PEP (Image de profil / OMEMO) XEP-0363 : Envoi de fichiers via HTTP XEP-0357 : Notifications Push supportée @@ -224,11 +224,11 @@ Afficher les détails du contact Bloquer le contact Débloquer le contact - Ajouter + Créer Sélectionner Le contact existe déjà Rejoindre - salon@conference.example.com/surnom + salon@conference.example.com/pseudo salon@conference.example.com Enregistrer comme favori Supprimer le favori @@ -244,14 +244,14 @@ Sujet Rejoindre le groupe… Partir - Votre correspondant vous a ajouté dans sa liste de contacts + Votre correspondant·e vous a ajouté dans sa liste de contacts Ajouter en retour %s a tout lu jusqu\'ici %s ont tout lu jusqu\'ici - %1$s+%2$d autres ont tout lu jusqu\'ici + %1$s +%2$d autres ont tout lu jusqu\'ici Tout le monde a lu jusqu\'ici Publier - Appuyer sur l\'image de profil pour choisir une image depuis la galerie + Appuyer sur l\'image de profil pour en choisir une depuis la galerie Mise à jour… Le serveur a rejeté votre publication Impossible de convertir votre image @@ -304,7 +304,7 @@ Options du message Citation Coller en tant que citation - Copier l\'URL + Copier l\'URL originale Envoyer de nouveau URL du fichier URL copiée dans le presse-papier @@ -361,7 +361,7 @@ Mise à jour… Mot de passe modifié ! Impossible de changer le mot de passe - Changer de mot de passe + Changer le mot de passe Mot de passe actuel Nouveau mot de passe Le mot de passe ne peut pas être vide @@ -370,7 +370,7 @@ Faire une action avec Aucune affiliation Hors ligne - Banni + Banni·e Membre Mode avancé Accorder des privilèges aux membres @@ -404,7 +404,7 @@ Touche Entrée pour envoyer Utilisez la touche Entrée pour envoyer un message. Vous pourrez toujours utiliser la combinaison Ctrl+Entrée pour envoyer un message, même si cette option est désactivée. Afficher la touche Entrée - Remplacer la touche Émoticônes par la touche Entrée + Remplacer la touche Emoji par la touche Entrée audio vidéo image @@ -427,7 +427,7 @@ Position Quitter le groupe privé Quitte le salon public - Ne pas utiliser les CAs système + Ne pas utiliser les AC du système Tous les certificats doivent être approuvés manuellement Retirer les certificats Supprimer les certificats approuvés manuellement @@ -457,7 +457,7 @@ Échec du téléchargement : Fichier non valide Réseau Tor inaccessible La liaison a échoué - Le serveur n\'est pas responsable pour ce domaine + Pas responsable pour le domaine Détraqué Disponibilité Absent quand l\'appareil est verrouillé @@ -772,7 +772,7 @@ Numéro de téléphone Vérifier votre numéro de téléphone Quicksy va envoyer un message SMS (des frais opérateurs sont possibles) pour vérifier votre numéro de téléphone. Saisissez votre code de pays et votre numéro de téléphone : -
%s

. Est-ce correct ou souhaitez-vous modifier le numéro ?]]>
+
%s

. Est-ce correct ou souhaitez-vous modifier le numéro ?]]>
%s n\'est pas un numéro de téléphone valide. Veuillez saisir votre numéro de téléphone. Recherche de pays @@ -877,7 +877,7 @@ Impossible de réaliser cette action Rejoindre le salon public… L\'application de partage n\'a pas accordé la permission d\'accéder à ce fichier. - Salons et groupes de discussion + jabber.network Serveur local La plupart des utilisateur·ices devraient choisir « jabber.network » pour de meilleures suggestions provenant de l’écosystème public entier de XMPP. @@ -1099,7 +1099,7 @@ Modifier les paramètres de notification L\'appel passe par les écouteurs. Tapotez pour passer sur haut-parleur. L\'appel passe par les écouteurs. - XEP-0386 : Bind 2 + XEP-0386: Bind 2 Éditer le pseudo Supprimer la clé OpenPGP L\'appel passe par le bluetooth. @@ -1110,4 +1110,27 @@ L\'appel passe par le haut-parleur. Tapotez pour passer sur les écouteurs. L\'appel passe par le haut-parleur. Mécanisme de connexion + Délai d\'attente de connexion dépassé + Ajouter une réaction… + Couleur de fond, taille de police, images de profil + Document Word + Montrer les images de profil pour vos messages et dans les discussions un-à-un, en plus des groupes de discussion. + Voulez-vous supprimer votre image de profil ? Certains clients pourraient continuer d\'en afficher une version mise en cache. + Réessayer avec P2P + Bulles de discussion + Montrer aux contacts uniquement + Bulles de discussion + Impossible de modifier l\'appel + Channel binding indisponible + Le client XMPP de votre contact peut ne pas prendre en charge les appels audio/vidéo. + Impossible d\'ajouter une réaction + Intégration d\'appel + Messages alignés à gauche + Afficher tous les messages, y compris ceux envoyés, à gauche pour un rendu uniforme. + Notifications personnalisées + Activer les paramètres de notifications personnalisées (importance, son, vibration) pour cette discussion ? + Les appels de cette application intéragissent avec les appels téléphoniques normaux, comme en coupant un appel quand un autre commence. + Plus de réactions + Ajouter une réaction + Montrer l\'image de profil From 2d076e71cc7b7c2b55feaa11d0d1f7e78b5150c5 Mon Sep 17 00:00:00 2001 From: Adolfo Jayme Barrientos Date: Thu, 13 Mar 2025 18:01:48 +0000 Subject: [PATCH 042/169] Translated using Weblate (Spanish) Currently translated at 99.9% (1061 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 209 ++++++++++++++--------------- 1 file changed, 104 insertions(+), 105 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index abfb73c7e5c3370200d57aa140d08e934bab1a10..e437cfc0c32bcac12dcf9f19746a86e2480be716 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -1,6 +1,6 @@ - Ajustes + Configuración Gestionar cuentas Gestionar cuenta Detalles del contacto @@ -16,10 +16,10 @@ Desbloquear dominio Bloquear participante Desbloquear participante - Gestionar Cuentas - Ajustes - Seleccionar Contacto - Seleccionar Contactos + Gestionar cuentas + Configuración + Elegir contacto + Elegir contactos Compartir via cuenta Lista contactos bloqueados ahora @@ -31,18 +31,18 @@ %d chats sin leer enviando… - Descifrando el mensaje. Espere por favor… + Descifrando el mensaje. Espere… Mensaje cifrado con OpenPGP El apodo ya está en uso - Apodo inválido + Apodo no válido Administrador Propietario Moderador Participante Visitante - ¿Quieres eliminar a %s de tu lista de contactos? Los chats con este contacto no se eliminarán. - ¿Quieres bloquear a %s para que no pueda enviarte mensajes? - ¿Quieres desbloquear a %s y permitirle que te envíe mensajes? + ¿Quiere eliminar a %s de su lista de contactos? La conversación con este contacto no se eliminará. + ¿Quiere bloquear a %s para que no pueda enviarle mensajes? + ¿Quiere desbloquear a %s y permitirle que le envíe mensajes? ¿Bloquear todos los contactos de %s? ¿Desbloquear todos los contatos de %s? Contacto bloqueado @@ -62,16 +62,16 @@ Bloquear Desbloquear Guardar - OK + Aceptar %1$s se ha detenido Usar tu cuenta XMPP para enviar trazas de error ayuda al desarrollo de %1$s. Enviar ahora No preguntar de nuevo No se ha podido conectar a la cuenta No se ha podido conectar a varias cuentas - Pulsa aquí para gestionar tus cuentas + Toque para gestionar sus cuentas Adjuntar - El contacto no está en tu lista. ¿Te gustaría añadirlo? + El contacto no está en su lista. ¿Le gustaría añadirlo? Añadir contacto Error al enviar Preparando para enviar imagen @@ -83,26 +83,24 @@ \n \nAviso: Esto no afectará a los mensajes guardados en otros dispositivos o servidores.
Eliminar fichero - ¿Está seguro de que desea eliminar este archivo\? -\n -\nAdvertencia: Esto no eliminará las copias de este archivo almacenadas en otros dispositivos o servidores. - Seleccionar dispositivo + ¿Confirma que quiere eliminar este archivo? \n \nAviso: esto no eliminará las copias de este archivo almacenadas en otros dispositivos o servidores. + Elegir dispositivo Enviar mensaje sin cifrar Enviar mensaje Enviar mensaje a %s Enviar mensaje cifrado v\\OMEMO El apodo ha sido modificado Enviar sin cifrar - Falló el descifrado. Tal vez no tengas la clave privada apropiada. + Falló el descifrado. Tal vez no tenga la clave privada apropiada. OpenKeychain OpenKeychain para cifrar y descifrar mensajes y gestionar tus claves públicas.

Está publicado bajo licencia GPLv3+ y disponible en F-Droid y Google Play.

(Por favor, reinicie %1$s después.)]]>
Reiniciar Instalar - Por favor, instala OpenKeyChain + Instale OpenKeyChain ofreciendo… esperando… Clave OpenPGP no encontrada - No se ha podido cifrar tu mensaje porque tu contacto no está anunciando su clave pública.\n\nPor favor, pide a tu contacto que configure OpenPGP. + No se ha podido cifrar tu mensaje porque su contacto no está anunciando su clave pública.\n\nPida a su contacto que configure OpenPGP. Claves OpenPGP no encontradas No se ha podido cifrar tu mensaje porque tus contactos no están anunciando sus claves públicas.\n\nPor favor, pide a tus contactos que configuren OpenPGP. General @@ -123,7 +121,7 @@ Avanzado Al enviar los informes de los fallos, ayudará a un mayor desarrollo Confirmar mensajes - Permitir a tus contactos saber cuando has recibido y leído sus mensajes + Permitir a sus contactos saber cuando ha recibido y leído sus mensajes Impedir capturas de pantalla Ocultar el contenido de la aplicación en el selector de aplicaciones y bloquear las capturas de pantalla Pantalla @@ -132,11 +130,11 @@ Aceptar Ha ocurrido un error Error - Tu cuenta + Su cuenta Enviar actualizaciones de presencia Recibir actualizaciones de presencia Solicitar actualizaciones de presencia - Seleccionar imagen + Elegir imagen Hacer foto De forma automática conceder suscripción de presencia El archivo seleccionado no es una imagen @@ -148,7 +146,7 @@ \nUse otro administrador de archivos para seleccionar una imagen.
La aplicación que utilizó para compartir este archivo no tiene suficientes permisos. Desconocido - Deshabilitado temporalmente + Desactivado temporalmente Conectado Conectando\u2026 Desconectado @@ -158,7 +156,7 @@ Error en el registro El identificador ya está en uso Registro completado - El servidor no soporta registros + El servidor no admite registros Token de registro inválido Error de negociación TLS Dominio no verificable @@ -172,14 +170,14 @@ OpenPGP OMEMO Eliminar cuenta - Deshabilitar temporalmente - Imagen de perfil + Desactivar temporalmente + Publicar avatar Publicar clave pública OpenPGP Eliminar la clave pública OpenPGP ¿Estás seguro de que quieres eliminar tu clave pública OpenPGP de tu anuncio de presencia?\nTus contactos no podrán enviarte mensajes cifrados con OpenPGP. La clave pública OpenPGP ha sido publicada. - Habilitar - ¿Estás seguro de que desea eliminar tu cuenta? Eliminar tú cuenta borrando todo tu historial del chat + Activar cuenta + ¿Confirma que quiere eliminar su cuenta? Eliminar la cuenta borrará por completo el histórico de conversaciones Grabar audio Dirección XMPP Bloquear dirección XMPP @@ -187,7 +185,7 @@ Contraseña Esta no es una dirección XMPP válida Sin memoria. La imagen es demasiado grande - ¿Quieres añadir a %s a tus contactos? + ¿Quiere añadir a %s a sus contactos? Información de servidor XEP-0313: MAM XEP-0280: Copias de los mensajes @@ -201,7 +199,7 @@ XEP-0357: Notificaciones automáticas No - Se han perdido las claves de anuncio públicas + Anuncios de clave pública no notificados Visto última vez ahora visto última vez hace un minuto Visto última vez hace %d minutos @@ -235,30 +233,30 @@ canal@salas.ejemplo.com Guardar en marcadores Eliminar marcador - Destruir conversación en grupo + Destruir conversación grupal Destruir canal - ¿Estás seguro de que quieres destruir esta conversación en grupo?\n\nAviso:La conversación en grupo será eliminada completamente en el servidor. - ¿Estás seguro de que quieres destruir este canal público?\n\nAviso:El canal será eliminado completamente en el servidor. - No se ha podido destruir la conversación en grupo + ¿Confirma que quiere destruir esta conversación grupal?\n\nAviso: la conversación grupal se eliminará completamente del servidor. + ¿Confirma que quiere destruir este canal público?\n\nAviso: el canal se eliminará completamente del servidor. + No se ha podido destruir la conversación grupal No se ha podido destruir el canal Editar asunto de la conversación Asunto Uniéndose a un chat de grupo… - Salir - El contacto te ha añadido a su lista de contactos - Añadir contacto + Abandonar + El contacto le ha añadido a su lista de contactos + Añadir de vuelta %s ha leído hasta aquí %s han leído hasta aquí %1$s + %2$d han leído hasta aquí Todos han leído hasta aquí Publicar - Pulsa la imagen de perfil para seleccionar una imagen de la galería + Toque el avatar para seleccionar una imagen de la galería Publicando… El servidor rechazó la publicación No se ha podido convertir su imagen No se ha podido guardar la imagen de perfil en disco (O pulsación prolongada para volver a tu imagen de la agenda) - Tu servidor no soporta la publicación de imágenes de perfil + Su servidor no admite la publicación de avatares en privado en privado para %s Enviar mensaje privado a %s @@ -267,14 +265,14 @@ Siguiente Sesión establecida Omitir - Deshabilitar notificaciones - Habilitar + Desactivar notificaciones + Activar Esta conversación en grupo requiere contraseña - Introduce la contraseña + Introduzca la contraseña Por favor, solicita la actualización de presencia a tu contacto primero.\n\nEsto se usará para determinar qué aplicación de mensajería está usando tu contacto. Solicitar ahora Ignorar - Aviso: Si envías esto sin actualización de presencia mutua con tu contacto se podrían producir problemas inesperados.\n\nVe a “Detalles del contacto” para verificar las actualizaciones de presencia. + Aviso: si envía esto sin actualización de presencia mutua con su contacto se podrían producir problemas inesperados.\n\nVaya a «Detalles del contacto» para verificar las actualizaciones de presencia. Seguridad Corrección de los mensajes Permitir a tus contactos editar mensajes previamente enviados @@ -283,7 +281,7 @@ Acerca de %s Horario de silencio Hora de comienzo - Hora de fin + Hora de finalización Habilitar horario de silencio Las notificaciones serán silenciadas durante el horario de silencio Otros @@ -329,7 +327,7 @@ Restaurando copia de respaldo Tu copia de respaldo ha sido restaurada No olvides activar la cuenta. - Seleccionar archivo + Elegir archivo Recibiendo %1$s (%2$d%% completado) Descargar %s Eliminar %s @@ -347,7 +345,7 @@ No se ha encontrado aplicación para ver el contacto Etiquetas dinámicas Mostrar información en forma de etiquetas debajo de los contactos - Habilitar notificaciones + Activar notificaciones No se ha encontrado el servidor de la conversación en grupo No se ha podido crear la conversación en grupo Imagen de perfil @@ -367,8 +365,8 @@ Contraseña actual Nueva contraseña La contraseña no puede ser vacía - Habilitar todas las cuentas - Deshabilitar todas las cuentas + Activar todas las cuentas + Desactivar todas las cuentas Realizar acción con Sin afiliación Desconectado @@ -404,7 +402,7 @@ Marcar como leído Entrada Intro para enviar - Utilizar la tecla Enter para enviar un mensaje. Siempre puedes usar Ctrl+Enter para enviar un mensaje, incluso si esta opción está deshabilitada. + Usar la tecla Intro para enviar un mensaje. Siempre puede usar Ctrl+Intro para enviar un mensaje, incluso si esta opción está desactivada. Mostrar tecla Intro Cambiar la tecla de emoticonos por la tecla Intro audio @@ -413,9 +411,9 @@ gráfico de vectores archivo multimedia documento PDF - Android App + aplicación para Android Contacto - ¡La imagen de perfil ha sido publicada! + Se ha publicado el avatar. Enviando %s Ofreciendo %s Ocultar desconectados @@ -443,7 +441,7 @@ %d certificados eliminados %d certificados eliminados - Cambiar el botón de “Enviar” por el botón de acción rápida + Cambiar el botón «Enviar» por el botón de acción rápida Acción rápida Ninguna Usada más recientemente @@ -486,7 +484,7 @@ Renovar certificado ¡Error buscando clave OMEMO! ¡Clave OMEMO con certificado verificada! - ¡Tu dispositivo no soporta la elección de certificados de cliente! + Su dispositivo no admite la selección de certificados de cliente. Conexión Conectar via Tor Todas las conexiones se realizan a través de la red TOR. Requiere Orbot @@ -514,23 +512,23 @@ \n¡Ningún dato de la lista de contactos sale de tu dispositivo!
Notificar para todos los mensajes Notificar solo cuando eres mencionado - Notificaciones deshabilitadas + Notificaciones desactivadas Notificaciones pausadas Compresión de imagen - Pista: Usa \'Seleccionar archivo\' en lugar de \'Seleccionar imagen\' para enviar imágenes individuales sin comprimir con independencia de los ajustes. + Consejo: use «Elegir archivo» en lugar de «Elegir imagen» para enviar imágenes separadas no comprimidas sin tener en cuenta esta opción. Siempre Solo imágenes de gran tamaño - Optimizaciones de uso de batería habilitadas + Optimizaciones de batería activadas Tu dispositivo está empleando severas optimizaciones del uso de batería por parte de %1$s, lo cual puede hacer que las notificaciones se retrasen o incluso que los mensajes se pierdan.\nEs recomendable deshabilitarlas. Tu dispositivo está empleando severas optimizaciones del uso de batería por parte de %1$s, lo cual puede hacer que las notificaciones se retrasen o incluso que los mensajes se pierdan.\n\nA continuación se te preguntará si quiere deshabilitarlas. - Deshabilitar + Desactivar El área seleccionada es demasiado grande (No hay cuentas activas) Este campo es requerido Corregir mensaje Enviar mensaje corregido - Ya has confiado en la huella digital de esta persona. Al seleccionar “Hecho” solo estás confirmando que %s es parte de este chat grupal. - Has deshabilitado esta cuenta + Ya ha confiado en la huella digital de esta persona. Al seleccionar «Hecho» solo está confirmando que %s es parte de este chat grupal. + Ha desactivado esta cuenta Error de seguridad: ¡Acceso a archivo inválido! No se ha encontrado ninguna aplicación para compartir la URI Compartir URI con… @@ -550,13 +548,13 @@ No disponible Ocupado Se ha generado una contraseña segura - Tu dispositivo no soporta la opción de optimización de batería + Su dispositivo no admite desactivar la optimización de batería El registro falló. Prueba de nuevo más tarde Error en el registro: La contraseña es demasiado débil Elige a los participantes Creando un chat de grupo… Invitar de nuevo - Deshabilitar + Desactivar Corto Medio Largo @@ -593,7 +591,7 @@ Mensaje de error Optimización de datos habilitado Tu sistema operativo está restringiendo a %1$s el acceso a Internet cuando está en segundo plano. Para recibir notificaciones de nuevos mensajes deberías permitir a %1$s un acceso sin restricciones cuando la optimización de datos está habilitada.\n%1$s se esforzará igualmente en ahorrar datos cuando sea posible. - Tu dispositivo no soporta la opción de deshabilitar la optimización de datos para %1$s. + Su dispositivo no admite la desactivación de la optimización de datos para %1$s. No se ha podido crear el archivo temporal Este dispositivo ha sido verificado Copiar huella digital @@ -682,33 +680,33 @@ Copiar al portapapeles Mensaje copiado en el portapapeles Mensaje - Los mensajes privados están deshabilitados + Los mensajes privados están desactivados ¿Aceptar certificado desconocido? El certificado del servidor no está firmado por una Autoridad Certificadora conocida. ¿Aceptar nombre del servidor no coincidente? - El servidor no pudo autenticarse como \"%s\". El certificado es solo válido para: + El servidor no pudo autenticarse como «%s». El certificado solo es válido para: ¿Quieres conectar de todas formas? - Detalles del Certificado: + Detalles del certificado: Una vez El escáner de código QR necesita acceso a la cámara Desplazarse hasta abajo Desplazarse hasta abajo después de mandar un mensaje - Editar Mensaje de Estado + Editar mensaje de estado Editar mensaje de estado - Deshabilitar cifrado + Desactivar cifrado %1$s no puede enviar mensajes cifrados a %2$s. Esto puede deberse a que tu contacto está usando un servidor o un cliente desactualizado que no puede manejar las claves OMEMO. No se ha podido conseguir la lista de dispositivos No se han podido conseguir las claves de cifrado Consejo: En algunas ocasiones esto puede corregirse agregando a tu contacto a tu lista de contactos. Tu contacto deberá asegurarse también que estás en su lista de contactos. ¿Estás seguro de que deseas desactivar el cifrado OMEMO para este chat? \nEsto permitirá que el administrador de su servidor lea sus mensajes, pero podría ser la única forma de comunicarse con personas que utilizan clientes obsoletos. - Deshabilitar ahora + Desactivar ahora Borrador: Cifrado OMEMO OMEMO siempre será usado para conversaciones uno a uno y en conversaciones en grupo privadas. OMEMO será usado por defecto para chats nuevos. OMEMO tendrá que ser activado explícitamente para los nuevos chats. - Crear acceso directo + Crear atajo Activo por defecto Desactivado por defecto El mensaje no fue cifrado para este dispositivo. @@ -718,7 +716,7 @@ Fijar posición Desfijar posición Copiar ubicación - Compatir Ubicación + Compatir ubicación Direcciones Compartir ubicación Mostrar ubicación @@ -733,18 +731,18 @@ Usar el Plugin Compartir Ubicación en lugar del propio de la aplicación Copiar dirección web Copiar dirección XMPP - Compartición de Archivos mediante S3 + Compartición de archivos HTTP con S3 Búsqueda directa - En la pantalla \"Nuevo chat\", abra el teclado y coloque el cursor en el campo de búsqueda - Avatar de la conversación en grupo - El servidor no soporta avatares en conversaciones en grupo + En la pantalla «Chat nuevo», abra el teclado y coloque el cursor en el campo de búsqueda + Avatar de la conversación grupal + El anfitrión no admite avatares en conversaciones grupales Solo el propietario de la conversación puede cambiar el avatar Nombre del contacto Apodo Nombre Añadir un nombre es opcional - Nombre de la Conversación en grupo - Esta conversación en grupo ha sido destruida + Nombre de la conversación grupal + Esta conversación grupal se ha destruido No se ha podido guardar la grabación Servicio en primer plano Esta categoría de notificación se usa para mostrar una notificación permantente indicando que %1$s está ejecutándose. @@ -760,8 +758,8 @@ Mensajes sin sonido Este grupo de notificaciones se usa para mostrar notificaciones que no deberían emitir ningún sonido. Por ejemplo, cuando estás activo en otro dispositivo (periodo de gracia). Envíos fallidos - Ajustes de notificación de mensajes - Ajustes de notificación de llamadas + Configuración de notificaciones de mensajes + Configuración de notificaciones de llamadas Importancia, Sonido, Vibración Compresión de video Ver galería @@ -794,7 +792,7 @@ Atrás Pegado automático del posible PIN desde el portapapeles. Por favor, introduzca su PIN de 6 dígitos. - ¿Estás seguro de que quieres abortar el proceso de registro? + ¿Confirma que quiere interrumpir el proceso de registro? No Verificando… @@ -892,7 +890,7 @@ Método para la búsqueda de Canales Copia de respaldo Acerca de - Por favor, habilita una cuenta + Active una cuenta Hacer una llamada Llamada entrante Videollamada entrante @@ -918,7 +916,7 @@ Video llamada saliente Reconectando llamada Reconectando video llamada - Deshabilitar Tor para hacer llamadas + Desactive Tor para hacer llamadas Llamada entrante Llamada perdida · %s Llamada saliente @@ -976,16 +974,16 @@ No se ha encontrado aplicación Invitar a Conversations No se ha podido leer la invitación - El servidor no soporta la creación de invitaciones - Ninguna cuenta activa soporta esta característica + El servidor no admite la generación de invitaciones + Ninguna cuenta activa admite esta funcionalidad La copia de seguridad ha empezado. Recibirás una notificación cuando se haya completado. - No se ha podido habilitar el vídeo. + No se ha podido activar el vídeo. Documento de texto plano - Los registros de cuenta no están soportados + No se admiten las altas de cuentas Dirección XMPP no encontrada Fallo temporal de autenticación Eliminar imagen de perfil - Las llamadas están deshabilitadas cuando se usa Tor + Las llamadas se desactivan cuando se usa Tor Cambiar a vídeo Rechazar petición de cambiar a vídeo Distribuidor de UnifiedPush @@ -999,7 +997,7 @@ Rechazar Eliminar la cuenta del servidor No se pudo eliminar la cuenta del servidor - Chats en grupo + Conversaciones grupales Buscar un grupo de chats ¡No intentes restaurar las copias de seguridad que no creaste tu mismo! Estás intentando importar un formato de copia de seguridad obsoleto @@ -1011,12 +1009,12 @@ Tu contacto utiliza dispositivos no verificados. Escanea su código QR para realizar la verificación e impedir ataques MITM activos. Desconectarse Desconectado - Estás utilizando dispositivos no verificados. Escanea el código QR de tus otros dispositivos para realizar la verificación e impedir ataques MITM activos. + Está utilizando dispositivos no verificados. Escanee el código QR en sus otros dispositivos para verificarlos e impedir ataques MITM activos. Informar de spam y bloquear al spammer Informar sobre spam ¡Bienvenido a Quicksy! Quicksy pide tu consentimiento para utilizar tus datos - Política de privacidad + Normativa de privacidad La lista de contactos no está disponible Sin permiso para llamar por teléfono Contacto no disponible @@ -1044,11 +1042,11 @@ Al actuar como un Distribuidor de UnifiedPush la conexión XMPP persistente, fiable y de bajo consumo de batería se utilizará para despertar a otras aplicaciones compatibles con UnifiedPush como Tusky, Ltt.rs, FluffyChat y más. Enviar mensaje cifrado Interfaz - Tema, Colores, Capturas de pantalla, Entrada + Tema, colores, capturas de pantalla, entrada Seguridad Relé de notificaciones para aplicaciones de terceros compatibles con UnifiedPush Notificaciones - Período de gracia, Tono de llamada, Vibración, Extraños + Período de gracia, tono de llamada, vibración, extraños Enviando Recibiendo Descarga automática @@ -1073,19 +1071,19 @@ Iniciar un chat El descubrimiento de canales utiliza un servicio de terceros llamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Usar esta función transmitirá tu dirección IP y términos de búsqueda a ese servicio. Consulte su <a href=https://search.jabber.network/privacy>Política de privacidad</a> para obtener más información. ¡No se ha seleccionado ningún certificado de cliente! - Tamaño de archivo, Compresión de imagen, Calidad de vídeo + Tamaño de archivo, compresión de imagen, calidad de vídeo Mostrar el contenido de la aplicación en el conmutador de aplicaciones y permitir la realización de capturas de pantalla Invitaciones de extraños Aceptar invitaciones a chats grupales de extraños Crear una sola vez, Programar recurrentes Crear una copia de seguridad única Copia de seguridad periódica - Operación no soportada + Operación no admitida Notificaciones a pantalla completa Permite que esta aplicación muestre notificaciones de llamadas entrantes que ocupan toda la pantalla cuando el dispositivo está bloqueado. Permitir mensajes privados Más reacciones - Tu avatar. Toca para seleccionar un nuevo avatar de la galería. + Su avatar. Toque para seleccionar un avatar nuevo de la galería. XEP-0386: Vinculación 2 XEP-0388: Perfil SASL Extensible No se pudo desactivar el video. @@ -1096,32 +1094,33 @@ Editar nombre y tema Cambiar configuración Cambiar la configuración de notificaciones - La llamada está utilizando el auricular. Toca para cambiar al altavoz. + La llamada está utilizando el auricular. Toque para cambiar al altavoz. La llamada está utilizando el auricular. La llamada está utilizando auriculares con cable - La llamada está utilizando altavoz. Toca para cambiar a auricular. + La llamada está utilizando el altavoz. Toque para cambiar al auricular. La llamada está usando el altavoz. - La llamada está usando bluetooth. + La llamada está usando Bluetooth. Video desactivado. Toca para activar. Método de acceso - No se pudo agregar la reacción - Agregar reacción… - Agregar reacción - El cliente XMPP de tu contacto puede que no soporte llamadas de audio/video. + No se pudo añadir la reacción + Añadir reacción… + Añadir reacción + El cliente XMPP de su contacto puede que no admita llamadas de audio/vídeo. No se pudo modificar la llamada Burbujas de chat Color, Tamaño de fuente, Imágenes de perfil Burbujas de Chat Integración de llamadas - Ver imágenes de perfil + Mostrar avatares Mostrar imágenes de perfil para tus mensajes y conversaciones 1:1, aparte de las conversaciones en grupo. Las llamadas desde esta app interactúan con las llamadas telefónicas regulares, como ser finalizar una llamada cuando recibimos otra. Mensajes alineados a la izquierda Mostrar todos los mensajes, incluso los propios, sobre el margen izquierdo para una distribución uniforme del chat. Notificaciones personalizadas - ¿Habilitar los ajustes de notificaciones personalizadas (importancia, sonido, vibración) para esta conversación? + ¿Quiere activar la configuración de notificaciones personalizadas (importancia, sonido, vibración) para esta conversación? ¿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 + documento de Word + From 2667495dcdd2c5bba49c1a9520a4de0fdf67a82b Mon Sep 17 00:00:00 2001 From: Adolfo Jayme Barrientos Date: Thu, 13 Mar 2025 21:41:51 +0000 Subject: [PATCH 043/169] Translated using Weblate (Spanish) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/es/ --- src/quicksy/res/values-es/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/quicksy/res/values-es/strings.xml b/src/quicksy/res/values-es/strings.xml index f581a993adc2c0c6195447d72e389abb96c7d721..15108394297bcd76e8bd073e873103022d8184ef 100644 --- a/src/quicksy/res/values-es/strings.xml +++ b/src/quicksy/res/values-es/strings.xml @@ -1,12 +1,12 @@ - El tiempo que Quicksy silencia las notificaciones tras detectar actividad en otro de tus dispositivos + El tiempo que Quicksy silencia las notificaciones tras detectar actividad en otro dispositivo Al enviar informes de fallos, ayudará a desarrollar Quicksy aún más - Informar a tus contactos cuando usas Quicksy + Informar a sus contactos cuando usa Quicksy Para continuar recibiendo notificaciones incluso cuando la pantalla está apagada, debe agregar Quicksy a la lista de aplicaciones protegidas. Foto de perfil de Quicksy - Quicksy no está disponible en tu país. + Quicksy no está disponible en su país. No se ha podido verificar la identidad del servidor. Error de seguridad desconocido. Se ha superado el tiempo máximo de espera conectando al servidor. - \ No newline at end of file + From 00a800fc15ec8f3e51ac23270cd2e5b70db59b8d Mon Sep 17 00:00:00 2001 From: Adolfo Jayme Barrientos Date: Fri, 14 Mar 2025 11:20:33 +0000 Subject: [PATCH 044/169] Translated using Weblate (Spanish) Currently translated at 99.9% (1061 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index e437cfc0c32bcac12dcf9f19746a86e2480be716..878c959ef6b3f6d95efce98d40f9fafa75ae9bef 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -552,7 +552,7 @@ El registro falló. Prueba de nuevo más tarde Error en el registro: La contraseña es demasiado débil Elige a los participantes - Creando un chat de grupo… + Creando una conversación grupal… Invitar de nuevo Desactivar Corto @@ -703,7 +703,7 @@ Desactivar ahora Borrador: Cifrado OMEMO - OMEMO siempre será usado para conversaciones uno a uno y en conversaciones en grupo privadas. + OMEMO siempre se usará para conversaciones uno a uno y en conversaciones grupales privadas. OMEMO será usado por defecto para chats nuevos. OMEMO tendrá que ser activado explícitamente para los nuevos chats. Crear atajo @@ -836,9 +836,9 @@ No se ha podido descifrar la copia de respaldo. ¿Es la contraseña correcta? Respaldar & Restaurar Introduce dirección XMPP - Crear una conversación en grupo + Crear una conversación grupal Unirse a canal público - Crear una conversación en grupo privada + Crear una conversación grupal privada Crear un canal público Nombre del canal Dirección XMPP From 95641f805a01d8ede32561b45de36a3fb919d1a9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 21 Mar 2025 17:03:13 +0100 Subject: [PATCH 045/169] fix french translation --- src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-fr/strings.xml b/src/main/res/values-fr/strings.xml index cc0e711f1dafe51f85a427f9d63659e6ae467c99..05fbbf368536a14030530bb0daf28b35258886e6 100644 --- a/src/main/res/values-fr/strings.xml +++ b/src/main/res/values-fr/strings.xml @@ -95,7 +95,7 @@ Envoyer non-chiffré Échec du déchiffrement. Peut-être que vous n\'avez pas la bonne clé privée. OpenKeychain - OpenKeychain pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.

OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.

(Veuillez redémarrer %1$s après l'installation de l'application.)]]>
+ OpenKeychain pour chiffrer et déchiffrer les messages et pour gérer vos clés publiques.

OpenKeychain est sous licence GPLv3 et est disponible sur F-Droid et Google Play.

(Veuillez redémarrer %1$s après l\'installation de l\'application.)]]>
Redémarrer Installer Veuillez installer OpenKeychain From 46677a77e436ace6b36a038bbc31a246901b0055 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 24 Mar 2025 07:27:16 +0000 Subject: [PATCH 046/169] Translated using Weblate (Hebrew) Currently translated at 30.5% (324 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/he/ --- src/main/res/values-iw/strings.xml | 71 +++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index afee2558260cda97496d47df995e9b9b67520a43..416850bc55c870787c7bc720027f9b73ba6d84ee 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -273,4 +273,73 @@ מקוון ההודעה הועתקה הראה מיקום - \ No newline at end of file + נהל חשבון + פרטי צ\'אט קבוצתי + פרטי הערוץ + %1$s קרס + התנתק + הפרת מדיניות + + פסק זמן התחברות + + %d צ\'אט שלא נקרא + %d צ\'אטים שלא נקראו + %d צ\'אטים שלא נקראו + + שלח דיווחי קריסות + האם אתה רוצה למחוק את כל ההודעות בצ\'אט הזה?\n\nאזהרה: זה לא ישפיע על הודעות המאוחסנות במכשירים או שרתים אחרים. + תחום לא חוקי + שגיאת זרימה + האם ברצונך להסיר את הסימניה עבור %s? + האם תרצה להסיר את הסימניה עבור %s ולהעביר את הצ\'אט לארכיון? + הזמן איש קשר + הזמן + השימוש בחשבון XMPP שלך לשליחת באגים עוזר לפיתוח מתמשך של %1$s. + שתף עם… + ארכיון צ\'אט + הוסף לפנקס הכתובות + חסום משתתף + בטל חסימת משתמש + בחר אנשי קשר + שתף באמצעות חשבון + צא\'ט חדש + כינוי לא חוקי + "האם תרצה להסיר את %s מרשימת אנשי הקשר שלך? הצ\'אט עם איש הקשר הזה לא יוסר." + בחר איש קשר + חסום + לא ניתן להתחבר לחשבון + לא ניתן להתחבר למספר חשבונות + הקש כדי לנהל את החשבונות שלך + האם להוסיף את איש הקשר החסר הזה לרשימת אנשי הקשר שלך? + מתכונן לשלוח תמונה + מתכון לשלוח תמונות + משתף קבצים. נא להמתין… + מחק קובץ + מחק את הצ\'אט לאחר מכן + בחר מכשיר + שלח הודעה + שלח הודעה ל%s + שלח הודעה מוצפנת v\\OMEMO + כינוי חדש בשימוש + לא ניתן היה להצפין את ההודעה שלך כי אנשי הקשר שלך לא מכריזים על המפתחות הציבוריים שלהם.\n\nבקש מהם להגדיר את OpenPGP. + שלח הודעה מוצפנת + לא ניתן היה להצפין את ההודעה שלך כי איש הקשר שלך לא הכריז על המפתח הציבורי שלו.\n\nאנא בקש מאיש הקשר שלך להגדיר את OpenPGP. + מצורפים + התראות + רטט כשמגיעה הודעה חדשה + התראות שמע + התראת LED + נורית התראה מהבהבת כשמגיעה הודעה חדשה + רינגטון + מתקדם + על ידי שליחת דיווחי קריסות אתה עוזר לפיתוח + אפשר לאנשי הקשר שלך לדעת כאשר קיבלת וקראת את ההודעות שלהם + מניעת צילום מסך + לא ניתן להמיר את התמונה + האפליקציה שבה השתמשת כדי לשתף את הקובץ הזה לא סיפקה מספיק הרשאות. + צליל התראה עבור הודעות חדשות + OpenKeychain יצר שגיאה. + הסתר את תוכן האפליקציה במחליף האפליקציות וחסום צילומי מסך + OpenKeychain כדי להצפין ולפענח הודעות ולנהל את המפתחות הציבוריים שלך.

הוא מורשה תחת GPLv3+ וזמין ב-F-Droid וב-Google Play.

(אנא הפעל מחדש את %1$s לאחר מכן.)]]>
+ האם אתה בטוח שברצונך למחוק את הקובץ הזה?\n\nאזהרה: פעולה זו לא תמחק עותקים של קובץ זה המאוחסנים במכשירים או שרתים אחרים. + From f91d8c5819ea64a9d573739a76557a585de5e1ce Mon Sep 17 00:00:00 2001 From: Codeberg Translate Date: Mon, 24 Mar 2025 11:40:15 +0000 Subject: [PATCH 047/169] Update translation files Updated by "Remove blank strings" hook in Weblate. Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ --- src/main/res/values-iw/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 416850bc55c870787c7bc720027f9b73ba6d84ee..94e362a8cdb397f0091fb3c53ce392d35b7b581f 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -279,7 +279,6 @@ %1$s קרס התנתק הפרת מדיניות - פסק זמן התחברות %d צ\'אט שלא נקרא From 15e247d585cffdd2ff9bcad5781243242fe6f49c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 26 Mar 2025 12:38:10 +0100 Subject: [PATCH 048/169] refactor backup restore to use worker instead of service --- src/conversations/AndroidManifest.xml | 9 +- .../services/ImportBackupService.java | 503 ------------------ src/main/AndroidManifest.xml | 63 ++- .../siacs/conversations/entities/Account.java | 19 +- .../ui/ImportBackupActivity.java | 343 ++++++------ .../conversations/ui/RtpSessionActivity.java | 1 + .../ui/adapter/BackupFileAdapter.java | 66 ++- .../siacs/conversations/utils/BackupFile.java | 140 +++++ .../worker/ExportBackupWorker.java | 17 +- .../worker/ImportBackupWorker.java | 389 ++++++++++++++ .../res/layout/activity_import_backup.xml | 0 .../res/layout/dialog_enter_password.xml | 47 +- src/main/res/values/strings.xml | 6 +- 13 files changed, 867 insertions(+), 736 deletions(-) delete mode 100644 src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java rename src/{conversations => main}/java/eu/siacs/conversations/ui/ImportBackupActivity.java (50%) rename src/{conversations => main}/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java (75%) create mode 100644 src/main/java/eu/siacs/conversations/utils/BackupFile.java create mode 100644 src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java rename src/{conversations => main}/res/layout/activity_import_backup.xml (100%) rename src/{conversations => main}/res/layout/dialog_enter_password.xml (54%) diff --git a/src/conversations/AndroidManifest.xml b/src/conversations/AndroidManifest.xml index 5b111101e32d68a31b66cac0df703df02a0dd514..0f5834b49a61c1018d6c29feb82888d923933e7d 100644 --- a/src/conversations/AndroidManifest.xml +++ b/src/conversations/AndroidManifest.xml @@ -5,8 +5,8 @@ + android:launchMode="singleTask" + android:theme="@style/Theme.Conversations3" /> - diff --git a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java b/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java deleted file mode 100644 index c15208bfa1b8c0fc383dadbcce3d92ff95292aed..0000000000000000000000000000000000000000 --- a/src/conversations/java/eu/siacs/conversations/services/ImportBackupService.java +++ /dev/null @@ -1,503 +0,0 @@ -package eu.siacs.conversations.services; - -import static eu.siacs.conversations.utils.Compatibility.s; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Binder; -import android.os.IBinder; -import android.provider.OpenableColumns; -import android.util.Log; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; -import com.google.common.base.Charsets; -import com.google.common.base.Stopwatch; -import com.google.common.io.CountingInputStream; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.persistance.DatabaseBackend; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.ui.ManageAccountActivity; -import eu.siacs.conversations.utils.BackupFileHeader; -import eu.siacs.conversations.utils.SerialSingleThreadExecutor; -import eu.siacs.conversations.worker.ExportBackupWorker; -import eu.siacs.conversations.xmpp.Jid; -import java.io.BufferedReader; -import java.io.DataInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Pattern; -import java.util.zip.GZIPInputStream; -import java.util.zip.ZipException; -import javax.crypto.BadPaddingException; -import org.bouncycastle.crypto.engines.AESEngine; -import org.bouncycastle.crypto.io.CipherInputStream; -import org.bouncycastle.crypto.modes.AEADBlockCipher; -import org.bouncycastle.crypto.modes.GCMBlockCipher; -import org.bouncycastle.crypto.params.AEADParameters; -import org.bouncycastle.crypto.params.KeyParameter; - -public class ImportBackupService extends Service { - - private static final ExecutorService BACKUP_FILE_READER_EXECUTOR = - Executors.newSingleThreadExecutor(); - - private static final int NOTIFICATION_ID = 21; - private static final AtomicBoolean running = new AtomicBoolean(false); - private final ImportBackupServiceBinder binder = new ImportBackupServiceBinder(); - private final SerialSingleThreadExecutor executor = - new SerialSingleThreadExecutor(getClass().getSimpleName()); - private final Set mOnBackupProcessedListeners = - Collections.newSetFromMap(new WeakHashMap<>()); - private DatabaseBackend mDatabaseBackend; - private NotificationManager notificationManager; - - private static final Collection TABLE_ALLOW_LIST = - Arrays.asList( - Account.TABLENAME, - Conversation.TABLENAME, - Message.TABLENAME, - SQLiteAxolotlStore.PREKEY_TABLENAME, - SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, - SQLiteAxolotlStore.SESSION_TABLENAME, - SQLiteAxolotlStore.IDENTITIES_TABLENAME); - private static final Pattern COLUMN_PATTERN = Pattern.compile("^[a-zA-Z_]+$"); - - @Override - public void onCreate() { - mDatabaseBackend = DatabaseBackend.getInstance(getBaseContext()); - notificationManager = - (android.app.NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent == null) { - return START_NOT_STICKY; - } - final String password = intent.getStringExtra("password"); - final Uri data = intent.getData(); - final Uri uri; - if (data == null) { - final String file = intent.getStringExtra("file"); - uri = file == null ? null : Uri.fromFile(new File(file)); - } else { - uri = data; - } - - if (password == null || password.isEmpty() || uri == null) { - return START_NOT_STICKY; - } - if (running.compareAndSet(false, true)) { - executor.execute( - () -> { - startForegroundService(); - final boolean success = importBackup(uri, password); - stopForeground(true); - running.set(false); - if (success) { - notifySuccess(); - } - stopSelf(); - }); - } else { - Log.d(Config.LOGTAG, "backup already running"); - } - return START_NOT_STICKY; - } - - public boolean getLoadingState() { - return running.get(); - } - - public void loadBackupFiles(final OnBackupFilesLoaded onBackupFilesLoaded) { - executor.execute( - () -> { - final List accounts = mDatabaseBackend.getAccountJids(false); - final ArrayList backupFiles = new ArrayList<>(); - final Set apps = - new HashSet<>( - Arrays.asList( - "Conversations", - "Quicksy", - getString(R.string.app_name))); - final List directories = new ArrayList<>(); - for (final String app : apps) { - directories.add(FileBackend.getLegacyBackupDirectory(app)); - } - directories.add(FileBackend.getBackupDirectory(this)); - for (final File directory : directories) { - if (!directory.exists() || !directory.isDirectory()) { - Log.d( - Config.LOGTAG, - "directory not found: " + directory.getAbsolutePath()); - continue; - } - final File[] files = directory.listFiles(); - if (files == null) { - continue; - } - Log.d(Config.LOGTAG, "looking for backups in " + directory); - for (final File file : files) { - if (file.isFile() && file.getName().endsWith(".ceb")) { - try { - final BackupFile backupFile = BackupFile.read(file); - if (accounts.contains(backupFile.getHeader().getJid())) { - Log.d( - Config.LOGTAG, - "skipping backup for " - + backupFile.getHeader().getJid()); - } else { - backupFiles.add(backupFile); - } - } catch (final IOException - | IllegalArgumentException - | BackupFileHeader.OutdatedBackupFileVersion e) { - Log.d(Config.LOGTAG, "unable to read backup file ", e); - } - } - } - } - Collections.sort( - backupFiles, Comparator.comparing(a -> a.header.getJid().toString())); - onBackupFilesLoaded.onBackupFilesLoaded(backupFiles); - }); - } - - private void startForegroundService() { - startForeground(NOTIFICATION_ID, createImportBackupNotification(1, 0)); - } - - private void updateImportBackupNotification(final long total, final long current) { - final int max; - final int progress; - if (total == 0) { - max = 1; - progress = 0; - } else { - max = 100; - progress = (int) (current * 100 / total); - } - final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); - try { - notificationManager.notify( - NOTIFICATION_ID, createImportBackupNotification(max, progress)); - } catch (final RuntimeException e) { - Log.d(Config.LOGTAG, "unable to make notification", e); - } - } - - private Notification createImportBackupNotification(final int max, final int progress) { - NotificationCompat.Builder mBuilder = - new NotificationCompat.Builder(getBaseContext(), "backup"); - mBuilder.setContentTitle(getString(R.string.restoring_backup)) - .setSmallIcon(R.drawable.ic_unarchive_24dp) - .setProgress(max, progress, max == 1 && progress == 0); - return mBuilder.build(); - } - - private boolean importBackup(final Uri uri, final String password) { - Log.d(Config.LOGTAG, "importing backup from " + uri); - final Stopwatch stopwatch = Stopwatch.createStarted(); - try { - final SQLiteDatabase db = mDatabaseBackend.getWritableDatabase(); - final InputStream inputStream; - final String path = uri.getPath(); - final long fileSize; - if ("file".equals(uri.getScheme()) && path != null) { - final File file = new File(path); - inputStream = new FileInputStream(file); - fileSize = file.length(); - } else { - final Cursor returnCursor = getContentResolver().query(uri, null, null, null, null); - if (returnCursor == null) { - fileSize = 0; - } else { - returnCursor.moveToFirst(); - fileSize = - returnCursor.getLong( - returnCursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); - returnCursor.close(); - } - inputStream = getContentResolver().openInputStream(uri); - } - if (inputStream == null) { - synchronized (mOnBackupProcessedListeners) { - for (final OnBackupProcessed l : mOnBackupProcessedListeners) { - l.onBackupRestoreFailed(); - } - } - return false; - } - final CountingInputStream countingInputStream = new CountingInputStream(inputStream); - final DataInputStream dataInputStream = new DataInputStream(countingInputStream); - final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); - Log.d(Config.LOGTAG, backupFileHeader.toString()); - - if (mDatabaseBackend.getAccountJids(false).contains(backupFileHeader.getJid())) { - synchronized (mOnBackupProcessedListeners) { - for (OnBackupProcessed l : mOnBackupProcessedListeners) { - l.onAccountAlreadySetup(); - } - } - return false; - } - - final byte[] key = ExportBackupWorker.getKey(password, backupFileHeader.getSalt()); - - final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); - cipher.init( - false, - new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); - final CipherInputStream cipherInputStream = - new CipherInputStream(countingInputStream, cipher); - - final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); - final BufferedReader reader = - new BufferedReader(new InputStreamReader(gzipInputStream, Charsets.UTF_8)); - final JsonReader jsonReader = new JsonReader(reader); - if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) { - jsonReader.beginArray(); - } else { - throw new IllegalStateException("Backup file did not begin with array"); - } - db.beginTransaction(); - while (jsonReader.hasNext()) { - if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) { - importRow(db, jsonReader, backupFileHeader.getJid(), password); - } else if (jsonReader.peek() == JsonToken.END_ARRAY) { - jsonReader.endArray(); - continue; - } - updateImportBackupNotification(fileSize, countingInputStream.getCount()); - } - db.setTransactionSuccessful(); - db.endTransaction(); - final Jid jid = backupFileHeader.getJid(); - final Cursor countCursor = - db.rawQuery( - "select count(messages.uuid) from messages join conversations on" - + " conversations.uuid=messages.conversationUuid join accounts on" - + " conversations.accountUuid=accounts.uuid where" - + " accounts.username=? and accounts.server=?", - new String[] {jid.getLocal(), jid.getDomain().toString()}); - countCursor.moveToFirst(); - final int count = countCursor.getInt(0); - Log.d( - Config.LOGTAG, - String.format( - "restored %d messages in %s", count, stopwatch.stop().toString())); - countCursor.close(); - stopBackgroundService(); - synchronized (mOnBackupProcessedListeners) { - for (OnBackupProcessed l : mOnBackupProcessedListeners) { - l.onBackupRestored(); - } - } - return true; - } catch (final Exception e) { - final Throwable throwable = e.getCause(); - final boolean reasonWasCrypto = - throwable instanceof BadPaddingException || e instanceof ZipException; - synchronized (mOnBackupProcessedListeners) { - for (OnBackupProcessed l : mOnBackupProcessedListeners) { - if (reasonWasCrypto) { - l.onBackupDecryptionFailed(); - } else { - l.onBackupRestoreFailed(); - } - } - } - Log.d(Config.LOGTAG, "error restoring backup " + uri, e); - return false; - } - } - - private void importRow( - final SQLiteDatabase db, - final JsonReader jsonReader, - final Jid account, - final String passphrase) - throws IOException { - jsonReader.beginObject(); - final String firstParameter = jsonReader.nextName(); - if (!firstParameter.equals("table")) { - throw new IllegalStateException("Expected key 'table'"); - } - final String table = jsonReader.nextString(); - if (!TABLE_ALLOW_LIST.contains(table)) { - throw new IOException(String.format("%s is not recognized for import", table)); - } - final ContentValues contentValues = new ContentValues(); - final String secondParameter = jsonReader.nextName(); - if (!secondParameter.equals("values")) { - throw new IllegalStateException("Expected key 'values'"); - } - jsonReader.beginObject(); - while (jsonReader.peek() != JsonToken.END_OBJECT) { - final String name = jsonReader.nextName(); - if (COLUMN_PATTERN.matcher(name).matches()) { - if (jsonReader.peek() == JsonToken.NULL) { - jsonReader.nextNull(); - contentValues.putNull(name); - } else if (jsonReader.peek() == JsonToken.NUMBER) { - contentValues.put(name, jsonReader.nextLong()); - } else { - contentValues.put(name, jsonReader.nextString()); - } - } else { - throw new IOException(String.format("Unexpected column name %s", name)); - } - } - jsonReader.endObject(); - jsonReader.endObject(); - if (Account.TABLENAME.equals(table)) { - final Jid jid = - Jid.of( - contentValues.getAsString(Account.USERNAME), - contentValues.getAsString(Account.SERVER), - null); - final String password = contentValues.getAsString(Account.PASSWORD); - if (jid.equals(account) && passphrase.equals(password)) { - Log.d(Config.LOGTAG, "jid and password from backup header had matching row"); - } else { - throw new IOException("jid or password in table did not match backup"); - } - } - db.insert(table, null, contentValues); - } - - private void notifySuccess() { - NotificationCompat.Builder mBuilder = - new NotificationCompat.Builder(getBaseContext(), "backup"); - mBuilder.setContentTitle(getString(R.string.notification_restored_backup_title)) - .setContentText(getString(R.string.notification_restored_backup_subtitle)) - .setAutoCancel(true) - .setContentIntent( - PendingIntent.getActivity( - this, - 145, - new Intent(this, ManageAccountActivity.class), - s() - ? PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT - : PendingIntent.FLAG_UPDATE_CURRENT)) - .setSmallIcon(R.drawable.ic_unarchive_24dp); - notificationManager.notify(NOTIFICATION_ID, mBuilder.build()); - } - - private void stopBackgroundService() { - Intent intent = new Intent(this, XmppConnectionService.class); - stopService(intent); - } - - public void removeOnBackupProcessedListener(OnBackupProcessed listener) { - synchronized (mOnBackupProcessedListeners) { - mOnBackupProcessedListeners.remove(listener); - } - } - - public void addOnBackupProcessedListener(OnBackupProcessed listener) { - synchronized (mOnBackupProcessedListeners) { - mOnBackupProcessedListeners.add(listener); - } - } - - public static ListenableFuture read(final Context context, final Uri uri) { - return Futures.submit(() -> BackupFile.read(context, uri), BACKUP_FILE_READER_EXECUTOR); - } - - @Override - public IBinder onBind(Intent intent) { - return this.binder; - } - - public interface OnBackupFilesLoaded { - void onBackupFilesLoaded(List files); - } - - public interface OnBackupProcessed { - void onBackupRestored(); - - void onBackupDecryptionFailed(); - - void onBackupRestoreFailed(); - - void onAccountAlreadySetup(); - } - - public static class BackupFile { - private final Uri uri; - private final BackupFileHeader header; - - private BackupFile(Uri uri, BackupFileHeader header) { - this.uri = uri; - this.header = header; - } - - private static BackupFile read(File file) throws IOException { - final FileInputStream fileInputStream = new FileInputStream(file); - final DataInputStream dataInputStream = new DataInputStream(fileInputStream); - BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); - fileInputStream.close(); - return new BackupFile(Uri.fromFile(file), backupFileHeader); - } - - public static BackupFile read(final Context context, final Uri uri) throws IOException { - final InputStream inputStream = context.getContentResolver().openInputStream(uri); - if (inputStream == null) { - throw new FileNotFoundException(); - } - final DataInputStream dataInputStream = new DataInputStream(inputStream); - final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); - inputStream.close(); - return new BackupFile(uri, backupFileHeader); - } - - public BackupFileHeader getHeader() { - return header; - } - - public Uri getUri() { - return uri; - } - } - - public class ImportBackupServiceBinder extends Binder { - public ImportBackupService getService() { - return ImportBackupService.this; - } - } -} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index ddc93f285f4393df4950420a44525b281cf870cd..f765d5dfb58082ae7dc92c10e8b412d77eb92ee3 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -120,11 +120,6 @@ android:foregroundServiceType="dataSync" tools:node="merge" /> - - + android:parentActivityName=".ui.activity.SettingsActivity"> + android:value="eu.siacs.conversations.ui.activity.SettingsActivity" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index ef5e4152d6be0b700ea10b6d4acb743565cfb0e8..075ee301aa08380e072f67cf3b94dfdb02b5cf07 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -148,13 +148,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.password = password; this.options = options; this.rosterVersion = rosterVersion; - JSONObject tmp; - try { - tmp = new JSONObject(keys); - } catch (JSONException e) { - tmp = new JSONObject(); - } - this.keys = tmp; + this.keys = parseKeys(keys); this.avatar = avatar; this.displayName = displayName; this.hostname = hostname; @@ -167,6 +161,17 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable this.fastToken = fastToken; } + public static JSONObject parseKeys(final String keys) { + if (Strings.isNullOrEmpty(keys)) { + return new JSONObject(); + } + try { + return new JSONObject(keys); + } catch (final JSONException e) { + return new JSONObject(); + } + } + public static Account fromCursor(final Cursor cursor) { final Jid jid; try { diff --git a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java similarity index 50% rename from src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java rename to src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java index 331857e29fb2242060ee8a77d8afd4b29f3b9e50..3a81fcd0a4921c4d55f97e71e47c0df07c209d41 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -1,84 +1,100 @@ package eu.siacs.conversations.ui; import android.Manifest; -import android.content.ComponentName; -import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.IBinder; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.databinding.DataBindingUtil; - +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Transformations; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityImportBackupBinding; import eu.siacs.conversations.databinding.DialogEnterPasswordBinding; -import eu.siacs.conversations.services.ImportBackupService; +import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.ui.adapter.BackupFileAdapter; -import eu.siacs.conversations.ui.util.MainThreadExecutor; +import eu.siacs.conversations.utils.BackupFile; import eu.siacs.conversations.utils.BackupFileHeader; - +import eu.siacs.conversations.worker.ImportBackupWorker; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.UUID; public class ImportBackupActivity extends ActionBarActivity - implements ServiceConnection, - ImportBackupService.OnBackupFilesLoaded, - BackupFileAdapter.OnItemClickedListener, - ImportBackupService.OnBackupProcessed { + implements BackupFileAdapter.OnItemClickedListener { private ActivityImportBackupBinding binding; private BackupFileAdapter backupFileAdapter; - private ImportBackupService service; - private boolean mLoadingState = false; + private LiveData inProgressImport; + private UUID currentWorkRequest; + private final ActivityResultLauncher requestPermissions = registerForActivityResult( new ActivityResultContracts.RequestMultiplePermissions(), results -> { if (results.containsValue(Boolean.TRUE)) { - final var service = this.service; - if (service == null) { - return; - } - service.loadBackupFiles(this); + loadBackupFiles(); } }); + private final ActivityResultLauncher openBackup = + registerForActivityResult( + new ActivityResultContracts.GetContent(), + uri -> openBackupFileFromUri(uri, false)); + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_import_backup); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(binding.toolbar); - setLoadingState( - savedInstanceState != null - && savedInstanceState.getBoolean("loading_state", false)); + + final var workManager = WorkManager.getInstance(this); + final var imports = + workManager.getWorkInfosByTagLiveData(ImportBackupWorker.TAG_IMPORT_BACKUP); + this.inProgressImport = + Transformations.map( + imports, infos -> Iterables.any(infos, i -> !i.getState().isFinished())); + + this.inProgressImport.observe( + this, inProgress -> setLoadingState(Boolean.TRUE.equals(inProgress))); + + if (savedInstanceState != null) { + final var currentWorkRequest = savedInstanceState.getString("current-work-request"); + if (currentWorkRequest != null) { + this.currentWorkRequest = UUID.fromString(currentWorkRequest); + } + } + monitorWorkRequest(this.currentWorkRequest); + this.backupFileAdapter = new BackupFileAdapter(); this.binding.list.setAdapter(this.backupFileAdapter); this.backupFileAdapter.setOnItemClickedListener(this); @@ -88,31 +104,33 @@ public class ImportBackupActivity extends ActionBarActivity public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.import_backup, menu); final MenuItem openBackup = menu.findItem(R.id.action_open_backup_file); - openBackup.setVisible(!this.mLoadingState); + final var inProgress = + this.inProgressImport == null ? null : this.inProgressImport.getValue(); + openBackup.setVisible(!Boolean.TRUE.equals(inProgress)); return true; } @Override - public void onSaveInstanceState(Bundle bundle) { - bundle.putBoolean("loading_state", this.mLoadingState); + public void onSaveInstanceState(@NonNull final Bundle bundle) { + if (this.currentWorkRequest != null) { + bundle.putString("current-work-request", this.currentWorkRequest.toString()); + } super.onSaveInstanceState(bundle); } @Override public void onStart() { - super.onStart(); - bindService(new Intent(this, ImportBackupService.class), this, Context.BIND_AUTO_CREATE); - final Intent intent = getIntent(); - if (intent != null - && Intent.ACTION_VIEW.equals(intent.getAction()) - && !this.mLoadingState) { - Uri uri = intent.getData(); - if (uri != null) { - openBackupFileFromUri(uri, true); - return; - } + + final var intent = getIntent(); + final var action = intent == null ? null : intent.getAction(); + final var data = intent == null ? null : intent.getData(); + if (Intent.ACTION_VIEW.equals(action) && data != null) { + openBackupFileFromUri(data, true); + setIntent(new Intent(Intent.ACTION_MAIN)); + return; } + final List desiredPermission; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { desiredPermission = @@ -154,44 +172,50 @@ public class ImportBackupActivity extends ActionBarActivity @Override public void onStop() { super.onStop(); - if (this.service != null) { - this.service.removeOnBackupProcessedListener(this); - } - unbindService(this); } - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - ImportBackupService.ImportBackupServiceBinder binder = - (ImportBackupService.ImportBackupServiceBinder) service; - this.service = binder.getService(); - this.service.addOnBackupProcessedListener(this); - setLoadingState(this.service.getLoadingState()); - this.service.loadBackupFiles(this); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - this.service = null; - } + private void loadBackupFiles() { + final var future = BackupFile.listAsync(getApplicationContext()); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(List files) { + runOnUiThread(() -> backupFileAdapter.setFiles(files)); + } - @Override - public void onBackupFilesLoaded(final List files) { - runOnUiThread(() -> backupFileAdapter.setFiles(files)); + @Override + public void onFailure(@NonNull Throwable t) {} + }, + ContextCompat.getMainExecutor(getApplication())); } @Override - public void onClick(final ImportBackupService.BackupFile backupFile) { + public void onClick(final BackupFile backupFile) { showEnterPasswordDialog(backupFile, false); } private void openBackupFileFromUri(final Uri uri, final boolean finishOnCancel) { - final var backupFileFuture = ImportBackupService.read(this, uri); + final var backupFileFuture = BackupFile.readAsync(this, uri); Futures.addCallback( backupFileFuture, new FutureCallback<>() { @Override - public void onSuccess(final ImportBackupService.BackupFile backupFile) { + public void onSuccess(final BackupFile backupFile) { + if (QuickConversationsService.isQuicksy()) { + if (!backupFile + .getHeader() + .getJid() + .getDomain() + .equals(Config.QUICKSY_DOMAIN)) { + Snackbar.make( + binding.coordinator, + R.string.non_quicksy_backup, + Snackbar.LENGTH_LONG) + .show(); + return; + } + } showEnterPasswordDialog(backupFile, finishOnCancel); } @@ -201,7 +225,7 @@ public class ImportBackupActivity extends ActionBarActivity showBackupThrowable(throwable); } }, - MainThreadExecutor.getInstance()); + ContextCompat.getMainExecutor(getApplication())); } private void showBackupThrowable(final Throwable throwable) { @@ -216,6 +240,7 @@ public class ImportBackupActivity extends ActionBarActivity Snackbar.make(binding.coordinator, R.string.not_a_backup_file, Snackbar.LENGTH_LONG) .show(); } else if (throwable instanceof SecurityException e) { + Log.d(Config.LOGTAG, "not able to parse backup file", e); Snackbar.make( binding.coordinator, R.string.sharing_application_not_grant_permission, @@ -225,7 +250,7 @@ public class ImportBackupActivity extends ActionBarActivity } private void showEnterPasswordDialog( - final ImportBackupService.BackupFile backupFile, final boolean finishOnCancel) { + final BackupFile backupFile, final boolean finishOnCancel) { final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate( LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); @@ -247,133 +272,125 @@ public class ImportBackupActivity extends ActionBarActivity builder.setPositiveButton(R.string.restore, null); builder.setCancelable(false); final AlertDialog dialog = builder.create(); - dialog.setOnShowListener( - (d) -> { - dialog.getButton(DialogInterface.BUTTON_POSITIVE) - .setOnClickListener( - v -> { - final String password = - enterPasswordBinding - .accountPassword - .getEditableText() - .toString(); - if (password.isEmpty()) { - enterPasswordBinding.accountPasswordLayout.setError( - getString(R.string.please_enter_password)); - return; - } - final Intent intent = getIntent(backupFile, password); - setLoadingState(true); - ContextCompat.startForegroundService(this, intent); - d.dismiss(); - }); - }); + dialog.setOnShowListener((d) -> onDialogShow(backupFile, d, enterPasswordBinding)); dialog.show(); } - @NonNull - private Intent getIntent(ImportBackupService.BackupFile backupFile, String password) { - final Uri uri = backupFile.getUri(); - Intent intent = new Intent(this, ImportBackupService.class); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra("password", password); - if ("file".equals(uri.getScheme())) { - intent.putExtra("file", uri.getPath()); - } else { - intent.setData(uri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + private void onDialogShow( + final BackupFile backupFile, + final DialogInterface d, + final DialogEnterPasswordBinding enterPasswordBinding) { + if (d instanceof AlertDialog alertDialog) { + alertDialog + .getButton(DialogInterface.BUTTON_POSITIVE) + .setOnClickListener(v -> onRestoreClick(backupFile, d, enterPasswordBinding)); } - return intent; + } + + private void onRestoreClick( + final BackupFile backupFile, + final DialogInterface d, + final DialogEnterPasswordBinding enterPasswordBinding) { + final String password = enterPasswordBinding.accountPassword.getEditableText().toString(); + if (password.isEmpty()) { + enterPasswordBinding.accountPasswordLayout.setError( + getString(R.string.please_enter_password)); + return; + } + + importBackup(backupFile, password, enterPasswordBinding.includeKeys.isChecked()); + d.dismiss(); + } + + private void importBackup( + final BackupFile backupFile, final String password, final boolean includeOmemo) { + final OneTimeWorkRequest importBackupWorkRequest = + new OneTimeWorkRequest.Builder(ImportBackupWorker.class) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setInputData( + ImportBackupWorker.data( + password, backupFile.getUri(), includeOmemo)) + .addTag(ImportBackupWorker.TAG_IMPORT_BACKUP) + .build(); + + final var id = importBackupWorkRequest.getId(); + this.currentWorkRequest = id; + monitorWorkRequest(id); + + final var workManager = WorkManager.getInstance(this); + workManager.enqueue(importBackupWorkRequest); + } + + private void monitorWorkRequest(final UUID uuid) { + if (uuid == null) { + return; + } + Log.d(Config.LOGTAG, "monitorWorkRequest(" + uuid + ")"); + final var workInfoLiveData = WorkManager.getInstance(this).getWorkInfoByIdLiveData(uuid); + workInfoLiveData.observe( + this, + workInfo -> { + final var state = workInfo.getState(); + if (state.isFinished()) { + this.currentWorkRequest = null; + } + if (state == WorkInfo.State.FAILED) { + final var data = workInfo.getOutputData(); + final var reason = + ImportBackupWorker.Reason.valueOfOrGeneric( + data.getString("reason")); + switch (reason) { + case DECRYPTION_FAILED -> onBackupDecryptionFailed(); + case ACCOUNT_ALREADY_EXISTS -> onAccountAlreadySetup(); + default -> onBackupRestoreFailed(); + } + } else if (state == WorkInfo.State.SUCCEEDED) { + onBackupRestored(); + } + }); } private void setLoadingState(final boolean loadingState) { + Log.d(Config.LOGTAG, "setLoadingState(" + loadingState + ")"); binding.coordinator.setVisibility(loadingState ? View.GONE : View.VISIBLE); binding.inProgress.setVisibility(loadingState ? View.VISIBLE : View.GONE); setTitle(loadingState ? R.string.restoring_backup : R.string.restore_backup); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); configureActionBar(getSupportActionBar(), !loadingState); - this.mLoadingState = loadingState; invalidateOptionsMenu(); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent intent) { - super.onActivityResult(requestCode, resultCode, intent); - if (resultCode == RESULT_OK) { - if (requestCode == 0xbac) { - openBackupFileFromUri(intent.getData(), false); - } - } - } - - @Override - public void onAccountAlreadySetup() { - runOnUiThread( - () -> { - setLoadingState(false); - Snackbar.make( - binding.coordinator, - R.string.account_already_setup, - Snackbar.LENGTH_LONG) - .show(); - }); + private void onAccountAlreadySetup() { + Snackbar.make(binding.coordinator, R.string.account_already_setup, Snackbar.LENGTH_LONG) + .show(); } - @Override - public void onBackupRestored() { - runOnUiThread( - () -> { - Intent intent = new Intent(this, ConversationActivity.class); - intent.addFlags( - Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - finish(); - }); + private void onBackupRestored() { + final Intent intent = new Intent(this, ConversationActivity.class); + intent.addFlags( + Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); } - @Override - public void onBackupDecryptionFailed() { - runOnUiThread( - () -> { - setLoadingState(false); - Snackbar.make( - binding.coordinator, - R.string.unable_to_decrypt_backup, - Snackbar.LENGTH_LONG) - .show(); - }); + private void onBackupDecryptionFailed() { + Snackbar.make(binding.coordinator, R.string.unable_to_decrypt_backup, Snackbar.LENGTH_LONG) + .show(); } - @Override - public void onBackupRestoreFailed() { - runOnUiThread( - () -> { - setLoadingState(false); - Snackbar.make( - binding.coordinator, - R.string.unable_to_restore_backup, - Snackbar.LENGTH_LONG) - .show(); - }); + private void onBackupRestoreFailed() { + Snackbar.make(binding.coordinator, R.string.unable_to_restore_backup, Snackbar.LENGTH_LONG) + .show(); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_open_backup_file) { - openBackupFile(); + this.openBackup.launch("*/*"); return true; } return super.onOptionsItemSelected(item); } - - private void openBackupFile() { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); - intent.addCategory(Intent.CATEGORY_OPENABLE); - startActivityForResult( - Intent.createChooser(intent, getString(R.string.open_backup)), 0xbac); - } } diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 8435a4da1804df5887555be80a7d6b310984f077..d88a77b4cf45a63eb7abc8521c7e4fde7ebd3651 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -1191,6 +1191,7 @@ public class RtpSessionActivity extends XmppActivity } }, MainThreadExecutor.getInstance()); + // TODO ^ replace with ContextCompat.getMainExecutor(getApplication()) } private void enableVideo(final View view) { diff --git a/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java similarity index 75% rename from src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java rename to src/main/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java index 9f32352eed913b313667e48ed21f968e92e4c094..7ffde3cd3e6280fb433ec74c1fed240aaf76a4a4 100644 --- a/src/conversations/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/BackupFileAdapter.java @@ -11,49 +11,62 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; - import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.RecyclerView; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.RejectedExecutionException; - import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ItemAccountBinding; import eu.siacs.conversations.services.AvatarService; -import eu.siacs.conversations.services.ImportBackupService; +import eu.siacs.conversations.utils.BackupFile; import eu.siacs.conversations.utils.BackupFileHeader; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; -public class BackupFileAdapter extends RecyclerView.Adapter { +public class BackupFileAdapter + extends RecyclerView.Adapter { private OnItemClickedListener listener; - private final List files = new ArrayList<>(); - + private final List files = new ArrayList<>(); @NonNull @Override public BackupFileViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { - return new BackupFileViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.item_account, viewGroup, false)); + return new BackupFileViewHolder( + DataBindingUtil.inflate( + LayoutInflater.from(viewGroup.getContext()), + R.layout.item_account, + viewGroup, + false)); } @Override public void onBindViewHolder(@NonNull BackupFileViewHolder backupFileViewHolder, int position) { - final ImportBackupService.BackupFile backupFile = files.get(position); + final BackupFile backupFile = files.get(position); final BackupFileHeader header = backupFile.getHeader(); backupFileViewHolder.binding.accountJid.setText(header.getJid().asBareJid().toString()); - backupFileViewHolder.binding.accountStatus.setText(String.format("%s · %s",header.getApp(), DateUtils.formatDateTime(backupFileViewHolder.binding.getRoot().getContext(), header.getTimestamp(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR))); + backupFileViewHolder.binding.accountStatus.setText( + String.format( + "%s · %s", + header.getApp(), + DateUtils.formatDateTime( + backupFileViewHolder.binding.getRoot().getContext(), + header.getTimestamp(), + DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR))); backupFileViewHolder.binding.tglAccountStatus.setVisibility(View.GONE); - backupFileViewHolder.binding.getRoot().setOnClickListener(v -> { - if (listener != null) { - listener.onClick(backupFile); - } - }); + backupFileViewHolder + .binding + .getRoot() + .setOnClickListener( + v -> { + if (listener != null) { + listener.onClick(backupFile); + } + }); loadAvatar(header.getJid(), backupFileViewHolder.binding.accountImage); } @@ -62,7 +75,7 @@ public class BackupFileAdapter extends RecyclerView.Adapter files) { + public void setFiles(List files) { this.files.clear(); this.files.addAll(files); notifyDataSetChanged(); @@ -79,22 +92,21 @@ public class BackupFileAdapter extends RecyclerView.Adapter { private final WeakReference imageViewReference; - private Jid jid = null; + private Jid jid = null; private final int size; BitmapWorkerTask(final ImageView imageView) { imageViewReference = new WeakReference<>(imageView); DisplayMetrics metrics = imageView.getContext().getResources().getDisplayMetrics(); - this.size = ((int) (48 * metrics.density)); + this.size = ((int) (48 * metrics.density)); } @Override @@ -120,7 +132,8 @@ public class BackupFileAdapter extends RecyclerView.Adapter { + + private static final ExecutorService BACKUP_FILE_READER_EXECUTOR = + Executors.newSingleThreadExecutor(); + + private final Uri uri; + private final BackupFileHeader header; + + private BackupFile(Uri uri, BackupFileHeader header) { + this.uri = uri; + this.header = header; + } + + public static ListenableFuture readAsync(final Context context, final Uri uri) { + return Futures.submit(() -> read(context, uri), BACKUP_FILE_READER_EXECUTOR); + } + + private static BackupFile read(final File file) throws IOException { + final FileInputStream fileInputStream = new FileInputStream(file); + final DataInputStream dataInputStream = new DataInputStream(fileInputStream); + BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + fileInputStream.close(); + return new BackupFile(Uri.fromFile(file), backupFileHeader); + } + + public static BackupFile read(final Context context, final Uri uri) throws IOException { + final InputStream inputStream = context.getContentResolver().openInputStream(uri); + if (inputStream == null) { + throw new FileNotFoundException(); + } + final DataInputStream dataInputStream = new DataInputStream(inputStream); + final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + inputStream.close(); + return new BackupFile(uri, backupFileHeader); + } + + public BackupFileHeader getHeader() { + return header; + } + + public Uri getUri() { + return uri; + } + + public static ListenableFuture> listAsync(final Context context) { + return Futures.submit(() -> list(context), BACKUP_FILE_READER_EXECUTOR); + } + + private static List list(final Context context) { + final var database = DatabaseBackend.getInstance(context); + final List accounts = database.getAccountJids(false); + final var backupFiles = new ImmutableList.Builder(); + final var apps = + ImmutableSet.of("Conversations", "Quicksy", context.getString(R.string.app_name)); + final List directories = new ArrayList<>(); + for (final String app : apps) { + directories.add(FileBackend.getLegacyBackupDirectory(app)); + } + directories.add(FileBackend.getBackupDirectory(context)); + for (final File directory : directories) { + if (!directory.exists() || !directory.isDirectory()) { + Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); + continue; + } + final File[] files = directory.listFiles(); + if (files == null) { + continue; + } + Log.d(Config.LOGTAG, "looking for backups in " + directory); + for (final File file : files) { + if (file.isFile() && file.getName().endsWith(".ceb")) { + try { + final BackupFile backupFile = BackupFile.read(file); + if (accounts.contains(backupFile.getHeader().getJid())) { + Log.d( + Config.LOGTAG, + "skipping backup for " + backupFile.getHeader().getJid()); + } else { + backupFiles.add(backupFile); + } + } catch (final IOException + | IllegalArgumentException + | BackupFileHeader.OutdatedBackupFileVersion e) { + Log.d(Config.LOGTAG, "unable to read backup file ", e); + } + } + } + } + final var list = backupFiles.build(); + if (QuickConversationsService.isQuicksy()) { + return Ordering.natural() + .immutableSortedCopy( + Collections2.filter( + list, + b -> + b.header + .getJid() + .getDomain() + .equals(Config.QUICKSY_DOMAIN))); + } + return Ordering.natural().immutableSortedCopy(backupFiles.build()); + } + + @Override + public int compareTo(final BackupFile o) { + return ComparisonChain.start() + .compare(header.getJid(), o.header.getJid()) + .compare(header.getTimestamp(), o.header.getTimestamp()) + .result(); + } +} diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java index d848ca54ec2d8a4c1282160d251ad46e0bf67c14..c5178e0adfa91c251b8afa028711964fd1ba5891 100644 --- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java +++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java @@ -31,6 +31,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.QuickConversationsService; import eu.siacs.conversations.utils.BackupFileHeader; import eu.siacs.conversations.utils.Compatibility; import java.io.DataOutputStream; @@ -65,9 +66,9 @@ public class ExportBackupWorker extends Worker { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm", Locale.US); - public static final String KEYTYPE = "AES"; - public static final String CIPHERMODE = "AES/GCM/NoPadding"; - public static final String PROVIDER = "BC"; + private static final String KEY_TYPE = "AES"; + private static final String CIPHER_MODE = "AES/GCM/NoPadding"; + private static final String PROVIDER = "BC"; public static final String MIME_TYPE = "application/vnd.conversations.backup"; @@ -240,10 +241,10 @@ public class ExportBackupWorker extends Worker { final Cipher cipher = Compatibility.twentyEight() - ? Cipher.getInstance(CIPHERMODE) - : Cipher.getInstance(CIPHERMODE, PROVIDER); + ? Cipher.getInstance(CIPHER_MODE) + : Cipher.getInstance(CIPHER_MODE, PROVIDER); final byte[] key = getKey(password, salt); - SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE); + SecretKeySpec keySpec = new SecretKeySpec(key, KEY_TYPE); IvParameterSpec ivSpec = new IvParameterSpec(IV); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); @@ -326,7 +327,9 @@ public class ExportBackupWorker extends Worker { } else if (Account.OPTIONS.equals(accountCursor.getColumnName(i)) && value.matches("\\d+")) { int intValue = Integer.parseInt(value); - intValue |= 1 << Account.OPTION_DISABLED; + if (QuickConversationsService.isConversations()) { + intValue |= 1 << Account.OPTION_DISABLED; + } writer.value(intValue); } else { writer.value(value); diff --git a/src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..556748a72362b2b04abe6c98f4bde5ad9035b6c9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/worker/ImportBackupWorker.java @@ -0,0 +1,389 @@ +package eu.siacs.conversations.worker; + +import static eu.siacs.conversations.utils.Compatibility.s; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.provider.OpenableColumns; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.core.app.NotificationCompat; +import androidx.work.Data; +import androidx.work.ForegroundInfo; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import com.google.common.base.Stopwatch; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.io.CountingInputStream; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.persistance.DatabaseBackend; +import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.AccountUtils; +import eu.siacs.conversations.utils.BackupFileHeader; +import eu.siacs.conversations.xmpp.Jid; +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.zip.GZIPInputStream; +import java.util.zip.ZipException; +import javax.crypto.BadPaddingException; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.json.JSONException; +import org.json.JSONObject; + +public class ImportBackupWorker extends Worker { + + public static final String TAG_IMPORT_BACKUP = "tag-import-backup"; + + private static final String DATA_KEY_PASSWORD = "password"; + private static final String DATA_KEY_URI = "uri"; + private static final String DATA_KEY_INCLUDE_OMEMO = "omemo"; + + private static final Collection OMEMO_TABLE_LIST = + Arrays.asList( + SQLiteAxolotlStore.PREKEY_TABLENAME, + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + SQLiteAxolotlStore.SESSION_TABLENAME, + SQLiteAxolotlStore.IDENTITIES_TABLENAME); + + private static final List TABLE_ALLOW_LIST = + new ImmutableList.Builder() + .add(Account.TABLENAME, Conversation.TABLENAME, Message.TABLENAME) + .addAll(OMEMO_TABLE_LIST) + .build(); + + private static final Pattern COLUMN_PATTERN = Pattern.compile("^[a-zA-Z_]+$"); + + private static final int NOTIFICATION_ID = 21; + + private final String password; + private final Uri uri; + private final boolean includeOmemo; + + public ImportBackupWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + final var inputData = workerParams.getInputData(); + this.password = inputData.getString(DATA_KEY_PASSWORD); + this.uri = Uri.parse(inputData.getString(DATA_KEY_URI)); + this.includeOmemo = inputData.getBoolean(DATA_KEY_INCLUDE_OMEMO, true); + } + + @NonNull + @Override + public Result doWork() { + setForegroundAsync( + new ForegroundInfo(NOTIFICATION_ID, createImportBackupNotification(1, 0))); + final Result result; + try { + result = importBackup(this.uri, this.password); + } catch (final FileNotFoundException e) { + return failure(Reason.FILE_NOT_FOUND); + } catch (final Exception e) { + Log.d(Config.LOGTAG, "error restoring backup " + uri, e); + final Throwable throwable = e.getCause(); + if (throwable instanceof BadPaddingException || e instanceof ZipException) { + return failure(Reason.DECRYPTION_FAILED); + } else { + return failure(Reason.GENERIC); + } + } finally { + getApplicationContext() + .getSystemService(NotificationManager.class) + .cancel(NOTIFICATION_ID); + } + + return result; + } + + private Result importBackup(final Uri uri, final String password) + throws IOException, InvalidKeySpecException { + final var context = getApplicationContext(); + final var database = DatabaseBackend.getInstance(context); + Log.d(Config.LOGTAG, "importing backup from " + uri); + final Stopwatch stopwatch = Stopwatch.createStarted(); + final SQLiteDatabase db = database.getWritableDatabase(); + final InputStream inputStream; + final String path = uri.getPath(); + final long fileSize; + if ("file".equals(uri.getScheme()) && path != null) { + final File file = new File(path); + inputStream = new FileInputStream(file); + fileSize = file.length(); + } else { + final Cursor returnCursor = + context.getContentResolver().query(uri, null, null, null, null); + if (returnCursor == null) { + fileSize = 0; + } else { + returnCursor.moveToFirst(); + fileSize = + returnCursor.getLong( + returnCursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); + returnCursor.close(); + } + inputStream = context.getContentResolver().openInputStream(uri); + } + if (inputStream == null) { + return failure(Reason.FILE_NOT_FOUND); + } + final CountingInputStream countingInputStream = new CountingInputStream(inputStream); + final DataInputStream dataInputStream = new DataInputStream(countingInputStream); + final BackupFileHeader backupFileHeader = BackupFileHeader.read(dataInputStream); + Log.d(Config.LOGTAG, backupFileHeader.toString()); + + final var accounts = database.getAccountJids(false); + + if (QuickConversationsService.isQuicksy() && !accounts.isEmpty()) { + return failure(Reason.ACCOUNT_ALREADY_EXISTS); + } + + if (accounts.contains(backupFileHeader.getJid())) { + return failure(Reason.ACCOUNT_ALREADY_EXISTS); + } + + final byte[] key = ExportBackupWorker.getKey(password, backupFileHeader.getSalt()); + + final AEADBlockCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance()); + cipher.init( + false, new AEADParameters(new KeyParameter(key), 128, backupFileHeader.getIv())); + final CipherInputStream cipherInputStream = + new CipherInputStream(countingInputStream, cipher); + + final GZIPInputStream gzipInputStream = new GZIPInputStream(cipherInputStream); + final BufferedReader reader = + new BufferedReader(new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8)); + final JsonReader jsonReader = new JsonReader(reader); + if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) { + jsonReader.beginArray(); + } else { + throw new IllegalStateException("Backup file did not begin with array"); + } + db.beginTransaction(); + while (jsonReader.hasNext()) { + if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) { + importRow(db, jsonReader, backupFileHeader.getJid(), password); + } else if (jsonReader.peek() == JsonToken.END_ARRAY) { + jsonReader.endArray(); + continue; + } + updateImportBackupNotification(fileSize, countingInputStream.getCount()); + } + db.setTransactionSuccessful(); + db.endTransaction(); + final Jid jid = backupFileHeader.getJid(); + final Cursor countCursor = + db.rawQuery( + "select count(messages.uuid) from messages join conversations on" + + " conversations.uuid=messages.conversationUuid join accounts on" + + " conversations.accountUuid=accounts.uuid where" + + " accounts.username=? and accounts.server=?", + new String[] {jid.getLocal(), jid.getDomain().toString()}); + countCursor.moveToFirst(); + final int count = countCursor.getInt(0); + Log.d(Config.LOGTAG, String.format("restored %d messages in %s", count, stopwatch.stop())); + countCursor.close(); + stopBackgroundService(); + notifySuccess(); + return Result.success(); + } + + private void importRow( + final SQLiteDatabase db, + final JsonReader jsonReader, + final Jid account, + final String passphrase) + throws IOException { + jsonReader.beginObject(); + final String firstParameter = jsonReader.nextName(); + if (!firstParameter.equals("table")) { + throw new IllegalStateException("Expected key 'table'"); + } + final String table = jsonReader.nextString(); + if (!TABLE_ALLOW_LIST.contains(table)) { + throw new IOException(String.format("%s is not recognized for import", table)); + } + final ContentValues contentValues = new ContentValues(); + final String secondParameter = jsonReader.nextName(); + if (!secondParameter.equals("values")) { + throw new IllegalStateException("Expected key 'values'"); + } + jsonReader.beginObject(); + while (jsonReader.peek() != JsonToken.END_OBJECT) { + final String name = jsonReader.nextName(); + if (COLUMN_PATTERN.matcher(name).matches()) { + if (jsonReader.peek() == JsonToken.NULL) { + jsonReader.nextNull(); + contentValues.putNull(name); + } else if (jsonReader.peek() == JsonToken.NUMBER) { + contentValues.put(name, jsonReader.nextLong()); + } else { + contentValues.put(name, jsonReader.nextString()); + } + } else { + throw new IOException(String.format("Unexpected column name %s", name)); + } + } + jsonReader.endObject(); + jsonReader.endObject(); + if (Account.TABLENAME.equals(table)) { + final Jid jid = + Jid.of( + contentValues.getAsString(Account.USERNAME), + contentValues.getAsString(Account.SERVER), + null); + final String password = contentValues.getAsString(Account.PASSWORD); + if (QuickConversationsService.isQuicksy()) { + if (!jid.getDomain().equals(Config.QUICKSY_DOMAIN)) { + throw new IOException("Trying to restore non Quicksy account on Quicksy"); + } + } + if (jid.equals(account) && passphrase.equals(password)) { + Log.d(Config.LOGTAG, "jid and password from backup header had matching row"); + } else { + throw new IOException("jid or password in table did not match backup"); + } + final var keys = Account.parseKeys(contentValues.getAsString(Account.KEYS)); + final var deviceId = keys.optString(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID); + final var importReadyKeys = new JSONObject(); + if (!Strings.isNullOrEmpty(deviceId) && this.includeOmemo) { + try { + importReadyKeys.put(SQLiteAxolotlStore.JSONKEY_REGISTRATION_ID, deviceId); + } catch (final JSONException e) { + Log.e(Config.LOGTAG, "error writing omemo registration id", e); + } + } + contentValues.put(Account.KEYS, importReadyKeys.toString()); + } + if (this.includeOmemo) { + db.insert(table, null, contentValues); + } else { + if (OMEMO_TABLE_LIST.contains(table)) { + if (SQLiteAxolotlStore.IDENTITIES_TABLENAME.equals(table) + && contentValues.getAsInteger(SQLiteAxolotlStore.OWN) == 0) { + db.insert(table, null, contentValues); + } else { + Log.d(Config.LOGTAG, "skipping over omemo key material in table " + table); + } + } else { + db.insert(table, null, contentValues); + } + } + } + + private void stopBackgroundService() { + final var intent = new Intent(getApplicationContext(), XmppConnectionService.class); + getApplicationContext().stopService(intent); + } + + private void updateImportBackupNotification(final long total, final long current) { + final int max; + final int progress; + if (total == 0) { + max = 1; + progress = 0; + } else { + max = 100; + progress = (int) (current * 100 / total); + } + getApplicationContext() + .getSystemService(NotificationManager.class) + .notify(NOTIFICATION_ID, createImportBackupNotification(max, progress)); + } + + private Notification createImportBackupNotification(final int max, final int progress) { + final var context = getApplicationContext(); + final var builder = new NotificationCompat.Builder(getApplicationContext(), "backup"); + builder.setContentTitle(context.getString(R.string.restoring_backup)) + .setSmallIcon(R.drawable.ic_unarchive_24dp) + .setProgress(max, progress, max == 1 && progress == 0); + return builder.build(); + } + + private void notifySuccess() { + final var context = getApplicationContext(); + final var builder = new NotificationCompat.Builder(context, "backup"); + builder.setContentTitle(context.getString(R.string.notification_restored_backup_title)) + .setContentText(context.getString(R.string.notification_restored_backup_subtitle)) + .setAutoCancel(true) + .setSmallIcon(R.drawable.ic_unarchive_24dp); + if (QuickConversationsService.isConversations() + && AccountUtils.MANAGE_ACCOUNT_ACTIVITY != null) { + builder.setContentText( + context.getString(R.string.notification_restored_backup_subtitle)); + builder.setContentIntent( + PendingIntent.getActivity( + context, + 145, + new Intent(context, AccountUtils.MANAGE_ACCOUNT_ACTIVITY), + s() + ? PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT + : PendingIntent.FLAG_UPDATE_CURRENT)); + } + getApplicationContext() + .getSystemService(NotificationManager.class) + .notify(NOTIFICATION_ID + 2, builder.build()); + } + + public static Data data(final String password, final Uri uri, final boolean includeOmemo) { + return new Data.Builder() + .putString(DATA_KEY_PASSWORD, password) + .putString(DATA_KEY_URI, uri.toString()) + .putBoolean(DATA_KEY_INCLUDE_OMEMO, includeOmemo) + .build(); + } + + private static Result failure(final Reason reason) { + return Result.failure(new Data.Builder().putString("reason", reason.toString()).build()); + } + + public enum Reason { + ACCOUNT_ALREADY_EXISTS, + DECRYPTION_FAILED, + FILE_NOT_FOUND, + GENERIC; + + public static Reason valueOfOrGeneric(final String value) { + if (Strings.isNullOrEmpty(value)) { + return GENERIC; + } + try { + return valueOf(value); + } catch (final IllegalArgumentException e) { + return GENERIC; + } + } + } +} diff --git a/src/conversations/res/layout/activity_import_backup.xml b/src/main/res/layout/activity_import_backup.xml similarity index 100% rename from src/conversations/res/layout/activity_import_backup.xml rename to src/main/res/layout/activity_import_backup.xml diff --git a/src/conversations/res/layout/dialog_enter_password.xml b/src/main/res/layout/dialog_enter_password.xml similarity index 54% rename from src/conversations/res/layout/dialog_enter_password.xml rename to src/main/res/layout/dialog_enter_password.xml index 623168aa4f40a3c0994a995677d2a0f2f00f26d5..220ef0e6f808cab35d4e945376e6a3eab2ba01c2 100644 --- a/src/conversations/res/layout/dialog_enter_password.xml +++ b/src/main/res/layout/dialog_enter_password.xml @@ -19,25 +19,40 @@ android:text="@string/enter_password_to_restore" android:textAppearance="?textAppearanceBodyMedium" /> - + + + + + + + + + - + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6bf79ca139943c41aa2d439f1203b19e73bdcccd..a7df4b226bd6e4a304b2a2a3d49b77034692c088 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -848,8 +848,9 @@ Restore backup Restore Enter your password for the account %s to restore the backup. - Do not use the restore backup feature in an attempt to clone (run simultaneously) an installation. Restoring a backup is only meant for migrations or in case you’ve lost the original device. - Do not attempt to restore backups that you have not created yourself! + Restore OMEMO keys + Do not restore OMEMO keys in an attempt to clone (run simultaneously) an installation. Restoring OMEMO keys is only meant for migrations or in case you’ve lost the original device. + Only restore backups you’ve personally created. Could not restore backup. Could not decrypt backup. Is the password correct? Backup & Restore @@ -898,6 +899,7 @@ Open backup The file you selected is not a Conversations backup file You are trying to import an outdated backup file format + Quicksy can only restore backups for quicksy.im accounts This account has already been setup Please enter the password for this account Could not perform this action From b818fa516b1c963082ad91893bb25ca17589f452 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 26 Mar 2025 13:38:01 +0100 Subject: [PATCH 049/169] Quicksy: add restore backup menu item to 'enter phone number' activity --- .../ui/EnterPhoneNumberActivity.java | 214 ++++++++++++------ .../conversations/ui/VerifyActivity.java | 202 ++++++++++------- .../res/menu/verify_phone_number_menu.xml | 8 + 3 files changed, 274 insertions(+), 150 deletions(-) create mode 100644 src/quicksy/res/menu/verify_phone_number_menu.xml diff --git a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java index a1f55e2b4d77de17d01fbd6be2bf6a33d9dd4306..21753de3850b1237b7aa894fc8bb99a4d915c8c1 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java @@ -1,10 +1,10 @@ package eu.siacs.conversations.ui; +import static eu.siacs.conversations.utils.PermissionUtils.allGranted; + +import android.Manifest; import android.app.AlertDialog; import android.content.Intent; - -import androidx.annotation.NonNull; -import androidx.databinding.DataBindingUtil; import android.os.Bundle; import android.text.Editable; import android.text.Html; @@ -12,11 +12,13 @@ import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.widget.EditText; - -import java.util.concurrent.atomic.AtomicBoolean; - +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityEnterNumberBinding; @@ -30,57 +32,73 @@ import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import io.michaelrocks.libphonenumber.android.NumberParseException; import io.michaelrocks.libphonenumber.android.PhoneNumberUtil; import io.michaelrocks.libphonenumber.android.Phonenumber; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; -public class EnterPhoneNumberActivity extends XmppActivity implements QuickConversationsService.OnVerificationRequested { +public class EnterPhoneNumberActivity extends XmppActivity + implements QuickConversationsService.OnVerificationRequested { private static final int REQUEST_CHOOSE_COUNTRY = 0x1234; + private static final int REQUEST_IMPORT_BACKUP = 0x63fb; private ActivityEnterNumberBinding binding; private final AtomicBoolean redirectInProgress = new AtomicBoolean(false); private String region = null; - private final TextWatcher countryCodeTextWatcher = new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable editable) { - final String text = editable.toString(); - try { - final int oldCode = region != null ? PhoneNumberUtilWrapper.getInstance(EnterPhoneNumberActivity.this).getCountryCodeForRegion(region) : 0; - final int code = Integer.parseInt(text); - if (oldCode != code) { - region = PhoneNumberUtilWrapper.getInstance(EnterPhoneNumberActivity.this).getRegionCodeForCountryCode(code); + private final TextWatcher countryCodeTextWatcher = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable editable) { + final String text = editable.toString(); + try { + final int oldCode = + region != null + ? PhoneNumberUtilWrapper.getInstance( + EnterPhoneNumberActivity.this) + .getCountryCodeForRegion(region) + : 0; + final int code = Integer.parseInt(text); + if (oldCode != code) { + region = + PhoneNumberUtilWrapper.getInstance( + EnterPhoneNumberActivity.this) + .getRegionCodeForCountryCode(code); + } + if ("ZZ".equals(region)) { + binding.country.setText( + TextUtils.isEmpty(text) + ? R.string.choose_a_country + : R.string.invalid_country_code); + } else { + binding.number.requestFocus(); + binding.country.setText( + PhoneNumberUtilWrapper.getCountryForCode(region)); + } + } catch (NumberFormatException e) { + binding.country.setText( + TextUtils.isEmpty(text) + ? R.string.choose_a_country + : R.string.invalid_country_code); + } } - if ("ZZ".equals(region)) { - binding.country.setText(TextUtils.isEmpty(text) ? R.string.choose_a_country : R.string.invalid_country_code); - } else { - binding.number.requestFocus(); - binding.country.setText(PhoneNumberUtilWrapper.getCountryForCode(region)); - } - } catch (NumberFormatException e) { - binding.country.setText(TextUtils.isEmpty(text) ? R.string.choose_a_country : R.string.invalid_country_code); - } - } - }; + }; private boolean requestingVerification = false; @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override public void onBackendConnected() { - xmppConnectionService.getQuickConversationsService().addOnVerificationRequestedListener(this); + xmppConnectionService + .getQuickConversationsService() + .addOnVerificationRequestedListener(this); final Account account = AccountUtils.getFirst(xmppConnectionService); if (account != null) { runOnUiThread(this::performRedirectToVerificationActivity); @@ -92,7 +110,9 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve super.onCreate(savedInstanceState); String region = savedInstanceState != null ? savedInstanceState.getString("region") : null; - boolean requestingVerification = savedInstanceState != null && savedInstanceState.getBoolean("requesting_verification", false); + boolean requestingVerification = + savedInstanceState != null + && savedInstanceState.getBoolean("requesting_verification", false); if (region != null) { this.region = region; } else { @@ -100,31 +120,73 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve } this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_number); - this.binding.countryCode.setCompoundDrawables(new TextDrawable(this.binding.countryCode, "+"), null, null, null); + this.binding.countryCode.setCompoundDrawables( + new TextDrawable(this.binding.countryCode, "+"), null, null, null); this.binding.country.setOnClickListener(this::onSelectCountryClick); this.binding.next.setOnClickListener(this::onNextClick); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(this.binding.toolbar); this.binding.countryCode.addTextChangedListener(this.countryCodeTextWatcher); - this.binding.countryCode.setText(String.valueOf(PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(this.region))); - this.binding.number.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() != KeyEvent.ACTION_DOWN) { - return false; + this.binding.countryCode.setText( + String.valueOf( + PhoneNumberUtilWrapper.getInstance(this) + .getCountryCodeForRegion(this.region))); + this.binding.number.setOnKeyListener( + (v, keyCode, event) -> { + if (event.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + final EditText editText = (EditText) v; + final boolean cursorAtZero = + editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0; + if (keyCode == KeyEvent.KEYCODE_DEL + && (cursorAtZero || editText.getText().length() == 0)) { + final Editable countryCode = this.binding.countryCode.getText(); + if (countryCode.length() > 0) { + countryCode.delete(countryCode.length() - 1, countryCode.length()); + this.binding.countryCode.setSelection(countryCode.length()); + } + this.binding.countryCode.requestFocus(); + return true; + } + return false; + }); + setRequestingVerificationState(requestingVerification); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.verify_phone_number_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + if (item.getItemId() == R.id.action_import_backup) { + if (hasStoragePermission(REQUEST_IMPORT_BACKUP)) { + startActivity(new Intent(this, ImportBackupActivity.class)); } - final EditText editText = (EditText) v; - final boolean cursorAtZero = editText.getSelectionEnd() == 0 && editText.getSelectionStart() == 0; - if (keyCode == KeyEvent.KEYCODE_DEL && (cursorAtZero || editText.getText().length() == 0)) { - final Editable countryCode = this.binding.countryCode.getText(); - if (countryCode.length() > 0) { - countryCode.delete(countryCode.length() - 1, countryCode.length()); - this.binding.countryCode.setSelection(countryCode.length()); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); + if (grantResults.length > 0) { + if (allGranted(grantResults)) { + if (requestCode == REQUEST_IMPORT_BACKUP) { + startActivity(new Intent(this, ImportBackupActivity.class)); } - this.binding.countryCode.requestFocus(); - return true; + } else if (Arrays.asList(permissions) + .contains(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + Toast.makeText(this, R.string.no_storage_permission, Toast.LENGTH_SHORT).show(); } - return false; - }); - setRequestingVerificationState(requestingVerification); + } } @Override @@ -139,7 +201,9 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve @Override public void onStop() { if (xmppConnectionService != null) { - xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this); + xmppConnectionService + .getQuickConversationsService() + .removeOnVerificationRequestedListener(this); } super.onStop(); } @@ -149,18 +213,26 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve try { final Editable number = this.binding.number.getText(); final String input = number.toString(); - final Phonenumber.PhoneNumber phoneNumber = PhoneNumberUtilWrapper.getInstance(this).parse(input, region); + final Phonenumber.PhoneNumber phoneNumber = + PhoneNumberUtilWrapper.getInstance(this).parse(input, region); this.binding.countryCode.setText(String.valueOf(phoneNumber.getCountryCode())); number.clear(); number.append(String.valueOf(phoneNumber.getNationalNumber())); - final String formattedPhoneNumber = PhoneNumberUtilWrapper.getInstance(this).format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL).replace(' ','\u202F'); + final String formattedPhoneNumber = + PhoneNumberUtilWrapper.getInstance(this) + .format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL) + .replace(' ', '\u202F'); if (PhoneNumberUtilWrapper.getInstance(this).isValidNumber(phoneNumber)) { - builder.setMessage(Html.fromHtml(getString(R.string.we_will_be_verifying, formattedPhoneNumber))); + builder.setMessage( + Html.fromHtml( + getString(R.string.we_will_be_verifying, formattedPhoneNumber))); builder.setNegativeButton(R.string.edit, null); - builder.setPositiveButton(R.string.ok, (dialog, which) -> onPhoneNumberEntered(phoneNumber)); + builder.setPositiveButton( + R.string.ok, (dialog, which) -> onPhoneNumberEntered(phoneNumber)); } else { - builder.setMessage(getString(R.string.not_a_valid_phone_number, formattedPhoneNumber)); + builder.setMessage( + getString(R.string.not_a_valid_phone_number, formattedPhoneNumber)); builder.setPositiveButton(R.string.ok, null); } Log.d(Config.LOGTAG, phoneNumber.toString()); @@ -199,7 +271,8 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve final String region = data.getStringExtra("region"); if (region != null) { this.region = region; - final int countryCode = PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(region); + final int countryCode = + PhoneNumberUtilWrapper.getInstance(this).getCountryCodeForRegion(region); this.binding.countryCode.setText(String.valueOf(countryCode)); } } @@ -223,10 +296,11 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve @Override public void onVerificationRequestFailed(int code) { - runOnUiThread(() -> { - setRequestingVerificationState(false); - ApiDialogHelper.createError(this, code).show(); - }); + runOnUiThread( + () -> { + setRequestingVerificationState(false); + ApiDialogHelper.createError(this, code).show(); + }); } @Override diff --git a/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java b/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java index ae9894b47734b8700e433911a0fb8dafb243955f..728692def9afd85db0c571d45e80d6ffc73ee1d0 100644 --- a/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java +++ b/src/quicksy/java/eu/siacs/conversations/ui/VerifyActivity.java @@ -13,12 +13,9 @@ import android.os.Handler; import android.os.SystemClock; import android.text.Html; import android.view.View; - import androidx.databinding.DataBindingUtil; - import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; - import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityVerifyBinding; import eu.siacs.conversations.entities.Account; @@ -28,12 +25,13 @@ import eu.siacs.conversations.ui.util.PinEntryWrapper; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import eu.siacs.conversations.utils.TimeFrameUtils; - import io.michaelrocks.libphonenumber.android.NumberParseException; - import java.util.concurrent.atomic.AtomicBoolean; -public class VerifyActivity extends XmppActivity implements ClipboardManager.OnPrimaryClipChangedListener, QuickConversationsService.OnVerification, QuickConversationsService.OnVerificationRequested { +public class VerifyActivity extends XmppActivity + implements ClipboardManager.OnPrimaryClipChangedListener, + QuickConversationsService.OnVerification, + QuickConversationsService.OnVerificationRequested { public static final String EXTRA_RETRY_SMS_AFTER = "retry_sms_after"; private static final String EXTRA_RETRY_VERIFICATION_AFTER = "retry_verification_after"; @@ -46,23 +44,25 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP private boolean verifying = false; private boolean requestingVerification = false; private long retrySmsAfter = 0; - private final Runnable SMS_TIMEOUT_UPDATER = new Runnable() { - @Override - public void run() { - if (setTimeoutLabelInResendButton()) { - mHandler.postDelayed(this, 300); - } - } - }; + private final Runnable SMS_TIMEOUT_UPDATER = + new Runnable() { + @Override + public void run() { + if (setTimeoutLabelInResendButton()) { + mHandler.postDelayed(this, 300); + } + } + }; private long retryVerificationAfter = 0; - private final Runnable VERIFICATION_TIMEOUT_UPDATER = new Runnable() { - @Override - public void run() { - if (setTimeoutLabelInNextButton()) { - mHandler.postDelayed(this, 300); - } - } - }; + private final Runnable VERIFICATION_TIMEOUT_UPDATER = + new Runnable() { + @Override + public void run() { + if (setTimeoutLabelInNextButton()) { + mHandler.postDelayed(this, 300); + } + } + }; private final AtomicBoolean redirectInProgress = new AtomicBoolean(false); private boolean setTimeoutLabelInResendButton() { @@ -70,7 +70,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP long remaining = retrySmsAfter - SystemClock.elapsedRealtime(); if (remaining >= 0) { binding.resendSms.setEnabled(false); - binding.resendSms.setText(getString(R.string.resend_sms_in, TimeFrameUtils.resolve(VerifyActivity.this, remaining))); + binding.resendSms.setText( + getString( + R.string.resend_sms_in, + TimeFrameUtils.resolve(VerifyActivity.this, remaining))); return true; } } @@ -84,7 +87,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP long remaining = retryVerificationAfter - SystemClock.elapsedRealtime(); if (remaining >= 0) { binding.next.setEnabled(false); - binding.next.setText(getString(R.string.wait_x, TimeFrameUtils.resolve(VerifyActivity.this, remaining))); + binding.next.setText( + getString( + R.string.wait_x, + TimeFrameUtils.resolve(VerifyActivity.this, remaining))); return true; } } @@ -97,11 +103,20 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); String pin = savedInstanceState != null ? savedInstanceState.getString("pin") : null; - boolean verifying = savedInstanceState != null && savedInstanceState.getBoolean("verifying"); - boolean requestingVerification = savedInstanceState != null && savedInstanceState.getBoolean("requesting_verification", false); + boolean verifying = + savedInstanceState != null && savedInstanceState.getBoolean("verifying"); + boolean requestingVerification = + savedInstanceState != null + && savedInstanceState.getBoolean("requesting_verification", false); this.pasted = savedInstanceState != null ? savedInstanceState.getString("pasted") : null; - this.retrySmsAfter = savedInstanceState != null ? savedInstanceState.getLong(EXTRA_RETRY_SMS_AFTER, 0L) : 0L; - this.retryVerificationAfter = savedInstanceState != null ? savedInstanceState.getLong(EXTRA_RETRY_VERIFICATION_AFTER, 0L) : 0L; + this.retrySmsAfter = + savedInstanceState != null + ? savedInstanceState.getLong(EXTRA_RETRY_SMS_AFTER, 0L) + : 0L; + this.retryVerificationAfter = + savedInstanceState != null + ? savedInstanceState.getLong(EXTRA_RETRY_VERIFICATION_AFTER, 0L) + : 0L; this.binding = DataBindingUtil.setContentView(this, R.layout.activity_verify); Activities.setStatusAndNavigationBarColors(this, binding.getRoot()); setSupportActionBar(this.binding.toolbar); @@ -126,11 +141,13 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP if (this.account != null) { final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setMessage(R.string.abort_registration_procedure); - builder.setPositiveButton(R.string.yes, (dialog, which) -> { - xmppConnectionService.deleteAccount(account); - startActivity(intent); - finish(); - }); + builder.setPositiveButton( + R.string.yes, + (dialog, which) -> { + xmppConnectionService.deleteAccount(account); + startActivity(intent); + finish(); + }); builder.setNegativeButton(R.string.no, null); builder.create().show(); } else { @@ -156,7 +173,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP private void onResendSmsButton(View view) { try { - xmppConnectionService.getQuickConversationsService().requestVerification(PhoneNumberUtilWrapper.toPhoneNumber(this, account.getJid())); + xmppConnectionService + .getQuickConversationsService() + .requestVerification( + PhoneNumberUtilWrapper.toPhoneNumber(this, account.getJid())); setRequestingVerificationState(true); } catch (NumberParseException e) { @@ -182,29 +202,35 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP } else { setTimeoutLabelInResendButton(); } - } @Override - protected void refreshUiReal() { - - } + protected void refreshUiReal() {} @Override public void onBackendConnected() { xmppConnectionService.getQuickConversationsService().addOnVerificationListener(this); - xmppConnectionService.getQuickConversationsService().addOnVerificationRequestedListener(this); + xmppConnectionService + .getQuickConversationsService() + .addOnVerificationRequestedListener(this); this.account = AccountUtils.getFirst(xmppConnectionService); if (this.account == null) { return; } - if (!account.isOptionSet(Account.OPTION_UNVERIFIED) && !account.isOptionSet(Account.OPTION_DISABLED)) { + if (!account.isOptionSet(Account.OPTION_UNVERIFIED) + && !account.isOptionSet(Account.OPTION_DISABLED)) { runOnUiThread(this::performPostVerificationRedirect); return; } - this.binding.weHaveSent.setText(Html.fromHtml(getString(R.string.we_have_sent_you_an_sms_to_x, PhoneNumberUtilWrapper.toFormattedPhoneNumber(this, this.account.getJid())))); + this.binding.weHaveSent.setText( + Html.fromHtml( + getString( + R.string.we_have_sent_you_an_sms_to_x, + PhoneNumberUtilWrapper.toFormattedPhoneNumber( + this, this.account.getJid())))); setVerifyingState(xmppConnectionService.getQuickConversationsService().isVerifying()); - setRequestingVerificationState(xmppConnectionService.getQuickConversationsService().isRequestingVerification()); + setRequestingVerificationState( + xmppConnectionService.getQuickConversationsService().isRequestingVerification()); } @Override @@ -225,7 +251,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP super.onStart(); clipboardManager.addPrimaryClipChangedListener(this); final Intent intent = getIntent(); - this.retrySmsAfter = intent != null ? intent.getLongExtra(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter) : this.retrySmsAfter; + this.retrySmsAfter = + intent != null + ? intent.getLongExtra(EXTRA_RETRY_SMS_AFTER, this.retrySmsAfter) + : this.retrySmsAfter; if (this.retrySmsAfter > 0) { mHandler.post(SMS_TIMEOUT_UPDATER); } @@ -242,7 +271,9 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP clipboardManager.removePrimaryClipChangedListener(this); if (xmppConnectionService != null) { xmppConnectionService.getQuickConversationsService().removeOnVerificationListener(this); - xmppConnectionService.getQuickConversationsService().removeOnVerificationRequestedListener(this); + xmppConnectionService + .getQuickConversationsService() + .removeOnVerificationRequestedListener(this); } } @@ -250,14 +281,15 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP public void onResume() { super.onResume(); if (pinEntryWrapper.isEmpty()) { - //starting with Android P we need input focus + // starting with Android P we need input focus pinEntryWrapper.requestFocus(); pastePinFromClipboard(); } } private void pastePinFromClipboard() { - final ClipDescription description = clipboardManager != null ? clipboardManager.getPrimaryClipDescription() : null; + final ClipDescription description = + clipboardManager != null ? clipboardManager.getPrimaryClipDescription() : null; if (description != null && description.hasMimeType(MIMETYPE_TEXT_PLAIN)) { final ClipData primaryClip = clipboardManager.getPrimaryClip(); if (primaryClip != null && primaryClip.getItemCount() > 0) { @@ -265,7 +297,11 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP if (PinEntryWrapper.isValidPin(clip) && !clip.toString().equals(this.pasted)) { this.pasted = clip.toString(); pinEntryWrapper.setPin(clip.toString()); - final Snackbar snackbar = Snackbar.make(binding.coordinator, R.string.possible_pin, Snackbar.LENGTH_LONG); + final Snackbar snackbar = + Snackbar.make( + binding.coordinator, + R.string.possible_pin, + Snackbar.LENGTH_LONG); snackbar.setAction(R.string.undo, v -> pinEntryWrapper.clear()); snackbar.show(); } @@ -291,17 +327,19 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP @Override public void onVerificationFailed(final int code) { - runOnUiThread(() -> { - setVerifyingState(false); - if (code == 401 || code == 404) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(code == 404 ? R.string.pin_expired : R.string.incorrect_pin); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); - } else { - ApiDialogHelper.createError(this, code).show(); - } - }); + runOnUiThread( + () -> { + setVerifyingState(false); + if (code == 401 || code == 404) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage( + code == 404 ? R.string.pin_expired : R.string.incorrect_pin); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + } else { + ApiDialogHelper.createError(this, code).show(); + } + }); } @Override @@ -312,10 +350,11 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP @Override public void onVerificationRetryAt(long timestamp) { this.retryVerificationAfter = timestamp; - runOnUiThread(() -> { - ApiDialogHelper.createTooManyAttempts(this).show(); - setVerifyingState(false); - }); + runOnUiThread( + () -> { + ApiDialogHelper.createTooManyAttempts(this).show(); + setVerifyingState(false); + }); mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER); runOnUiThread(VERIFICATION_TIMEOUT_UPDATER); } @@ -326,35 +365,38 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP setVerifyingState(true); } - //send sms again button callback + // send sms again button callback @Override public void onVerificationRequestFailed(int code) { - runOnUiThread(() -> { - setRequestingVerificationState(false); - ApiDialogHelper.createError(this, code).show(); - }); + runOnUiThread( + () -> { + setRequestingVerificationState(false); + ApiDialogHelper.createError(this, code).show(); + }); } - //send sms again button callback + // send sms again button callback @Override public void onVerificationRequested() { - runOnUiThread(() -> { - pinEntryWrapper.clear(); - setRequestingVerificationState(false); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setMessage(R.string.we_have_sent_you_another_sms); - builder.setPositiveButton(R.string.ok, null); - builder.create().show(); - }); + runOnUiThread( + () -> { + pinEntryWrapper.clear(); + setRequestingVerificationState(false); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(R.string.we_have_sent_you_another_sms); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + }); } @Override public void onVerificationRequestedRetryAt(long timestamp) { this.retrySmsAfter = timestamp; - runOnUiThread(() -> { - ApiDialogHelper.createRateLimited(this, timestamp).show(); - setRequestingVerificationState(false); - }); + runOnUiThread( + () -> { + ApiDialogHelper.createRateLimited(this, timestamp).show(); + setRequestingVerificationState(false); + }); mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER); runOnUiThread(SMS_TIMEOUT_UPDATER); } diff --git a/src/quicksy/res/menu/verify_phone_number_menu.xml b/src/quicksy/res/menu/verify_phone_number_menu.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd08df4333d011723e960767bf29cc96a77e07c3 --- /dev/null +++ b/src/quicksy/res/menu/verify_phone_number_menu.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file From 9270d7bbf3ea79ddcf295fb9f2ad59b06cb604b0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 27 Mar 2025 12:19:12 +0100 Subject: [PATCH 050/169] add ability to pick custom backup location --- src/main/AndroidManifest.xml | 5 - .../eu/siacs/conversations/AppSettings.java | 52 + .../crypto/axolotl/SQLiteAxolotlStore.java | 955 +++++++++--------- .../services/XmppConnectionService.java | 4 - .../settings/BackupSettingsFragment.java | 49 +- .../siacs/conversations/utils/BackupFile.java | 49 +- .../worker/ExportBackupWorker.java | 173 ++-- src/main/res/drawable/ic_folder_open_24dp.xml | 10 + src/main/res/values/strings.xml | 3 +- src/main/res/xml/preferences_backup.xml | 6 +- 10 files changed, 735 insertions(+), 571 deletions(-) create mode 100644 src/main/res/drawable/ic_folder_open_24dp.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index f765d5dfb58082ae7dc92c10e8b412d77eb92ee3..9ad0b4b364d3197a1399dc7334a9a468d5e6014a 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -70,10 +70,6 @@ - - - - @@ -82,7 +78,6 @@ - preKeysMarkedForRemoval = new HashSet<>(); - - private final LruCache trustCache = - new LruCache(NUM_TRUSTS_TO_CACHE) { - @Override - protected FingerprintStatus create(String fingerprint) { - return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint); - } - }; - - private static IdentityKeyPair generateIdentityKeyPair() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - } - - private static int generateRegistrationId() { - Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); - return KeyHelper.generateRegistrationId(true); - } - - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - } - - public int getCurrentPreKeyId() { - return currentPreKeyId; - } - - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- - - private IdentityKeyPair loadIdentityKeyPair() { - synchronized (mXmppConnectionService) { - IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); - - if (ownKey != null) { - return ownKey; - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair"); - ownKey = generateIdentityKeyPair(); - mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey); - } - return ownKey; - } - } - - private int loadRegistrationId() { - return loadRegistrationId(false); - } - - private int loadRegistrationId(boolean regenerate) { - String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); - int reg_id; - if (!regenerate && regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int prekey_id; - if (prekeyIdString != null) { - prekey_id = Integer.valueOf(prekeyIdString); - } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid()); - prekey_id = 0; - } - return prekey_id; - } - - public void regenerate() { - mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); - trustCache.evictAll(); - account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); - identityKeyPair = loadIdentityKeyPair(); - localRegistrationId = loadRegistrationId(true); - currentPreKeyId = 0; - mXmppConnectionService.updateAccountUi(); - } - - /** - * Get the local client's identity key pair. - * - * @return The local client's persistent identity key pair. - */ - @Override - public IdentityKeyPair getIdentityKeyPair() { - if (identityKeyPair == null) { - identityKeyPair = loadIdentityKeyPair(); - } - return identityKeyPair; - } - - /** - * Return the local client's registration ID. - *

- * Clients should maintain a registration ID, a random number - * between 1 and 16380 that's generated once at install time. - * - * @return the local client's registration ID. - */ - @Override - public int getLocalRegistrationId() { - return localRegistrationId; - } - - /** - * Save a remote client's identity key - *

- * Store a remote client's identity key as trusted. - * - * @param address The address of the remote client. - * @param identityKey The remote client's identity key. - * @return true on success - */ - @Override - public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, address.getName()).contains(identityKey)) { - String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); - FingerprintStatus status = getFingerprintStatus(fingerprint); - if (status == null) { - if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(address.getName())) { - Log.d(Config.LOGTAG,account.getJid().asBareJid()+": blindly trusted "+fingerprint+" of "+address.getName()); - status = FingerprintStatus.createActiveTrusted(); - } else { - status = FingerprintStatus.createActiveUndecided(); - } - } else { - status = status.toActive(); - } - mXmppConnectionService.databaseBackend.storeIdentityKey(account, address.getName(), identityKey, status); - trustCache.remove(fingerprint); - } - return true; - } - - /** - * Verify a remote client's identity key. - *

- * Determine whether a remote client's identity is trusted. Convention is - * that the TextSecure protocol is 'trust on first use.' This means that - * an identity key is considered 'trusted' if there is no entry for the recipient - * in the local store, or if it matches the saved key for a recipient in the local - * store. Only if it mismatches an entry in the local store is it considered - * 'untrusted.' - * - * @param identityKey The identity key to verify. - * @return true if trusted, false if untrusted. - */ - @Override - public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { - return true; - } - - public FingerprintStatus getFingerprintStatus(String fingerprint) { - return (fingerprint == null)? null : trustCache.get(fingerprint); - } - - public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); - trustCache.remove(fingerprint); - } - - public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { - mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate); - } - - public X509Certificate getFingerprintCertificate(String fingerprint) { - return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint); - } - - public Set getContactKeysWithTrust(String bareJid, FingerprintStatus status) { - return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); - } - - public long getContactNumTrustedKeys(String bareJid) { - return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); - } - - // -------------------------------------- - // SessionStore - // -------------------------------------- - - /** - * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, - * or a new SessionRecord if one does not currently exist. - *

- * It is important that implementations return a copy of the current durable information. The - * returned SessionRecord may be modified, but those changes should not have an effect on the - * durable session state (what is returned by subsequent calls to this method) without the - * store method being called here first. - * - * @param address The name and device ID of the remote client. - * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or - * a new SessionRecord if one does not currently exist. - */ - @Override - public SessionRecord loadSession(SignalProtocolAddress address) { - SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session != null) ? session : new SessionRecord(); - } - - /** - * Returns all known devices with active sessions for a recipient - * - * @param name the name of the client. - * @return all known sub-devices with active sessions. - */ - @Override - public List getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new SignalProtocolAddress(name, 0)); - } - - - public List getKnownAddresses() { - return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account); - } - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(SignalProtocolAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(SignalProtocolAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(SignalProtocolAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - SignalProtocolAddress address = new SignalProtocolAddress(name, 0); - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - address); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); - if (success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - Log.d(Config.LOGTAG,"mark prekey for removal "+preKeyId); - synchronized (preKeysMarkedForRemoval) { - preKeysMarkedForRemoval.add(preKeyId); - } - } - - - public boolean flushPreKeys() { - Log.d(Config.LOGTAG,"flushing pre keys"); - int count = 0; - synchronized (preKeysMarkedForRemoval) { - for(Integer preKeyId : preKeysMarkedForRemoval) { - count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - preKeysMarkedForRemoval.clear(); - } - return count > 0; - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if (record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - public int getSignedPreKeysCount() { - return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - - public void preVerifyFingerprint(Account account, String name, String fingerprint) { - mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified()); - } + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String IDENTITIES_TABLENAME = "identities"; + public static final String ACCOUNT = "account"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String FINGERPRINT = "fingerprint"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; // no longer used + public static final String TRUST = "trust"; + public static final String ACTIVE = "active"; + public static final String LAST_ACTIVATION = "last_activation"; + public static final String OWN = "ownkey"; + public static final String CERTIFICATE = "certificate"; + + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; + + private static final int NUM_TRUSTS_TO_CACHE = 100; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private IdentityKeyPair identityKeyPair; + private int localRegistrationId; + private int currentPreKeyId = 0; + + private final HashSet preKeysMarkedForRemoval = new HashSet<>(); + + private final LruCache trustCache = + new LruCache(NUM_TRUSTS_TO_CACHE) { + @Override + protected FingerprintStatus create(String fingerprint) { + return mXmppConnectionService.databaseBackend.getFingerprintStatus( + account, fingerprint); + } + }; + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.i( + Config.LOGTAG, + AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + return new IdentityKeyPair( + new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + } + + private static int generateRegistrationId() { + Log.i( + Config.LOGTAG, + AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID..."); + return KeyHelper.generateRegistrationId(true); + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + synchronized (mXmppConnectionService) { + IdentityKeyPair ownKey = + mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account); + + if (ownKey != null) { + return ownKey; + } else { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Could not retrieve own IdentityKeyPair"); + ownKey = generateIdentityKeyPair(); + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey); + } + return ownKey; + } + } + + private int loadRegistrationId() { + return loadRegistrationId(false); + } + + private int loadRegistrationId(boolean regenerate) { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (!regenerate && regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.i( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Could not retrieve axolotl registration id for account " + + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = + this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int prekey_id; + if (prekeyIdString != null) { + prekey_id = Integer.valueOf(prekeyIdString); + } else { + Log.w( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Could not retrieve current prekey id for account " + + account.getJid()); + prekey_id = 0; + } + return prekey_id; + } + + public void regenerate() { + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + trustCache.evictAll(); + account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); + identityKeyPair = loadIdentityKeyPair(); + localRegistrationId = loadRegistrationId(true); + currentPreKeyId = 0; + mXmppConnectionService.updateAccountUi(); + } + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + if (identityKeyPair == null) { + identityKeyPair = loadIdentityKeyPair(); + } + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + * + *

Clients should maintain a registration ID, a random number between 1 and 16380 that's + * generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + * + *

Store a remote client's identity key as trusted. + * + * @param address The address of the remote client. + * @param identityKey The remote client's identity key. + * @return true on success + */ + @Override + public boolean saveIdentity( + final SignalProtocolAddress address, final IdentityKey identityKey) { + if (!mXmppConnectionService + .databaseBackend + .loadIdentityKeys(account, address.getName()) + .contains(identityKey)) { + String fingerprint = CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()); + FingerprintStatus status = getFingerprintStatus(fingerprint); + if (status == null) { + if (mXmppConnectionService.getAppSettings().isBTBVEnabled() + && !account.getAxolotlService().hasVerifiedKeys(address.getName())) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": blindly trusted " + + fingerprint + + " of " + + address.getName()); + status = FingerprintStatus.createActiveTrusted(); + } else { + status = FingerprintStatus.createActiveUndecided(); + } + } else { + status = status.toActive(); + } + mXmppConnectionService.databaseBackend.storeIdentityKey( + account, address.getName(), identityKey, status); + trustCache.remove(fingerprint); + } + return true; + } + + /** + * Verify a remote client's identity key. + * + *

Determine whether a remote client's identity is trusted. Convention is that the TextSecure + * protocol is 'trust on first use.' This means that an identity key is considered 'trusted' if + * there is no entry for the recipient in the local store, or if it matches the saved key for a + * recipient in the local store. Only if it mismatches an entry in the local store is it + * considered 'untrusted.' + * + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity( + SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { + return true; + } + + public FingerprintStatus getFingerprintStatus(String fingerprint) { + return (fingerprint == null) ? null : trustCache.get(fingerprint); + } + + public void setFingerprintStatus(String fingerprint, FingerprintStatus status) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status); + trustCache.remove(fingerprint); + } + + public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { + mXmppConnectionService.databaseBackend.setIdentityKeyCertificate( + account, fingerprint, x509Certificate); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate( + account, fingerprint); + } + + public Set getContactKeysWithTrust(String bareJid, FingerprintStatus status) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status); + } + + public long getContactNumTrustedKeys(String bareJid) { + return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid); + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId + * tuple, or a new SessionRecord if one does not currently exist. + * + *

It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the store + * method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or a + * new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(SignalProtocolAddress address) { + SessionRecord session = + mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session != null) ? session : new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions( + account, new SignalProtocolAddress(name, 0)); + } + + public List getKnownAddresses() { + return mXmppConnectionService.databaseBackend.getKnownSignalAddresses(account); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(SignalProtocolAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId + * tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(SignalProtocolAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(SignalProtocolAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + SignalProtocolAddress address = new SignalProtocolAddress(name, 0); + mXmppConnectionService.databaseBackend.deleteAllSessions(account, address); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = + this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e( + Config.LOGTAG, + AxolotlService.getLogprefix(account) + + "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + Log.d(Config.LOGTAG, "mark prekey for removal " + preKeyId); + synchronized (preKeysMarkedForRemoval) { + preKeysMarkedForRemoval.add(preKeyId); + } + } + + public boolean flushPreKeys() { + Log.d(Config.LOGTAG, "flushing pre keys"); + int count = 0; + synchronized (preKeysMarkedForRemoval) { + for (Integer preKeyId : preKeysMarkedForRemoval) { + count += mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + preKeysMarkedForRemoval.clear(); + } + return count > 0; + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = + mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if (record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + public int getSignedPreKeysCount() { + return mXmppConnectionService.databaseBackend.getSignedPreKeysCount(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + + public void preVerifyFingerprint(Account account, String name, String fingerprint) { + mXmppConnectionService.databaseBackend.storePreVerification( + account, name, fingerprint, FingerprintStatus.createInactiveVerified()); + } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ce482490e10f26ed49e3ed29aea70d472cb4ef99..32b1ae4645475c275d3e39abf1156635168cb78e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -6341,10 +6341,6 @@ public class XmppConnectionService extends Service { return verifiedSomething; } - public boolean blindTrustBeforeVerification() { - return getBooleanPreference(AppSettings.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv); - } - public ShortcutService getShortcutService() { return mShortcutService; } diff --git a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java index 739598a69009561a7f6a6bec4076fc6530d31c32..2a92bebf594fee436d42cedeb515ee250abcfe25 100644 --- a/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/fragment/settings/BackupSettingsFragment.java @@ -1,12 +1,13 @@ package eu.siacs.conversations.ui.fragment.settings; import android.Manifest; +import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.widget.Toast; - import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -22,16 +23,13 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.OutOfQuotaPolicy; import androidx.work.PeriodicWorkRequest; import androidx.work.WorkManager; - import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Strings; import com.google.common.primitives.Longs; - +import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.worker.ExportBackupWorker; - import java.util.concurrent.TimeUnit; public class BackupSettingsFragment extends XmppPreferenceFragment { @@ -56,20 +54,33 @@ public class BackupSettingsFragment extends XmppPreferenceFragment { } }); + private final ActivityResultLauncher pickBackupLocationLauncher = + registerForActivityResult( + new ActivityResultContracts.OpenDocumentTree(), + uri -> { + if (uri == null) { + Log.d(Config.LOGTAG, "no backup location selected"); + return; + } + submitBackupLocationPreference(uri); + }); + @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.preferences_backup, rootKey); final var createOneOffBackup = findPreference(CREATE_ONE_OFF_BACKUP); final ListPreference recurringBackup = findPreference(RECURRING_BACKUP); - final var backupDirectory = findPreference("backup_directory"); - if (createOneOffBackup == null || recurringBackup == null || backupDirectory == null) { + final var backupLocation = findPreference(AppSettings.BACKUP_LOCATION); + if (createOneOffBackup == null || recurringBackup == null || backupLocation == null) { throw new IllegalStateException( "The preference resource file is missing some preferences"); } - backupDirectory.setSummary( + final var appSettings = new AppSettings(requireContext()); + backupLocation.setSummary( getString( R.string.pref_create_backup_summary, - FileBackend.getBackupDirectory(requireContext()).getAbsolutePath())); + appSettings.getBackupLocationAsPath())); + backupLocation.setOnPreferenceClickListener(this::onBackupLocationPreferenceClicked); createOneOffBackup.setOnPreferenceClickListener(this::onBackupPreferenceClicked); setValues( recurringBackup, @@ -77,6 +88,26 @@ public class BackupSettingsFragment extends XmppPreferenceFragment { value -> timeframeValueToName(requireContext(), value)); } + private boolean onBackupLocationPreferenceClicked(final Preference preference) { + this.pickBackupLocationLauncher.launch(null); + return false; + } + + private void submitBackupLocationPreference(final Uri uri) { + final var contentResolver = requireContext().getContentResolver(); + contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + final var appSettings = new AppSettings(requireContext()); + appSettings.setBackupLocation(uri); + final var preference = findPreference(AppSettings.BACKUP_LOCATION); + if (preference == null) { + return; + } + preference.setSummary( + getString(R.string.pref_create_backup_summary, AppSettings.asPath(uri))); + } + @Override protected void onSharedPreferenceChanged(@NonNull String key) { super.onSharedPreferenceChanged(key); diff --git a/src/main/java/eu/siacs/conversations/utils/BackupFile.java b/src/main/java/eu/siacs/conversations/utils/BackupFile.java index 01ecf35b83d4d0d6973a54da84c3374d2a89289c..7644f6ccbf2f64f08140159e53ae9f1edd1ad34c 100644 --- a/src/main/java/eu/siacs/conversations/utils/BackupFile.java +++ b/src/main/java/eu/siacs/conversations/utils/BackupFile.java @@ -1,8 +1,12 @@ package eu.siacs.conversations.utils; import android.content.Context; +import android.content.UriPermission; import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; import android.util.Log; +import androidx.documentfile.provider.DocumentFile; import com.google.common.collect.Collections2; import com.google.common.collect.ComparisonChain; import com.google.common.collect.ImmutableList; @@ -15,6 +19,7 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.persistance.DatabaseBackend; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.worker.ExportBackupWorker; import eu.siacs.conversations.xmpp.Jid; import java.io.DataInputStream; import java.io.File; @@ -81,11 +86,51 @@ public class BackupFile implements Comparable { final var backupFiles = new ImmutableList.Builder(); final var apps = ImmutableSet.of("Conversations", "Quicksy", context.getString(R.string.app_name)); + + final var uriPermissions = context.getContentResolver().getPersistedUriPermissions(); + + for (final UriPermission uriPermission : uriPermissions) { + final var uri = uriPermission.getUri(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && DocumentsContract.isTreeUri(uri)) { + Log.d(Config.LOGTAG, "looking for backups in " + uri); + final var tree = DocumentFile.fromTreeUri(context, uriPermission.getUri()); + final var files = tree == null ? new DocumentFile[0] : tree.listFiles(); + for (final DocumentFile documentFile : files) { + final var name = documentFile.getName(); + if (documentFile.isFile() + && (ExportBackupWorker.MIME_TYPE.equals(documentFile.getType()) + || (name != null && name.endsWith(".ceb")))) { + try { + final BackupFile backupFile = + BackupFile.read(context, documentFile.getUri()); + if (accounts.contains(backupFile.getHeader().getJid())) { + Log.d( + Config.LOGTAG, + "skipping backup for " + backupFile.getHeader().getJid()); + } else { + backupFiles.add(backupFile); + } + } catch (final IOException + | IllegalArgumentException + | BackupFileHeader.OutdatedBackupFileVersion e) { + Log.d(Config.LOGTAG, "unable to read backup file ", e); + } + } + } + } + } + final List directories = new ArrayList<>(); for (final String app : apps) { directories.add(FileBackend.getLegacyBackupDirectory(app)); } - directories.add(FileBackend.getBackupDirectory(context)); + if (uriPermissions.isEmpty()) { + Log.d( + Config.LOGTAG, + "including default directory since no uri permissions have been granted"); + directories.add(FileBackend.getBackupDirectory(context)); + } for (final File directory : directories) { if (!directory.exists() || !directory.isDirectory()) { Log.d(Config.LOGTAG, "directory not found: " + directory.getAbsolutePath()); @@ -134,7 +179,7 @@ public class BackupFile implements Comparable { public int compareTo(final BackupFile o) { return ComparisonChain.start() .compare(header.getJid(), o.header.getJid()) - .compare(header.getTimestamp(), o.header.getTimestamp()) + .compare(o.header.getTimestamp(), header.getTimestamp()) .result(); } } diff --git a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java index c5178e0adfa91c251b8afa028711964fd1ba5891..2ec4fc45e1b60038933bdbdc6100f076e260067a 100644 --- a/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java +++ b/src/main/java/eu/siacs/conversations/worker/ExportBackupWorker.java @@ -15,14 +15,15 @@ import android.os.SystemClock; import android.util.Log; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; +import androidx.documentfile.provider.DocumentFile; import androidx.work.ForegroundInfo; import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; -import com.google.common.base.Optional; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.gson.stream.JsonWriter; +import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore; @@ -38,6 +39,7 @@ import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; @@ -92,7 +94,7 @@ public class ExportBackupWorker extends Worker { @Override public Result doWork() { setForegroundAsync(getForegroundInfo()); - final List files; + final List files; try { files = export(); } catch (final IOException @@ -132,7 +134,7 @@ public class ExportBackupWorker extends Worker { } } - private List export() + private List export() throws IOException, InvalidKeySpecException, InvalidAlgorithmParameterException, @@ -141,17 +143,19 @@ public class ExportBackupWorker extends Worker { NoSuchAlgorithmException, NoSuchProviderException { final Context context = getApplicationContext(); + final var appSettings = new AppSettings(context); + final var backupLocation = appSettings.getBackupLocation(); final var database = DatabaseBackend.getInstance(context); final var accounts = database.getAccounts(); int count = 0; final int max = accounts.size(); - final ImmutableList.Builder files = new ImmutableList.Builder<>(); + final ImmutableList.Builder locations = new ImmutableList.Builder<>(); Log.d(Config.LOGTAG, "starting backup for " + max + " accounts"); for (final Account account : accounts) { if (isStopped()) { Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have"); - return files.build(); + return locations.build(); } final String password = account.getPassword(); if (Strings.nullToEmpty(password).trim().isEmpty()) { @@ -164,34 +168,24 @@ public class ExportBackupWorker extends Worker { count++; continue; } - final String filename = - String.format( - "%s.%s.ceb", - account.getJid().asBareJid().toString(), - DATE_FORMAT.format(new Date())); - final File file = new File(FileBackend.getBackupDirectory(context), filename); + final Uri uri; try { - export(database, account, password, file, max, count); + uri = export(database, account, password, backupLocation, max, count); } catch (final WorkStoppedException e) { - if (file.delete()) { - Log.d( - Config.LOGTAG, - "deleted in progress backup file " + file.getAbsolutePath()); - } Log.d(Config.LOGTAG, "ExportBackupWorker has stopped. Returning what we have"); - return files.build(); + return locations.build(); } - files.add(file); + locations.add(uri); count++; } - return files.build(); + return locations.build(); } - private void export( + private Uri export( final DatabaseBackend database, final Account account, final String password, - final File file, + final Uri backupLocation, final int max, final int count) throws IOException, @@ -230,12 +224,36 @@ public class ExportBackupWorker extends Worker { cancelPendingIntent) .build()); final Progress progress = new Progress(notification, max, count); - final File directory = file.getParentFile(); - if (directory != null && directory.mkdirs()) { - Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); + final String filename = + String.format( + "%s.%s.ceb", + account.getJid().asBareJid().toString(), DATE_FORMAT.format(new Date())); + final OutputStream outputStream; + final Uri location; + if ("file".equalsIgnoreCase(backupLocation.getScheme())) { + final File file = new File(backupLocation.getPath(), filename); + final File directory = file.getParentFile(); + if (directory != null && directory.mkdirs()) { + Log.d(Config.LOGTAG, "created backup directory " + directory.getAbsolutePath()); + } + outputStream = new FileOutputStream(file); + location = Uri.fromFile(file); + } else { + final var tree = DocumentFile.fromTreeUri(context, backupLocation); + if (tree == null) { + throw new IOException( + String.format( + "DocumentFile.fromTreeUri returned null for %s", backupLocation)); + } + final var file = tree.createFile(MIME_TYPE, filename); + if (file == null) { + throw new IOException( + String.format("Could not create %s in %s", filename, backupLocation)); + } + location = file.getUri(); + outputStream = context.getContentResolver().openOutputStream(location); } - final FileOutputStream fileOutputStream = new FileOutputStream(file); - final DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + final DataOutputStream dataOutputStream = new DataOutputStream(outputStream); backupFileHeader.write(dataOutputStream); dataOutputStream.flush(); @@ -247,7 +265,7 @@ public class ExportBackupWorker extends Worker { SecretKeySpec keySpec = new SecretKeySpec(key, KEY_TYPE); IvParameterSpec ivSpec = new IvParameterSpec(IV); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); - CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, cipher); + CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(cipherOutputStream); final JsonWriter jsonWriter = @@ -257,21 +275,24 @@ public class ExportBackupWorker extends Worker { final String uuid = account.getUuid(); accountExport(db, uuid, jsonWriter); simpleExport(db, Conversation.TABLENAME, Conversation.ACCOUNT, uuid, jsonWriter); - messageExport(db, uuid, jsonWriter, progress); + messageExport(db, uuid, location, jsonWriter, progress); for (final String table : Arrays.asList( SQLiteAxolotlStore.PREKEY_TABLENAME, SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, SQLiteAxolotlStore.SESSION_TABLENAME, SQLiteAxolotlStore.IDENTITIES_TABLENAME)) { - throwIfWorkStopped(); + throwIfWorkStopped(location); simpleExport(db, table, SQLiteAxolotlStore.ACCOUNT, uuid, jsonWriter); } jsonWriter.endArray(); jsonWriter.flush(); jsonWriter.close(); - mediaScannerScanFile(file); - Log.d(Config.LOGTAG, "written backup to " + file.getAbsoluteFile()); + if ("file".equalsIgnoreCase(location.getScheme())) { + mediaScannerScanFile(new File(location.getPath())); + } + Log.d(Config.LOGTAG, "written backup to " + location); + return location; } private NotificationCompat.Builder getNotification() { @@ -287,8 +308,20 @@ public class ExportBackupWorker extends Worker { return notification; } - private void throwIfWorkStopped() throws WorkStoppedException { + private void throwIfWorkStopped(final Uri location) throws WorkStoppedException { if (isStopped()) { + if ("file".equalsIgnoreCase(location.getScheme())) { + final var file = new File(location.getPath()); + if (file.delete()) { + Log.d(Config.LOGTAG, "deleted " + file.getAbsolutePath()); + } + } else { + final var documentFile = + DocumentFile.fromSingleUri(getApplicationContext(), location); + if (documentFile != null && documentFile.delete()) { + Log.d(Config.LOGTAG, "deleted " + location); + } + } throw new WorkStoppedException(); } } @@ -371,6 +404,7 @@ public class ExportBackupWorker extends Worker { private void messageExport( final SQLiteDatabase db, final String uuid, + final Uri location, final JsonWriter writer, final Progress progress) throws IOException, WorkStoppedException { @@ -388,7 +422,7 @@ public class ExportBackupWorker extends Worker { int i = 0; int p = Integer.MIN_VALUE; while (cursor != null && cursor.moveToNext()) { - throwIfWorkStopped(); + throwIfWorkStopped(location); writer.beginObject(); writer.name("table"); writer.value(Message.TABLENAME); @@ -425,16 +459,18 @@ public class ExportBackupWorker extends Worker { .getEncoded(); } - private void notifySuccess(final List files) { + private void notifySuccess(final List locations) { final var context = getApplicationContext(); - final String path = FileBackend.getBackupDirectory(context).getAbsolutePath(); - - final var openFolderIntent = getOpenFolderIntent(path); - + final var appSettings = new AppSettings(context); + final String path = appSettings.getBackupLocationAsPath(); final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); final ArrayList uris = new ArrayList<>(); - for (final File file : files) { - uris.add(FileBackend.getUriForFile(context, file)); + for (final Uri uri : locations) { + if ("file".equalsIgnoreCase(uri.getScheme())) { + uris.add(FileBackend.getUriForFile(context, new File(uri.getPath()))); + } else { + uris.add(uri); + } } intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -444,8 +480,8 @@ public class ExportBackupWorker extends Worker { final var shareFilesIntent = PendingIntent.getActivity(context, 190, chooser, PENDING_INTENT_FLAGS); - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, "backup"); - mBuilder.setContentTitle(context.getString(R.string.notification_backup_created_title)) + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "backup"); + builder.setContentTitle(context.getString(R.string.notification_backup_created_title)) .setContentText( context.getString(R.string.notification_backup_created_subtitle, path)) .setStyle( @@ -453,60 +489,17 @@ public class ExportBackupWorker extends Worker { .bigText( context.getString( R.string.notification_backup_created_subtitle, - FileBackend.getBackupDirectory(context) - .getAbsolutePath()))) + path))) .setAutoCancel(true) .setSmallIcon(R.drawable.ic_archive_24dp); - if (openFolderIntent.isPresent()) { - mBuilder.setContentIntent(openFolderIntent.get()); - } else { - Log.w(Config.LOGTAG, "no app can display folders"); - } - - mBuilder.addAction( + builder.addAction( R.drawable.ic_share_24dp, context.getString(R.string.share_backup_files), shareFilesIntent); + builder.setLocalOnly(true); final var notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, mBuilder.build()); - } - - private Optional getOpenFolderIntent(final String path) { - final var context = getApplicationContext(); - for (final Intent intent : getPossibleFileOpenIntents(context, path)) { - if (intent.resolveActivityInfo(context.getPackageManager(), 0) != null) { - return Optional.of( - PendingIntent.getActivity(context, 189, intent, PENDING_INTENT_FLAGS)); - } - } - return Optional.absent(); - } - - private static List getPossibleFileOpenIntents( - final Context context, final String path) { - - // http://www.openintents.org/action/android-intent-action-view/file-directory - // do not use 'vnd.android.document/directory' since this will trigger system file manager - final Intent openIntent = new Intent(Intent.ACTION_VIEW); - openIntent.addCategory(Intent.CATEGORY_DEFAULT); - if (Compatibility.runsAndTargetsTwentyFour(context)) { - openIntent.setType("resource/folder"); - } else { - openIntent.setDataAndType(Uri.parse("file://" + path), "resource/folder"); - } - openIntent.putExtra("org.openintents.extra.ABSOLUTE_PATH", path); - - final Intent amazeIntent = new Intent(Intent.ACTION_VIEW); - amazeIntent.setDataAndType(Uri.parse("com.amaze.filemanager:" + path), "resource/folder"); - - // will open a file manager at root and user can navigate themselves - final Intent systemFallBack = new Intent(Intent.ACTION_VIEW); - systemFallBack.addCategory(Intent.CATEGORY_DEFAULT); - systemFallBack.setData( - Uri.parse("content://com.android.externalstorage.documents/root/primary")); - - return Arrays.asList(openIntent, amazeIntent, systemFallBack); + notificationManager.notify(BACKUP_CREATED_NOTIFICATION_ID, builder.build()); } private static class Progress { diff --git a/src/main/res/drawable/ic_folder_open_24dp.xml b/src/main/res/drawable/ic_folder_open_24dp.xml new file mode 100644 index 0000000000000000000000000000000000000000..9881c7748400cbf8037c92bfd11c0fea7454421c --- /dev/null +++ b/src/main/res/drawable/ic_folder_open_24dp.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index a7df4b226bd6e4a304b2a2a3d49b77034692c088..c8cfeb49493bbee6995daab50f06e1bfcbdc398a 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -331,7 +331,7 @@ Foreground service Prevents the operating system from killing your connection Create backup - Backup files will be stored in %s + Backups will be stored in %s Creating backup files Your backup has been created The backup files have been stored in %s @@ -1107,4 +1107,5 @@ Enable customized notification settings (importance, sound, vibration) settings for this conversation? Would you like to delete your avatar? Some clients might continue to display a cached copy of your avatar. Show to contacts only + Backup location diff --git a/src/main/res/xml/preferences_backup.xml b/src/main/res/xml/preferences_backup.xml index b0362c7856b02349c266f04c63359e97e9634a51..f026103ceed70f37a2b150d0ec533912a9850e39 100644 --- a/src/main/res/xml/preferences_backup.xml +++ b/src/main/res/xml/preferences_backup.xml @@ -14,7 +14,9 @@ android:title="@string/pref_create_backup" /> + android:icon="@drawable/ic_folder_open_24dp" + android:key="backup_location" + android:summary="@string/pref_create_backup_summary" + android:title="@string/pref_backup_location" /> \ No newline at end of file From caed3a6ab302f24ee599f88eca4b9a281e92dc7f Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 24 Mar 2025 11:59:33 +0000 Subject: [PATCH 051/169] Added translation using Weblate (Hebrew) --- src/conversations/res/values-iw/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/conversations/res/values-iw/strings.xml diff --git a/src/conversations/res/values-iw/strings.xml b/src/conversations/res/values-iw/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/conversations/res/values-iw/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 9814fab5968642b4a1cfced27068f726283cf088 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 24 Mar 2025 11:59:43 +0000 Subject: [PATCH 052/169] Added translation using Weblate (Hebrew) --- src/quicksy/res/values-iw/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/quicksy/res/values-iw/strings.xml diff --git a/src/quicksy/res/values-iw/strings.xml b/src/quicksy/res/values-iw/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/quicksy/res/values-iw/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 89bd59ed14ce4fba1a007b15dc5465853ff6dfc8 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 24 Mar 2025 12:08:57 +0000 Subject: [PATCH 053/169] Translated using Weblate (Hebrew) Currently translated at 30.9% (329 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/he/ --- src/main/res/values-iw/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 94e362a8cdb397f0091fb3c53ce392d35b7b581f..54b8e68b8fa91b3f8408bc1068aba468864edf73 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -52,7 +52,7 @@ נקה היסטוריה נקה היסטוריית שיחה שלח הודעה בלתי מוצפנת - שלח ללא הצפנה + שלח טקסט נקי פענוח נכשל. אולי אין לך את המפתח הפרטי המתאים. OpenKeychain התחל מחדש @@ -341,4 +341,6 @@ הסתר את תוכן האפליקציה במחליף האפליקציות וחסום צילומי מסך OpenKeychain כדי להצפין ולפענח הודעות ולנהל את המפתחות הציבוריים שלך.

הוא מורשה תחת GPLv3+ וזמין ב-F-Droid וב-Google Play.

(אנא הפעל מחדש את %1$s לאחר מכן.)]]>
האם אתה בטוח שברצונך למחוק את הקובץ הזה?\n\nאזהרה: פעולה זו לא תמחק עותקים של קובץ זה המאוחסנים במכשירים או שרתים אחרים. + רינגטון לשיחות נכנסות + תקופת החסד From 7f7738d3bdfe387004a048be731bbf810491e983 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 24 Mar 2025 12:01:01 +0000 Subject: [PATCH 054/169] Translated using Weblate (Hebrew) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/he/ --- src/conversations/res/values-iw/strings.xml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-iw/strings.xml b/src/conversations/res/values-iw/strings.xml index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..b7c1d57121057192d39e340d42aba8a17e7aeb1f 100644 --- a/src/conversations/res/values-iw/strings.xml +++ b/src/conversations/res/values-iw/strings.xml @@ -1,2 +1,16 @@ - \ No newline at end of file + + בחר את ספק ה-XMPP שלך + השתמש ב-conversations.im + צור חשבון חדש + האם כבר יש לך חשבון XMPP? זה עשוי להיות המקרה אם אתה כבר משתמש בלקוח XMPP אחר או שהשתמשת ברפליקציה זו בעבר. אם לא, אתה יכול ליצור חשבון XMPP חדש כבר עכשיו.\nטיפ: ספקי דוא\"ל מסוימים מספקים גם חשבונות XMPP. + XMPP היא רשת הודעות מיידיות שאינן תלויות בספק. אתה יכול להשתמש באפליקציה זו עם כל שרת XMPP שתבחר.\nעם זאת, לנוחיותך הקלנו ליצור חשבון ב-conversations.im; ספק המתאים במיוחד לשימוש עם שיחות. + הוזמנת ל-%1$s. אנו נדריך אותך בתהליך יצירת החשבון.\nבעת בחירת %1$s כספק, תוכל לתקשר עם משתמשים של ספקים אחרים על ידי מתן כתובת ה-XMPP המלאה שלך. + הוזמנת ל-%1$s. שם משתמש כבר נבחר עבורך. אנו נדריך אותך בתהליך יצירת החשבון.\nתוכל לתקשר עם משתמשים של ספקים אחרים על ידי מתן כתובת ה-XMPP המלאה שלך. + הזמנת השרת שלך + קוד הקצאה בפורמט שגוי + הקש על כפתור השיתוף כדי לשלוח לאיש הקשר שלך הזמנה אל %1$s. + אם איש הקשר שלך נמצא בקרבת מקום, הוא יכול גם לסרוק את הקוד למטה כדי לקבל הזמנה. + הצטרף ל-%1$s ושוחח איתי: %2$s + שתף הזמנה באמצעות… + From 380c4cc36848bf3b0a20f747170709e59deb4d4a Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 24 Mar 2025 12:06:14 +0000 Subject: [PATCH 055/169] Translated using Weblate (Hebrew) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/he/ --- src/quicksy/res/values-iw/strings.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/quicksy/res/values-iw/strings.xml b/src/quicksy/res/values-iw/strings.xml index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..7440fe9a6c5920f2c7cb9142ec82b592542e21c8 100644 --- a/src/quicksy/res/values-iw/strings.xml +++ b/src/quicksy/res/values-iw/strings.xml @@ -1,2 +1,12 @@ - \ No newline at end of file + + משך הזמן ש-Quicksy שומרת על שקט לאחר פעילות במכשיר אחר + על ידי שליחת יומן קריסות אתה עוזר לפיתוח המתמשך של Quicksy + הודע לכל אנשי הקשר שלך כשאתה משתמש ב-Quicksy + כדי להמשיך לקבל התראות, גם כשהמסך כבוי, עליך להוסיף את Quicksy לרשימת האפליקציות המוגנות. + תמונת פרופיל של Quicksy + Quicksy אינו זמין במדינה שלך. + לא ניתן לאמת את זהות השרת. + שגיאת אבטחה לא ידועה. + פסק זמן בזמן ההתחברות לשרת. + From ff86bafdab7a39f4d3894642abda8276bfbab38b Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:39:54 +0000 Subject: [PATCH 056/169] Translated using Weblate (Bulgarian) Currently translated at 81.4% (865 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/bg/ --- src/main/res/values-bg/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index 6e126a864819f1218345d3e9c45744c2fd9c792d..dd1e5e672c41f16a2323027a671e378e4e26e26b 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -936,4 +936,4 @@ Създаването на резервно копие е стартирано. Ще получите известие, когато приключи. Видеото не може да бъде включено. Обикновен текстов документ - \ No newline at end of file + From 07f246625e3ec8ffa183a0b3c73a3f5e00667d85 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:39:56 +0000 Subject: [PATCH 057/169] Translated using Weblate (Catalan) Currently translated at 79.0% (840 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ca/ --- src/main/res/values-ca/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ca/strings.xml b/src/main/res/values-ca/strings.xml index ac107b9f6a8cf78240886fbce347ef2640ebb65f..ab12fc30b4be1d42f36aafcc181fdf49fd1c0fb0 100644 --- a/src/main/res/values-ca/strings.xml +++ b/src/main/res/values-ca/strings.xml @@ -914,4 +914,4 @@ No es pot processar la invitació El servidor no admet la generació d\'invitacions Cap compte actiu admet aquesta funció - \ No newline at end of file + From 2f10a1b828d8e637123006bef6085a9de50c2e04 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:39:57 +0000 Subject: [PATCH 058/169] Translated using Weblate (Czech) Currently translated at 96.1% (1021 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/cs/ --- src/main/res/values-cs/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 86788f029d7ac3d92d723bebfd58523e0a658faf..e2e8b73d77324d353107922fcddcb3ca9a314795 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -1132,4 +1132,4 @@ Integrace hovorů Hovory z této aplikace interagují s běžnými telefonními hovory, například ukončení jednoho hovoru, když začne další. Chcete smazat svůj avatar? Některé aplikace mohou i nadále zobrazovat uloženou kopii vašeho avataru. - \ No newline at end of file + From 939fef900b8ae20c0f29a8eab4e7a10913690ccd Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:00 +0000 Subject: [PATCH 059/169] Translated using Weblate (Greek) Currently translated at 82.4% (876 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/el/ --- src/main/res/values-el/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-el/strings.xml b/src/main/res/values-el/strings.xml index 8902f9c86ea1f2e4a172659d853e22082303cd72..ab1e8a8dfe3fd9f0fe94fe023b860ac8dfdacf5e 100644 --- a/src/main/res/values-el/strings.xml +++ b/src/main/res/values-el/strings.xml @@ -944,4 +944,4 @@ Έγγραφο απλού κειμένου Δεν υποστηρίζονται εγγραφές λογαριασμών Δεν βρέθηκε διεύθυνση XMPP - \ No newline at end of file + From 4adba73832790a0ba3f604512823893d58b8749e Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:39:58 +0000 Subject: [PATCH 060/169] Translated using Weblate (Danish) Currently translated at 85.2% (905 of 1062 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/da/ --- src/main/res/values-da-rDK/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-da-rDK/strings.xml b/src/main/res/values-da-rDK/strings.xml index b77a551f75f1d91c6da9adb4c3bdc1d94aae06a9..5232ee3c5b92c0033fcc97e07b5033a9a6f969e1 100644 --- a/src/main/res/values-da-rDK/strings.xml +++ b/src/main/res/values-da-rDK/strings.xml @@ -984,4 +984,4 @@ Udgående opkald (%s) · %s Indkommende opkald (%s) · %s Fjern konto fra server - \ No newline at end of file + From b66a67932dddc0cd89e9661382dd55897495e719 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:03 +0000 Subject: [PATCH 061/169] Translated using Weblate (Basque) Currently translated at 64.0% (682 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/eu/ --- src/main/res/values-eu/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index b9d3831c780dcfd8b03d527b4b21205e2f2182ab..b54edfe433fb10aabe1695b18e9b64386303610c 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -732,4 +732,4 @@ Parte-hartzaile %1$d ikusi %1$d parte-hartzaile ikusi - \ No newline at end of file + From 8f14673a6113e5f996e5a1874f3303332db02575 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:08 +0000 Subject: [PATCH 062/169] Translated using Weblate (Finnish) Currently translated at 99.0% (1054 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/fi/ --- src/main/res/values-fi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-fi/strings.xml b/src/main/res/values-fi/strings.xml index 9ad951c5bfe8cbec95ac6600e7e79981a75d5b24..3e41feb13a62f8a12d2ec2f663f68ae7ecf821ec 100644 --- a/src/main/res/values-fi/strings.xml +++ b/src/main/res/values-fi/strings.xml @@ -1102,4 +1102,4 @@ Pikakeskustelukuplat Näytä avatarit Pikakeskustelukuplat - \ No newline at end of file + From 0109d4fcff0c7bb850c0ea559d33505a6764e41c Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:16 +0000 Subject: [PATCH 063/169] Translated using Weblate (Japanese) Currently translated at 99.1% (1055 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 78a55790441d2fd259068034ba1bd1eb0ea37e51..859fb10b227cf96a82710039fedd6f51e5a2c203 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -1092,4 +1092,4 @@ 背景色、文字サイズ、プロフィール画像など ふきだし 接続タイムアウト - \ No newline at end of file + From f18c8e9fa0c6732ba087c1100779f7b57097ee9b Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:19 +0000 Subject: [PATCH 064/169] Translated using Weblate (Dutch) Currently translated at 99.5% (1059 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/nl/ --- src/main/res/values-nl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 78066be4bb75f711dfd0a8ea1e26e168ebc165a8..92dd83d18b7d66f4ec264bc5439b035afe51919a 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -1127,4 +1127,4 @@ Time-out voor verbinding Opnieuw proberen met P2P Kanaalbinding onbeschikbaar - \ No newline at end of file + From 13aa2feb85823eace60efef0cf7bb4bccdeb727b Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:31 +0000 Subject: [PATCH 065/169] Translated using Weblate (Swedish) Currently translated at 89.7% (955 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sv/ --- src/main/res/values-sv/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 216bacd00de4309d7f1b5eb57576c7d1e21ffb30..37f6679dfd42024793c111baa836e31477e80fd0 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -1046,4 +1046,4 @@ Skulle du vilja ta bort bokmärket för %s och arkivera chatten? Säkerhet Ta bort chatten i efterhand - \ No newline at end of file + From f073d113252ae4583d14596fa60b868e0da88b83 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:35 +0000 Subject: [PATCH 066/169] Translated using Weblate (Vietnamese) Currently translated at 85.1% (906 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/vi/ --- src/main/res/values-vi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-vi/strings.xml b/src/main/res/values-vi/strings.xml index 72e7b808a9dec213fafa2d83dd9681aece518929..e1ed0dd6ebb6e43dee202e94bbaef37a28b0514e 100644 --- a/src/main/res/values-vi/strings.xml +++ b/src/main/res/values-vi/strings.xml @@ -975,4 +975,4 @@ Từ chối yêu cầu chuyển sang video Xóa tài khoản khỏi máy chủ Không thể xóa tài khoản khỏi máy chủ - \ No newline at end of file + From 1ee0c71c5d19eab372e81e81dd3042d3d67577bb Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:32 +0000 Subject: [PATCH 067/169] Translated using Weblate (Silesian) Currently translated at 82.1% (874 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/szl/ --- src/main/res/values-szl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-szl/strings.xml b/src/main/res/values-szl/strings.xml index 8bb5d851c860773aae06d2400f363de648e7fa42..90786334e60d26033d9f645d930614c642eb64c4 100644 --- a/src/main/res/values-szl/strings.xml +++ b/src/main/res/values-szl/strings.xml @@ -981,4 +981,4 @@ Dokumynt ze samym tekstym Registracyjo kōnt niy je spiyrano Żodno adresa XMPP niyznojdziōno - \ No newline at end of file + From 048163339d2a8a1db2cd67101b5440faa205f013 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Wed, 26 Mar 2025 12:40:33 +0000 Subject: [PATCH 068/169] Translated using Weblate (Turkish) Currently translated at 94.4% (1005 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/tr/ --- src/main/res/values-tr-rTR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index a00a9f776f1c5de7e89f0273f827ba7b13e1089d..96b15cb3be6fb7e97742c3c82b1ca559cdccb758 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -1068,4 +1068,4 @@ Yabancılardan gelen davetler Yabancılardan gelen grup davetlerini kabul et Yapılandırmayı değiştir - \ No newline at end of file + From be1423d6edaa2fca638e4c2d3c8e0fa28bd565ae Mon Sep 17 00:00:00 2001 From: SomeTr Date: Wed, 26 Mar 2025 12:59:34 +0000 Subject: [PATCH 069/169] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 861f3996018e50971eb23a054e98c0b21e68f64c..d99c72bc6c788232a79a05f9f7ca8b12078d1112 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -813,7 +813,7 @@ Відновити з резервної копії Відновити Введіть пароль до облікового запису %s, щоб відновити з резервної копії. - Не використовуйте відновлення з резервної копії з метою клонування застосунку (запускати одночасно ще один примірник). Відновлення з резервної копії призначене виключно для перенесення даних або на випадок втрати оригінального пристрою. + Не відновлюйте ключі OMEMO з метою клонування застосунку (запускати одночасно ще один примірник). Відновлення ключів OMEMO призначене виключно для перенесення даних або на випадок втрати оригінального пристрою. Неможливо відновити з резервної копії. Не вдалося розшифрувати резервну копію. Чи правильний пароль? Створити або відновити резервну копію @@ -1035,7 +1035,7 @@ Не знайдено застосунку Дистриб\'ютор UnifiedPush Додати ще пісні\? - Не намагайтеся відновити резервні копії, які створили не Ви! + Відновлюйте лише ті резервні копії, які Ви створили особисто. Ви намагаєтеся імпортувати файл резервної копії у застарілому форматі аудіокнига Відновити з\'єднання на іншому вузлі @@ -1160,4 +1160,6 @@ Повторити спробу з P2P Прив\'язка каналу недоступна документ Word + Відновити ключі OMEMO + Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im From 8c346ff87665cbc06dbac81ae5c690c69d1f4f1d Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Wed, 26 Mar 2025 12:55:02 +0000 Subject: [PATCH 070/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index f6507ca959f57c3bae68dcbef53a9799dcccebc1..10e4003f1cf3124d9e71c7ed9eaf5fb537ef5f3d 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -838,7 +838,7 @@ Restaurează o copie de siguranță Restaurează Introduceți parola contului %s pentru a restaura copia de siguranță. - Nu folosiți funcția de restaurare a copiei de siguranță pentru a încerca clonarea (rularea simultană a) instalării. Restaurarea copiei de siguranță este gândită doar pentru a migra pe un alt dispozitiv sau în cazul în care ați pierdut dispozitivul original. + Nu restaurați cheile OMEMO în încercarea de a clona (rula simultan) aplicația. Restaurarea cheilor OMEMO este destinată doar migrărilor sau în cazul în care ați pierdut dispozitivul original. Nu s-a putut restaura copia de siguranță. Nu s-a putut decripta copia de siguranță. Este parola corectă? Copie de siguranță & Restaurare @@ -1006,7 +1006,7 @@ Șterge contul de pe server Discuții de grup Caută discuții de grup - Nu încercați să restaurați copii de rezervă pe care nu le-ați creat personal! + Restaurați numai copii de rezervă pe care le-ați creat personal. Încercați să importați un fișier copie de rezervă format vechi Carte audio Reconectat pe altă gazdă @@ -1131,4 +1131,6 @@ Reîncearcă cu P2P Channel binding indisponibil Document Word + Quicksy poate restaura doar copiile de rezervă pentru conturile quicksy.im + Restaurare chei OMEMO From e17db838585ba0651705f652ed13698076577470 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Wed, 26 Mar 2025 13:07:52 +0000 Subject: [PATCH 071/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 99.6% (1060 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 77 +++++++++++++------------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index e9250d775b75d5b4df6c2a4ac055de511e358a3f..4d1f044a7e28ecbf96079bb9a8f722975e85629a 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -108,8 +108,8 @@ \n \n请通知对方设置 OpenPGP。
常规 - 接收文件 - 自动接收小于此大小的文件… + 接受文件 + 自动接受小于此大小的文件… 附件 通知 振动 @@ -182,7 +182,7 @@ \n您的联系人将无法再向您发送 OpenPGP 加密消息。 OpenPGP 公钥已发布。 启用账号 - 是否确定要删除账号?删除账号会清除全部聊天记录 + 是否确定要删除账号?删除账号会清空全部聊天记录 录制语音 XMPP 地址 屏蔽 XMPP 地址 @@ -320,9 +320,9 @@ 复制原始 URL 再次发送 文件 URL - 已复制 URL 到剪贴板 - 已复制 XMPP 地址到剪贴板 - 已复制出错信息到剪贴板 + URL 已复制到剪贴板 + XMPP 地址已复制到剪贴板 + 错误消息已复制到剪贴板 网址 扫描二维码 显示二维码 @@ -365,7 +365,7 @@ 复制 OMEMO 指纹到剪贴板 重新生成 OMEMO 密钥 清除设备 - 是否确定要从 OMEMO 公布中清除所有其他设备?下次连接时,设备将会重新公布,但可能不会收到在此期间发送的消息。 + 是否确定要从 OMEMO 公布中清除所有其他设备?下次连接时,设备将会重新公布,但可能无法接收在此期间发送的消息。 此联系人没有可用密钥。 \n无法从服务器获取新密钥。也许是对方的服务器有问题? 此联系人没有可用密钥。 \n请确保双方都有在线状态订阅。 出了点问题 @@ -381,7 +381,7 @@ 启用所有账号 禁用所有账号 执行操作 - + 访客 离线 被驱逐者 成员 @@ -442,7 +442,7 @@ 位置 离开私人群聊 离开公开频道 - 不信任系统 CA + 不要信任系统 CA 所有证书必须手动批准 移除证书 删除手动批准的证书 @@ -470,7 +470,7 @@ 下载失败:文件无效 无法连接到 Tor 网络 绑定失败 - 域名未响应 + 不对域名负责 损坏 在线状态 设备锁定时离开 @@ -535,7 +535,7 @@ 更正消息 发送更正后的消息 您已信任此人的指纹。选择“完成”即表示您确认 %s 是此群聊的成员。 - 您禁用了此账号 + 您已禁用此账号 安全错误:文件访问无效! 未找到可以分享 URI 的应用 分享 URI… @@ -546,14 +546,14 @@ 使用我自己的提供者 选择您的用户名 手动更改在线状态 - 在编辑状态信息时,让您的联系人知道您是否可以聊天。 - 状态信息 + 在编辑状态消息时设置您的在线状态。 + 状态消息 有空聊天 在线 离开 没空 忙碌 - 安全密码已生成 + 已生成安全密码 您的设备不支持选择退出电池优化 注册失败:请稍后重试 注册失败:密码太弱 @@ -593,10 +593,10 @@ 重新生成 OMEMO 密钥。您的所有联系人将必须再次验证您。仅将此作为最后的方法。 删除所选密钥 连接后才能发布头像。 - 显示出错信息 - 出错信息 + 显示错误消息 + 错误消息 流量节省程序已启用 - 操作系统正限制 %1$s 在后台时访问互联网。要接收新消息通知,应当在“流量节省程序”开启时允许 %1$s 无限制访问。 \n在可能的情况下,%1$s 仍会尽可能节省数据。 + 您的操作系统正在限制 %1$s 在后台时访问互联网。要接收新消息通知,应当在“流量节省程序”开启时允许 %1$s 无限制访问。\n%1$s 仍会在可能的情况下尽可能节省数据。 设备不支持为 %1$s 禁用流量节省程序。 无法创建临时文件 此设备已经过验证 @@ -609,22 +609,22 @@ 以 XMPP URI 形式分享 以 HTTP 链接形式分享 验证前盲目信任 - 信任未经验证的联系人的新设备,但提示手动确认已验证的联系人的新设备。 - 盲目信任的 OMEMO 密钥,这意味着它们可能是别人的,可能会有人窃听。 + 自动信任未经验证的联系人的新设备,但已验证的联系人的新设备需手动确认。 + 盲目信任的 OMEMO 密钥,意味着存在密钥冒用或第三方窃听的风险。 未受信任 二维码无效 清理缓存文件夹(由相机使用) 清理缓存 清理私人存储空间 清理保存文件的私人存储(可从服务器重新下载) - 我从可信来源获得此链接 - 点击链接后,您将验证 %1$s 的 OMEMO 密钥。只有从可信来源(只有 %2$s 可以发布此链接)获得此链接才是安全的。 - 您将验证自己账号的 OMEMO 密钥。只有从可信来源(只有您可以发布此链接)获得此链接才是安全的。 + 我从可信来源访问此链接 + 点击链接后,您即将验证 %1$s 的 OMEMO 密钥。只有从可信来源(只有 %2$s 可以发布此链接)访问此链接才是安全的。 + 您即将验证自己账号的 OMEMO 密钥。只有从可信来源(只有您可以发布此链接)访问此链接才是安全的。 继续 验证 OMEMO 密钥 显示非活动设备 隐藏非活动设备 - 不再信任设备 + 不信任设备 是否确定要移除此设备的验证?\n此设备及其消息将标记为“未受信任”。 %d 秒 @@ -685,24 +685,23 @@ 需要访问相机来扫描二维码 滚动至底部 发送消息后向下滚屏 - 编辑状态信息 - 编辑状态信息 + 编辑状态消息 + 编辑状态消息 禁用加密 - %1$s 无法向 %2$s 发送加密消息。可能是由于您的联系人使用了无法处理 OMEMO 的过时服务器或客户端。 + %1$s 无法向 %2$s 发送加密消息。可能是由于对方使用了无法处理 OMEMO 的过时服务器或客户端。 无法获取设备列表 无法获取密钥 提示:某些情况下,双方可以添加到联系人列表解决此问题。 - 是否确定要禁用此对话的 OMEMO 加密? -\n将允许服务器管理员读取您的消息,但可能是与使用过时客户端的用户交流的唯一方法。 + 是否确定要禁用此对话的 OMEMO 加密?\n这将允许服务器管理员读取您的消息,但可能是与使用过时客户端的用户交流的唯一方式。 立即禁用 草稿: OMEMO 加密 OMEMO 将始终用于一对一聊天和私人群聊。 新对话将默认使用 OMEMO。 - 新对话必须明确开启 OMEMO。 + 新对话必须手动启用 OMEMO。 创建快捷方式 - 默认开启 - 默认关闭 + 默认启用 + 默认禁用 未对此设备加密消息。 无法解密 OMEMO 消息。 撤销 @@ -850,8 +849,8 @@ 管理员可以编辑话题。 所有者可以邀请他人。 参与者可以邀请他人。 - 管理员可以看到用户 XMPP 地址。 - 任何参与者可以看到用户 XMPP 地址。 + 管理员可以查看用户 XMPP 地址。 + 任何参与者可以查看用户 XMPP 地址。 此公开频道无参与者。邀请联系人或使用分享按钮分发频道的 XMPP 地址。 此私人群聊无参与者。 管理权限 @@ -951,7 +950,7 @@ 查看 %1$d 位参与者 - 一些消息发送失败 + 部分消息无法成功发送 发送失败 更多选项 @@ -987,10 +986,10 @@ 您正尝试导入过时的备份文件格式 有声读物 在其他主机上重新连接 - 您登出了此账号 - 登入 + 您已登出此账号 + 登录 隐藏通知 - 您的联系人使用未经验证的设备。扫描对方二维码进行验证并阻止主动式中间人攻击。 + 对方使用未经验证的设备。扫描其二维码进行验证并阻止主动式中间人攻击。 登出 已登出 您正在使用未经验证的设备。扫描您其他设备的二维码进行验证并阻止主动式中间人攻击。 @@ -1062,7 +1061,7 @@ 创建一次性备份 定期备份 全屏通知 - 当设备锁定时,允许此应用显示占据全屏的来电通知。 + 允许此应用在设备锁定时显示占据整个屏幕的来电通知。 不支持的操作 创建一次性备份、设置定期备份 允许私信 @@ -1090,7 +1089,7 @@ 更多回应 添加回应 无法修改通话 - 您的联系人的 XMPP 客户端可能不支持音频/视频通话。 + 对方的 XMPP 客户端可能不支持音频/视频通话。 显示头像 背景颜色、字体大小、头像 消息气泡 From 8632cc260247ff49aa2afa6e6359bb8a1b6a888e Mon Sep 17 00:00:00 2001 From: butterflyoffire Date: Wed, 26 Mar 2025 14:19:05 +0000 Subject: [PATCH 072/169] Added translation using Weblate (Kabyle) --- src/conversations/res/values-kab/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/conversations/res/values-kab/strings.xml diff --git a/src/conversations/res/values-kab/strings.xml b/src/conversations/res/values-kab/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/conversations/res/values-kab/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From b8d2721f9e78f2bb1066f2856b1ab2108492aaed Mon Sep 17 00:00:00 2001 From: butterflyoffire Date: Wed, 26 Mar 2025 14:19:28 +0000 Subject: [PATCH 073/169] Translated using Weblate (Kabyle) Currently translated at 15.3% (2 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/kab/ --- src/conversations/res/values-kab/strings.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/conversations/res/values-kab/strings.xml b/src/conversations/res/values-kab/strings.xml index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..4702267cebefbcbbb6de500c04a46c538f0f3197 100644 --- a/src/conversations/res/values-kab/strings.xml +++ b/src/conversations/res/values-kab/strings.xml @@ -1,2 +1,5 @@ - \ No newline at end of file + + Seqdec conversations.im + Snulfu-d amiḍan amaynut + From 76373619ba691faedfaea6b02e70fbca3db07778 Mon Sep 17 00:00:00 2001 From: butterflyoffire Date: Wed, 26 Mar 2025 14:20:20 +0000 Subject: [PATCH 074/169] Added translation using Weblate (Kabyle) --- src/main/res/values-kab/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/main/res/values-kab/strings.xml diff --git a/src/main/res/values-kab/strings.xml b/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/main/res/values-kab/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From d40967a46a90b1e4babfd6e76099e965bb6b6c01 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Wed, 26 Mar 2025 14:39:01 +0000 Subject: [PATCH 075/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 4d1f044a7e28ecbf96079bb9a8f722975e85629a..89c137ecf89121b3705d5488d4ef14fa715f1e1c 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -824,7 +824,7 @@ 恢复备份 恢复 请输入 %s 的密码以恢复备份。 - 请勿使用恢复备份功能尝试克隆(同时运行)安装。恢复备份仅适用于迁移或您丢失原始设备的情况。 + 请勿通过恢复 OMEMO 密钥尝试克隆(同时运行)安装。恢复 OMEMO 密钥仅适用于迁移或您丢失原始设备的情况。 无法恢复备份。 无法解密备份。密码是否正确? 备份和恢复 @@ -982,7 +982,7 @@ 群聊 搜索群聊 从服务器移除账号 - 请勿尝试恢复您尚未自行创建的备份! + 仅恢复您亲自创建的备份。 您正尝试导入过时的备份文件格式 有声读物 在其他主机上重新连接 @@ -1107,4 +1107,6 @@ 使用 P2P 重试 不支持通道绑定 Word 文档 + 恢复 OMEMO 密钥 + Quicksy 只能恢复 quicksy.im 账号的备份 From 5d807800e4feca8c3b5bb0458b1f60aa1a9cf3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Wed, 26 Mar 2025 14:21:24 +0000 Subject: [PATCH 076/169] Translated using Weblate (Estonian) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/et/ --- src/main/res/values-et/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 27b1bf45802c8e70fc288ce524ca8d4b075a0849..fbfdc7e2568b1637d3a88df300213155e32b1098 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -889,8 +889,8 @@ Palun oota %s ja proovi uuesti Sinu tehtavatele päringutele kehtib hetkel ajaühikuline piirang Liiga palju päringuid - Ära kasuta varukoopiat olemasoleva paigalduse kloonimiseks (samaaegseks käivitamiseks). Varukoopiast taastamine on mõeldud vaid teise seadmesse kolimise jaoks ning juhuks, kui kaotad oma algse nutiseadme. - Palun ära kasuta varukoopiaid, mida sa pole ise teinud! + Ära kasuta OMEMO võtmete taastamist olemasoleva paigalduse kloonimiseks (samaaegseks käivitamiseks). OMEMO võtmete taastamine on mõeldud vaid teise seadmesse kolimise jaoks ning juhuks, kui kaotad oma algse nutiseadme. + Palun taasta varukoopiatest, mille sa oled ise teinud. Varukoopiast taastamine ei õnnestunud. Kohalik server Enamus kasutajaid peaksid eelistama valikut „jabber.network“. See tagab asjakohasemad soovitused kogu avalikust XMPP võrgustikust. @@ -1132,4 +1132,6 @@ Proovi uuesti võrdõigusvõrguga Edastuskanaliga sidumine pole võimalik Wordi-dokument + Taasta OMEMO võtmed + Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid From 53186ff20579e0e1e4657c676648c01dc93d5c02 Mon Sep 17 00:00:00 2001 From: butterflyoffire Date: Wed, 26 Mar 2025 14:28:23 +0000 Subject: [PATCH 077/169] Translated using Weblate (Kabyle) Currently translated at 21.8% (233 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/kab/ --- src/main/res/values-kab/strings.xml | 234 +++++++++++++++++++++++++++- 1 file changed, 233 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-kab/strings.xml b/src/main/res/values-kab/strings.xml index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..a89a0bfb67f191c98ea9f6cf621ce5c5e4427f01 100644 --- a/src/main/res/values-kab/strings.xml +++ b/src/main/res/values-kab/strings.xml @@ -1,2 +1,234 @@ - \ No newline at end of file + + xmpp.example.com + Ddu + Immed + Snulfu-d + OpenKeychain + Yal tikkelt + Meferisem + Asnas + Asefrek n imiḍan + URL n ufaylu + Aleqqem… + Ssens-it + Uqqin + Ur yestuffi ara + Iɣewwaṛen + Sefrek amiḍan + Sefrek imiḍan + Talqayt unermis + Rnu amiḍan + Snifel isem + Talqayt n wegraw n usqerdec + Bḍu akked … + Fren anermis + Fren inermisen + Asqerdec amaynut + tura yakan + Bḍu-t s umiḍan + Tuzzna… + Anebdal + Bab-is + Imḍebbar + Imttekki + Anermis + Semmet + Rnu + Ẓreg + Kkes + Sewḥel + Sekles + Ih + Bḍu akked … + Snubeg anermis + Azen-it tura + Seddu afaylu + Rnu anermis + Sfeḍ amazray + Kkes afaylu + Fren ibenk + Azen izen + Ales asenker + Sebded + Ṛǧu… + Amatu + Aṭenṭen + Iɣewwaren leqqayen + Qbel + Tuccḍa + Amiḍan-inek·inem + Fren tugna + Ṭṭef tawlaft + D arussin + Uqqin + Tuqqna… + Aruqqin + Ur yettusireg ara + Teffɣeḍ + Ulac tuqqna + OTR + OpenPGP + OMEMO + Kkes amiḍan + aseqdac@example.com + Awal n uɛeddi + Sermed amiḍan-a + Tansa XMPP + XEP-0313: MAM + Ibenkan niḍen + Kkes anermis + Sewḥel anermis + Fren + Ttekki + Asentel + Suffeɣ + Uḍfir + Zgel + Sermed + i %s + Zgel + Taɣellist + Wiyyaḍ + Sekcem awal n uɛeddi + Suter tura + Iɣewwaṛen leqqayen + Ɣef %s + Sentem + tansa web + Talqayt n umiḍan + Ɛreḍ tikkelt nniḍen + afaylu + Fren afaylu + Zḍem-d %S + Kkes %s + Ldi %s + Afaylu yettwakkes + Kkes ibenkan + Awal uffir yettusnifel! + Snifel awal n uɛeddi + Awal n uɛeddi amiran + Aruqqin + Aεeggal + Werǧin + Askar alqayan + Err + ameslaw + tavidyutt + tugna + isemli PDF + Anermis + Isemli Word + Asnas Android + Tuzzna n %s + Ula d yiwen + Tigawt taruradt + Isem n useqdac + Isem n useqdac + Nadi inermisen + Tuqqna + Asenneftaɣ + Tawwurt + Seɣti izen + Ulac-it + Snulfu-d amiḍan + Ssens-it + Tabaḍnit + Asentel + Awurman + Aceεlal + Ubrik + Taṭablit + Tadiwent + Nekk + Sireg + Iminig web + Izen n tuccḍa + Ass-a + Iḍelli + Izen + Ldi asmel web + Sekles tavidyutt + Tikkelt + Arewway: + Bḍu + GIF + Isem + Rnu anegzum + Nɣel adeg + Bḍu adeg + Bḍu adeg + Sken-d adeg + Ttxil rǧu… + Nadi deg iznan + Isem n unermis + Iznan + Iznan + Imttekkiyen + Yettwasefsex + Taɣara n tvidyutt + D talemmast (360p) + Uɣal + Ih + Uhu + Uṭṭun n tiliɣri + Nadi timura + Asenqed iteddu… + Isem-ik·im + Sekker Orbot + Ldi s… + Fren amiḍan + Tansa XMPP + Nadi imttekkiyen + Tadyant + jabber.network + Aqeddac adigan + Yettṣuni… + Ur yestuffi ara + Tallalt + Asiwel iteddu + Asiwel i ikcem-d + Asiwel s uvidyu + Ffeɣ + Asqerdec-a + Amiḍan XMPP + Agi + Aqeddac Push + Senser + Qqen + Bdu asqerdec + Awgelhen seg yixef ɣer wayeḍ + Anagraw n wammud + Tamyigawt + Ɣef yibenk + Inermisen + Nadi + Aselkim + Yetteqqen + D uqqin + Agrudem + Qqen + semmet + Aleqqem + e-book + Seddu + Err-it-id + Ɣef + Agi + Udem + azen-it tikkelt-nniḍen + Sebded Orbot + Siwel + Asiwel i ikcem-d + Ugar n yiɣewwaṛen + Tasertit n tbaḍnit + Azdam awurman + Tuqqna ɣer uqeddac + Taɣellist + Tuzna + Anasiw + Rnu anermis + Awal n uɛeddi amaynut + Wali asqerdec + Rnu-t akken yebɣu yili + From 33077ab0ab5efa39eb7b03c6a373691b04a10c0b Mon Sep 17 00:00:00 2001 From: butterflyoffire Date: Wed, 26 Mar 2025 14:57:58 +0000 Subject: [PATCH 078/169] Added translation using Weblate (Kabyle) --- src/quicksy/res/values-kab/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/quicksy/res/values-kab/strings.xml diff --git a/src/quicksy/res/values-kab/strings.xml b/src/quicksy/res/values-kab/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a6b3daec9354f9ae75cdf8d94a67446c6227dd96 --- /dev/null +++ b/src/quicksy/res/values-kab/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From e89638001d2ab689dd531b60f59e8e7ab4e9d087 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Wed, 26 Mar 2025 16:50:37 +0000 Subject: [PATCH 079/169] Translated using Weblate (Albanian) Currently translated at 99.0% (1054 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index dd78479be0e04d293014df66b01672ef6bd365ce..c5a81a977a69994deeb7eb9ec61daa7a7337d282 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -953,7 +953,7 @@
%s

Dakord, apo do të donit të përpunonit numrin??]]>
%s.]]> Që të rikthehet kopjeruajtja, jepni fjalëkalimin tuaj për llogarinë %s. - Mos përdorni veçorinë e rikthimit të një kopjeruajtje në një përpjekje për të klonuar (xhiruar në të njëjtën kohë) një instalim. Rikthimi i një kopjeruajtje është menduar vetëm për migrime, ose në rast se humbët pajisjen origjinale. + Mos riktheni kyçe OMEMO, në një përpjekje për të klonuar (xhiruar në të njëjtën kohë) një instalim. Rikthimi i kyçeve OMEMO është menduar vetëm për migrime, ose në rast se humbët pajisjen origjinale. Ky kanal publik s’ka pjesëmarrës. Ftoni kontaktet tuaj, ose përdorni butonin e ndarjes me të tjerët për të dhënë adresën XMPP të tij. Aplikacioni dhënës nuk akordoi leje për hyrje në këtë kartelë. Shumica e përdoruesve duhet të zgjedhin ‘jabber.network’ për sugjerime më të mira nga krejt ekosistemi publik XMPP. @@ -968,7 +968,7 @@ %1$d thirrje të humbur prej %2$d kontakti %1$d thirrje të humbur prej %2$d kontaktesh - Mos u rrekni të riktheni kopjeruajtje që s’i keni krijuar ju vetë! + Riktheni vetëm kopjeruajtje që ’i keni krijuar ju vetë. Të shtohen pjesë shtesë? XEP-0280: Message Carbons Rilidhu te një tjetër strehë @@ -1122,4 +1122,6 @@ Mbarim kohe për lidhjen Riprovo me P2P Dokument Word + Rikthe kyçe OMEMO + Quicksy mund të rikthejë vetëm kopjeruajtje për llogari quicksy.im From ea03c6c36089a608c8e96c3d67fb83f6a1dfb3f6 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Wed, 26 Mar 2025 16:25:12 +0000 Subject: [PATCH 080/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 89c137ecf89121b3705d5488d4ef14fa715f1e1c..8cde6a33adde7044c57e25af89116911aaaf42c0 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -75,7 +75,7 @@ 正在准备发送图片 正在准备发送图片 正在分享文件。请稍候… - 清除历史记录 + 清除历史 清除聊天记录 是否要删除此对话中的所有消息? \n @@ -93,7 +93,7 @@ 发送未加密 解密失败。也许您没有合适的私钥。 OpenKeychain - OpenKeychain 来加密和解密消息并管理公钥。

它在 GPLv3+ 许可证下授权并可在 F-Droid 和 Google Play 上获得。

(请之后重启 %1$s。)]]>
+ OpenKeychain 来加密和解密消息并管理公钥。

它采用 GPLv3+ 许可,可在 F-Droid 和 Google Play 上获取。

(之后请重启 %1$s。)]]>
重启 安装 请安装 OpenKeychain @@ -288,7 +288,7 @@ \n请前往“联系人详情”以验证在线状态订阅。
安全 消息更正 - 允许您的联系人发送后编辑其消息 + 允许您的联系人重新编辑已发送的消息 专家设置 请谨慎设置 关于 %s @@ -540,7 +540,7 @@ 未找到可以分享 URI 的应用 分享 URI… 同意并继续 - 指导您在 conversations.im 上创建账号。\n选择 conversations.im 作为提供者时,向别人提供您的完整 XMPP 地址,就能和对方交流。 + conversations.im 账号创建引导流程。\n当选择 conversations.im 作为服务提供者时,您只需向其他服务提供者的用户提供您的完整 XMPP 地址,即可与对方互通消息。 您的完整 XMPP 地址将是:%s 创建账号 使用我自己的提供者 @@ -1094,15 +1094,15 @@ 背景颜色、字体大小、头像 消息气泡 消息气泡 - 在群聊和一对一聊天中为消息显示头像。 + 在群聊和一对一聊天中,在消息旁显示头像。 通话集成 自定义通知 是否为此对话启用自定义通知(重要程度、声音、振动)设置? 此应用的通话与常规通话交互,例如另一个通话开始时结束一个通话。 在左侧显示所有消息,包括发送的消息,以实现统一的聊天布局。 左对齐消息 - 是否删除头像?某些客户端可能会继续显示您头像的缓存副本。 - 仅显示给联系人 + 是否删除头像?部分客户端可能会继续显示已缓存的头像副本。 + 仅对联系人显示 连接超时 使用 P2P 重试 不支持通道绑定 From 23dfd952ded8710be0755308817975261c854004 Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Wed, 26 Mar 2025 15:39:04 +0000 Subject: [PATCH 081/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (13 of 13 strings) Translation: Conversations/Android App (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-conversations/zh_Hans/ --- src/conversations/res/values-zh-rCN/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/conversations/res/values-zh-rCN/strings.xml b/src/conversations/res/values-zh-rCN/strings.xml index 73198d777707c7291bef21e051400cdc42151ab6..c3dfcaaf10fb26587a44a8ae11131e11d763738d 100644 --- a/src/conversations/res/values-zh-rCN/strings.xml +++ b/src/conversations/res/values-zh-rCN/strings.xml @@ -3,14 +3,14 @@ 选择 XMPP 提供者 使用 conversations.im 创建新账号 - 您已经有 XMPP 账号了吗?如果之前使用过 Conversations 或其他 XMPP 客户端,那么已经有账号了。如果没有,现在可以创建账号。\n提示:一些邮件服务提供者也提供 XMPP 账号。 - XMPP 是独立于提供者的即时通讯网络。您选择的任何 XMPP 服务器都可以使用此应用。\n不过,您可以轻松地在 conversations.im 上创建账号,这是专门适用于 Conversations 的提供者。 - 您已受邀加入 %1$s。我们将指导您创建账号。\n选择 %1$s 作为提供者时,向别人提供您的完整 XMPP 地址,就能和对方交流。 - 您已受邀加入 %1$s。已为您选择了用户名。我们将指导您创建账号。\n向别人提供您的完整 XMPP 地址,就能和对方交流。 + 您是否已有 XMPP 账号?如果您已经在使用其他 XMPP 客户端或之前使用过 Conversations,可能已经有账号了。如果没有,您现在就可以创建新账号。\n提示:部分电子邮件服务提供者也会提供 XMPP 账号。 + XMPP 是独立于服务提供者的即时通讯网络。您可以选择任意 XMPP 服务器使用本应用。\n不过,为了方便起见,我们简化了在 conversations.im(专为 Conversations 优化的提供者)上创建账号的过程。 + 您已受邀加入 %1$s。我们将引导您完成创建账号的流程。\n当选择 %1$s 作为服务提供者时,您只需向其他服务提供者的用户提供您的完整 XMPP 地址,即可与对方互通消息。 + 您已受邀加入 %1$s。已为您选择了用户名。我们将引导您完成创建账号的流程。\n您只需向其他服务提供者的用户提供您的完整 XMPP 地址,即可与对方互通消息。 您的服务器邀请 配置代码格式不正确 点按分享按钮,向您的联系人发送 %1$s 的邀请。 如果联系人在附近,也可以扫描下方二维码接受邀请。 加入 %1$s 和我聊天:%2$s 分享邀请… - \ No newline at end of file + From dfb88cde5a25e034b89d178a5b68cf5a3e811411 Mon Sep 17 00:00:00 2001 From: butterflyoffire Date: Wed, 26 Mar 2025 14:58:24 +0000 Subject: [PATCH 082/169] Translated using Weblate (Kabyle) Currently translated at 33.3% (3 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/kab/ --- src/quicksy/res/values-kab/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/quicksy/res/values-kab/strings.xml b/src/quicksy/res/values-kab/strings.xml index a6b3daec9354f9ae75cdf8d94a67446c6227dd96..2c625e3012fc607e7a233b8302b596ae49dbb90e 100644 --- a/src/quicksy/res/values-kab/strings.xml +++ b/src/quicksy/res/values-kab/strings.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + Tugna n umaɣnu n Quicksy + Tuccḍa n tɣellist tarussint + Quicksy ur yelli ara deg tmurt-nnwen. + From 803421f0a4cf8abe350e2a22be68c245485857df Mon Sep 17 00:00:00 2001 From: nautilusx Date: Thu, 27 Mar 2025 06:34:04 +0000 Subject: [PATCH 083/169] Translated using Weblate (German) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 1b7830371c193a18867fe690c01a03a53a2db422..1d2423c06b8989c7bd84600eaa88c4f17f1ba43e 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -824,7 +824,7 @@ Sicherung wiederherstellen Wiederherstellung Gib dein Passwort für das Konto %s ein, um die Sicherung wiederherzustellen. - Benutze die Sicherungsfunktion nicht, um eine Installation zu klonen (gleichzeitig auszuführen). Die Wiederherstellung einer Sicherung ist nur für Migrationen oder für den Fall gedacht, dass du das ursprüngliche Gerät verloren hast. + Stelle die OMEMO-Schlüssel nicht wieder her, um eine Installation zu klonen (gleichzeitig auszuführen). Die Wiederherstellung von OMEMO-Schlüsseln ist nur für Migrationen oder für den Fall gedacht, dass du das ursprüngliche Gerät verloren hast. Sicherung konnte nicht wiederhergestellt werden. Sicherung konnte nicht entschlüsselt werden. Ist das Passwort korrekt? Sicherung & Wiederherstellung @@ -987,7 +987,7 @@ Konto konnte nicht vom Server gelöscht werden Gruppenchats Gruppenchats durchsuchen - Versuche nicht, Backups wiederherzustellen, die du nicht selbst erstellt hast! + Nur Sicherungen wiederherstellen, die du selbst erstellt hast. Du versuchst, ein veraltetes Sicherungsdateiformat zu importieren Hörbuch Verbindung auf anderem Host wiederherstellen @@ -1112,4 +1112,6 @@ Erneut mit P2P versuchen Kanalbindung nicht verfügbar Word-Dokument + OMEMO-Schlüssel wiederherstellen + Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen From 87a6ef8db16edf0b9a4b9d656e7926f3b238c3ec Mon Sep 17 00:00:00 2001 From: ghose Date: Thu, 27 Mar 2025 05:10:45 +0000 Subject: [PATCH 084/169] Translated using Weblate (Galician) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 47d5cd7810c498fd2ce7dafcfe44b1c632a27a42..df3ef3958306d0f43b4e0b77a4b5c24b9c3e3983 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -824,7 +824,7 @@ Restablecer copia de apoio Restablecer Escribe o contrasinal da conta %s para restablecer a copia. - Non utilices a función de restaurar a copia nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar unha copia só ten sentido para migrar ou en caso de perda do dispositivo orixinal. + Non restaures as claves OMEMO nun intento de clonar (utilizar simultaneamente) unha instalación. Restaurar as claves OMEMO só ten sentido para migrar ou en caso de perda do dispositivo orixinal. Non se puido restaurar a copia. Non se puido descifrar a copia. É correcto o contrasinal? Respaldar & Restaurar @@ -987,7 +987,7 @@ Non se puido eliminar a conta no servidor Buscar parolas en grupo Parolas en grupo - Non intentes restablecer unha copia de apoio que non tiveses creado ti! + Restaura só copias de apoio que crearas ti persoalmente. Estás intentando importar un ficheiro de apoio co formato antigo Audiolibro Volver conectar noutro servidor @@ -1112,4 +1112,6 @@ Reintentar con P2P Non está dispoñible a vinculación de canles Documento de Word + Restaurar claves OMEMO + Quicksy só pode restaurar copias de apoio de contas quicksy.im From a78c6c35e07846c509daa24979560188e1f27d34 Mon Sep 17 00:00:00 2001 From: random_r Date: Thu, 27 Mar 2025 09:45:33 +0000 Subject: [PATCH 085/169] Translated using Weblate (Italian) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 3bd44179ec94fcc7635668399b1fa4ebb27acc10..20b925442ab2de50c5341f62247d1a6b2b5993ef 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -833,7 +833,7 @@ Ripristina backup Ripristina Inserisci la tua password per il profilo %s per ripristinare il backup. - Non usare la funzione di ripristino del backup tentando di clonare (eseguire simultaneamente) un\'installazione. Il ripristino di un backup è inteso solo per migrazioni o in caso di smarrimento del dispositivo. + Non ripristinare le chiavi OMEMO nel tentativo di clonare (eseguire simultaneamente) un\'installazione. Il ripristino delle chiavi OMEMO è inteso solo per migrazioni o in caso di smarrimento del dispositivo. Impossibile ripristinare il backup. Impossibile decifrare il backup. La password è giusta? Backup e ripristino @@ -1001,7 +1001,7 @@ Impossibile eliminare il profilo dal server Chat di gruppo Cerca chat di gruppo - Non tentare di ripristinare dei backup che non hai creato te stesso! + Ripristina solo i backup che hai creato personalmente. Stai tentando di importare un formato di file di backup obsoleto Audiolibro Riconnetti su altro host @@ -1126,4 +1126,6 @@ Messaggi di chat Associazione dei canali non disponibile Documento Word + Ripristina chiavi OMEMO + Quicksy può ripristinare backup solo per profili quicksy.im From 64439de77ac15cfb37700e11c1ee07e0c4c89064 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 27 Mar 2025 05:39:04 +0000 Subject: [PATCH 086/169] Translated using Weblate (Russian) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 9d7b1ca73c4f7512a5b10a48ff5c531987b68793..82d96bd49d176c46dd24c2af21579916c55907b3 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -852,7 +852,7 @@ Восстановить из резервной копии Восстановить Введите пароль аккаунта %s для восстановления резервной копии. - Не используйте восстановление резервной копии для дублирования установленного приложения (одновременного исполнения). Восстановление резервной копии нужно лишь для того, чтобы перенести данные на другое устройство или на случай потери своего устройства. + Не используйте восстановление ключей OMEMO для дублирования установленного приложения (одновременного использования). Восстановление ключей OMEMO нужно только для переноса данных на другое устройство или на случай потери своего устройства. Невозможно восстановить резервную копию. Невозможно расшифровать резервную копию. Вы ввели верный пароль? Резервное копирование и восстановление @@ -1022,7 +1022,7 @@ Аккаунт для получения push-уведомлений Нет (неактивно) Вы собираетесь проверить ключи OMEMO своего аккаунта. Это безопасно только в том случае, если вы перешли по этой ссылке из надёжного источника, где только вы могли опубликовать эту ссылку. - Не пытайтесь восстановить резервные копии, которые не были созданы вами! + Восстанавливайте только те резервные копии, которые были созданы лично вами! %1$d пропущенный вызов от %2$s %1$d пропущенных вызова от %2$s @@ -1159,4 +1159,6 @@ Повторить через P2P Привязка канала недоступна Документ Word + Восстановить ключи OMEMO + Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im From 5f70eb2db66b33c8549b50f156f537d2c2539f08 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 27 Mar 2025 17:54:27 +0100 Subject: [PATCH 087/169] add 'copy fingerprint' in context menu of own key --- .../conversations/ui/EditAccountActivity.java | 7 +++++ .../siacs/conversations/ui/OmemoActivity.java | 28 ++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 14f73295716b1f14bdbf859f9112c7000f506abd..78b962be510c8535ca26ea6536fe12715a71ec26 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1336,6 +1336,13 @@ public class EditAccountActivity extends OmemoActivity this.mAccount.getAxolotlService().getOwnFingerprint(); if (ownAxolotlFingerprint != null && Config.supportOmemo()) { this.binding.axolotlFingerprintBox.setVisibility(View.VISIBLE); + this.binding.axolotlFingerprintBox.setOnCreateContextMenuListener( + (menu, v, menuInfo) -> { + getMenuInflater().inflate(R.menu.omemo_key_context, menu); + menu.findItem(R.id.verify_scan).setVisible(false); + menu.findItem(R.id.distrust_key).setVisible(false); + this.mSelectedFingerprint = ownAxolotlFingerprint; + }); if (ownAxolotlFingerprint.equals(messageFingerprint)) { this.binding.ownFingerprintDesc.setTextColor( MaterialColors.getColor( diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index 7ed58127c0eb7700e64fa0598c833f5f19efdd34..48327e505d6a18e04b8f1c798db646eb458a0b78 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -22,7 +22,7 @@ import eu.siacs.conversations.utils.XmppUri; public abstract class OmemoActivity extends XmppActivity { private Account mSelectedAccount; - private String mSelectedFingerprint; + protected String mSelectedFingerprint; protected XmppUri mPendingFingerprintVerificationUri = null; @@ -50,25 +50,27 @@ public abstract class OmemoActivity extends XmppActivity { distrust.setVisible( status.isVerified() || (!status.isActive() && status.isTrusted())); } + // TODO can we rework this into using Intents? 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; + public boolean onContextItemSelected(final MenuItem item) { + final var itemId = item.getItemId(); + if (itemId == R.id.distrust_key) { + showPurgeKeyDialog(mSelectedAccount, mSelectedFingerprint); + return true; + } else if (itemId == R.id.copy_omemo_key) { + copyOmemoFingerprint(mSelectedFingerprint); + return true; + } else if (itemId == R.id.verify_scan) { + ScanActivity.scan(this); + return true; + } else { + return super.onContextItemSelected(item); } - return true; } @Override From 35af51fa205538622170574f86fe515507868d4c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 27 Mar 2025 21:45:32 +0100 Subject: [PATCH 088/169] bump a few dependencies --- build.gradle | 14 +++++++------- .../eu/siacs/conversations/ui/OmemoActivity.java | 5 ++++- .../conversations/ui/adapter/AccountAdapter.java | 4 ++-- .../ui/adapter/ConversationAdapter.java | 8 ++------ .../conversations/ui/adapter/MessageAdapter.java | 9 +++------ .../utils/IrregularUnicodeDetector.java | 12 +++++------- 6 files changed, 23 insertions(+), 29 deletions(-) diff --git a/build.gradle b/build.gradle index 2c41cea2112ef5e70dd0faf48957668bebb61da3..36445df0d16f6ddc03697856ca21783e9c7f7651 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:8.5.2' - classpath "com.diffplug.spotless:spotless-plugin-gradle:6.25.0" + classpath "com.diffplug.spotless:spotless-plugin-gradle:7.0.2" } } @@ -42,7 +42,7 @@ spotless { dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' implementation project(':libs:annotation') annotationProcessor project(':libs:annotation-processor') @@ -50,30 +50,30 @@ dependencies { implementation 'androidx.viewpager:viewpager:1.1.0' - playstoreImplementation('com.google.firebase:firebase-messaging:24.1.0') { + playstoreImplementation('com.google.firebase:firebase-messaging:24.1.1') { exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' } conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") - quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.1.0' + quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.2.0' implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") implementation "androidx.sharetarget:sharetarget:1.2.0" implementation 'androidx.appcompat:appcompat:1.7.0' - implementation 'androidx.exifinterface:exifinterface:1.3.7' + implementation 'androidx.exifinterface:exifinterface:1.4.0' 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-alpha10' + implementation 'com.google.android.material:material:1.13.0-alpha12' implementation 'androidx.work:work-runtime:2.9.1' implementation "androidx.emoji2:emoji2:1.5.0" freeImplementation "androidx.emoji2:emoji2-bundled:1.5.0" implementation "androidx.emoji2:emoji2-emojipicker:1.5.0" - implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1' + implementation 'org.bouncycastle:bcmail-jdk18on:1.80' implementation 'com.google.zxing:core:3.5.3' implementation 'org.minidns:minidns-client:1.0.4' implementation 'org.minidns:minidns-dnssec:1.0.4' diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index 48327e505d6a18e04b8f1c798db646eb458a0b78..f3225b9ecda2db88eed0bec3f9525a8599f4dc36 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -7,6 +7,7 @@ import android.view.View; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.databinding.DataBindingUtil; import com.google.android.material.color.MaterialColors; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -242,7 +243,9 @@ public abstract class OmemoActivity extends XmppActivity { @Override public void onRequestPermissionsResult( - int requestCode, String[] permissions, int[] grantResults) { + final int requestCode, + @NonNull final String[] permissions, + @NonNull final 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 b33d1c2be16a10d2501aefa727a0c7df09b93940..96b1470c37a0fa01bc0eee516dae7cbe2d55045b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -58,7 +58,7 @@ public class AccountAdapter extends ArrayAdapter { viewHolder.binding.accountStatus.setTextColor( MaterialColors.getColor( viewHolder.binding.accountStatus, - com.google.android.material.R.attr.colorPrimary)); + androidx.appcompat.R.attr.colorPrimary)); break; case DISABLED: case LOGGED_OUT: @@ -72,7 +72,7 @@ public class AccountAdapter extends ArrayAdapter { viewHolder.binding.accountStatus.setTextColor( MaterialColors.getColor( viewHolder.binding.accountStatus, - com.google.android.material.R.attr.colorError)); + androidx.appcompat.R.attr.colorError)); break; } final boolean isDisabled = (account.getStatus() == Account.State.DISABLED); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 3bcb8286f38f6d55663609d3cd086beb52885cb1..e6cef09dd081d77aa06159e9fe6d0da7a72ba352 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -6,16 +6,13 @@ import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.core.widget.ImageViewCompat; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.RecyclerView; - import com.google.android.material.color.MaterialColors; import com.google.common.base.Optional; - import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ItemConversationBinding; import eu.siacs.conversations.entities.Conversation; @@ -29,7 +26,6 @@ import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession; - import java.util.List; public class ConversationAdapter @@ -103,14 +99,14 @@ public class ConversationAdapter ColorStateList.valueOf( MaterialColors.getColor( viewHolder.binding.messageStatus, - com.google.android.material.R.attr.colorPrimary))); + androidx.appcompat.R.attr.colorPrimary))); } else { ImageViewCompat.setImageTintList( viewHolder.binding.messageStatus, ColorStateList.valueOf( MaterialColors.getColor( viewHolder.binding.messageStatus, - com.google.android.material.R.attr.colorControlNormal))); + androidx.appcompat.R.attr.colorControlNormal))); } viewHolder.binding.messageStatus.setVisibility(View.VISIBLE); } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 091ab1fbbcaf085772bd03267996dd66c7fe9e03..3eba1fd367a13c9fd960bbbf7d9a8f839aacf2ca 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -236,8 +236,7 @@ public class MessageAdapter extends ArrayAdapter { .time() .setTextColor( MaterialColors.getColor( - viewHolder.time(), - com.google.android.material.R.attr.colorError)); + viewHolder.time(), androidx.appcompat.R.attr.colorError)); } else { setTextColor(viewHolder.time(), bubbleColor); } @@ -1397,8 +1396,7 @@ public class MessageAdapter extends ArrayAdapter { ImageViewCompat.setImageTintList( imageView, ColorStateList.valueOf( - MaterialColors.getColor( - imageView, com.google.android.material.R.attr.colorError))); + MaterialColors.getColor(imageView, androidx.appcompat.R.attr.colorError))); } public static void setTextColor(final TextView textView, final BubbleColor bubbleColor) { @@ -1406,8 +1404,7 @@ public class MessageAdapter extends ArrayAdapter { textView.setTextColor(color); if (BubbleColor.SURFACES.contains(bubbleColor)) { textView.setLinkTextColor( - MaterialColors.getColor( - textView, com.google.android.material.R.attr.colorPrimary)); + MaterialColors.getColor(textView, androidx.appcompat.R.attr.colorPrimary)); } else { textView.setLinkTextColor(color); } diff --git a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java index 9dcdbb15e11cfb90154392a20409d814d61de376..748dd497d7e4468f4d45b953baead004f703c8f2 100644 --- a/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java +++ b/src/main/java/eu/siacs/conversations/utils/IrregularUnicodeDetector.java @@ -77,14 +77,12 @@ public class IrregularUnicodeDetector { return style( jid, MaterialColors.getColor( - context, - com.google.android.material.R.attr.colorError, - "colorError not found")); + context, androidx.appcompat.R.attr.colorError, "colorError not found")); } - private static Spannable style(Jid jid, @ColorInt int color) { - PatternTuple patternTuple = find(jid); - SpannableStringBuilder builder = new SpannableStringBuilder(); + private static Spannable style(final Jid jid, final @ColorInt int color) { + final var patternTuple = find(jid); + final var builder = new SpannableStringBuilder(); if (jid.getLocal() != null && patternTuple.local != null) { SpannableString local = new SpannableString(jid.getLocal()); colorize(local, patternTuple.local, color); @@ -92,7 +90,7 @@ public class IrregularUnicodeDetector { builder.append('@'); } if (jid.getDomain() != null) { - String[] labels = jid.getDomain().toString().split("\\."); + final var labels = jid.getDomain().toString().split("\\."); for (int i = 0; i < labels.length; ++i) { SpannableString spannableString = new SpannableString(labels[i]); colorize(spannableString, patternTuple.domain.get(i), color); From 3d6e4c9942c6ddca3ffbbc63583a5dab954521dd Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 28 Mar 2025 12:40:17 +0100 Subject: [PATCH 089/169] limit links to actual URIs and include more well known schemes links without scheme like 'example.com' or no longer linked to avoid false positives. however now mailto and tel are linked as well --- .../siacs/conversations/entities/Message.java | 4 +- .../ui/ConferenceDetailsActivity.java | 2 +- .../ui/ConversationFragment.java | 3 +- .../ui/adapter/MessageAdapter.java | 2 +- .../conversations/ui/text/FixedURLSpan.java | 9 +- .../conversations/ui/util/MyLinkify.java | 115 +--- .../conversations/ui/util/ShareUtil.java | 2 +- .../siacs/conversations/utils/GeoHelper.java | 289 +++++----- .../siacs/conversations/utils/Patterns.java | 521 +----------------- .../conversations/xmpp/XmppConnection.java | 4 +- 10 files changed, 209 insertions(+), 742 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index d0262b25d50dd6b3d4fc71289c232304fe7774f2..7516e8caa38fab6cec2b9933d888539cec797b24 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -16,9 +16,9 @@ import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Emoticons; -import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MimeUtils; +import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import java.lang.ref.WeakReference; @@ -796,7 +796,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable public synchronized boolean isGeoUri() { if (isGeoUri == null) { - isGeoUri = GeoHelper.GEO_URI.matcher(body).matches(); + isGeoUri = Patterns.URI_GEO.matcher(body).matches(); } return isGeoUri; } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index bc526d8c216137adfc423809539a077c686d6187..4d6268c7070cdf5981a084f3c6122a17eae9ce09 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -558,7 +558,7 @@ public class ConferenceDetailsActivity extends XmppActivity if (printableValue(subject)) { SpannableStringBuilder spannable = new SpannableStringBuilder(subject); StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); - MyLinkify.addLinks(spannable, false); + MyLinkify.addLinks(spannable); this.binding.mucSubject.setText(spannable); this.binding.mucSubject.setTextAppearance( subject.length() > (hasTitle ? 128 : 196) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 498b08773cbfc37e7f9024a27bbb967bb2b5169b..bd3a34555c25f54bd56a757368d380bf7cbdf7be 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -112,6 +112,7 @@ import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.NickValidityChecker; +import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.StylingHelper; @@ -2614,7 +2615,7 @@ public class ConversationFragment extends XmppFragment } } } else { - if (text != null && GeoHelper.GEO_URI.matcher(text).matches()) { + if (text != null && Patterns.URI_GEO.matcher(text).matches()) { mediaPreviewAdapter.addMediaPreviews( Attachment.of(getActivity(), Uri.parse(text), Attachment.Type.LOCATION)); toggleInputMethod(); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 3eba1fd367a13c9fd960bbbf7d9a8f839aacf2ca..00bf4b17ef5e6760ddd66a294c272a688fe1ea67 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -576,7 +576,7 @@ public class MessageAdapter extends ArrayAdapter { } StylingHelper.format(body, viewHolder.messageBody().getCurrentTextColor()); - MyLinkify.addLinks(body, true); + MyLinkify.addLinks(body); if (highlightedTerm != null) { StylingHelper.highlight(viewHolder.messageBody(), body, highlightedTerm); } diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java index e84c04fbf88488983757f2bdf8a0e67592822fff..aba355214b764ab28db0d0ea641e68837717aee4 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -34,24 +34,21 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.text.Editable; import android.text.Spanned; import android.text.style.URLSpan; import android.view.SoundEffectConstants; import android.view.View; import android.widget.Toast; - -import java.util.Arrays; - import eu.siacs.conversations.R; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.ShowLocationActivity; +import java.util.Arrays; @SuppressLint("ParcelCreator") public class FixedURLSpan extends URLSpan { - private FixedURLSpan(String url) { + private FixedURLSpan(final String url) { super(url); } @@ -93,7 +90,7 @@ public class FixedURLSpan extends URLSpan { try { context.startActivity(intent); widget.playSoundEffect(SoundEffectConstants.CLICK); - } catch (ActivityNotFoundException e) { + } catch (final ActivityNotFoundException e) { Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT) .show(); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index d5d3a82da519d79309822751581796de549b01ae..3e7db20d03c05d6ad7ee1c9c1a16c9c74b02b9de 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -29,107 +29,47 @@ package eu.siacs.conversations.ui.util; -import android.os.Build; import android.text.Editable; import android.text.style.URLSpan; import android.text.util.Linkify; - +import com.google.common.base.Splitter; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; - +import eu.siacs.conversations.ui.text.FixedURLSpan; +import eu.siacs.conversations.utils.Patterns; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.List; -import java.util.Locale; import java.util.Objects; -import eu.siacs.conversations.ui.text.FixedURLSpan; -import eu.siacs.conversations.utils.GeoHelper; -import eu.siacs.conversations.utils.Patterns; -import eu.siacs.conversations.utils.XmppUri; - public class MyLinkify { - private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = (matcher, url) -> { - if (url == null) { - return null; - } - final String lcUrl = url.toLowerCase(Locale.US); - if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) { - return removeTrailingBracket(url); - } else { - return "http://" + removeTrailingBracket(url); - } - }; - - private static String removeTrailingBracket(final String url) { - int numOpenBrackets = 0; - for (char c : url.toCharArray()) { - if (c == '(') { - ++numOpenBrackets; - } else if (c == ')') { - --numOpenBrackets; - } - } - if (numOpenBrackets != 0 && url.charAt(url.length() - 1) == ')') { - return url.substring(0, url.length() - 1); - } else { - return url; - } - } - - private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = (cs, start, end) -> { - if (start > 0) { - if (cs.charAt(start - 1) == '@' || cs.charAt(start - 1) == '.' - || cs.subSequence(Math.max(0, start - 3), start).equals("://")) { - return false; - } - } - - if (end < cs.length()) { - // Reject strings that were probably matched only because they contain a dot followed by - // by some known TLD (see also comment for WORD_BOUNDARY in Patterns.java) - return !isAlphabetic(cs.charAt(end - 1)) || !isAlphabetic(cs.charAt(end)); - } - - return true; - }; - - private static final Linkify.MatchFilter XMPPURI_MATCH_FILTER = (s, start, end) -> { - XmppUri uri = new XmppUri(s.subSequence(start, end).toString()); - return uri.isValidJid(); - }; - - private static boolean isAlphabetic(final int code) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - return Character.isAlphabetic(code); - } - - switch (Character.getType(code)) { - case Character.UPPERCASE_LETTER: - case Character.LOWERCASE_LETTER: - case Character.TITLECASE_LETTER: - case Character.MODIFIER_LETTER: - case Character.OTHER_LETTER: - case Character.LETTER_NUMBER: - return true; - default: - return false; - } - } - - public static void addLinks(Editable body, boolean includeGeo) { - Linkify.addLinks(body, Patterns.XMPP_PATTERN, "xmpp", XMPPURI_MATCH_FILTER, null); - Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER); - if (includeGeo) { - Linkify.addLinks(body, GeoHelper.GEO_URI, "geo"); - } + private static final Linkify.MatchFilter MATCH_FILTER = + (s, start, end) -> { + final var match = s.subSequence(start, end); + final var scheme = + Iterables.getFirst(Splitter.on(':').limit(2).splitToList(match), null); + if (scheme == null) { + return false; + } + return switch (scheme) { + case "tel" -> Patterns.URI_TEL.matcher(match).matches(); + case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); + case "geo" -> Patterns.URI_GEO.matcher(match).matches(); + default -> true; + }; + }; + + public static void addLinks(final Editable body) { + Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null); FixedURLSpan.fix(body); } public static List extractLinks(final Editable body) { - MyLinkify.addLinks(body, false); + MyLinkify.addLinks(body); final Collection spans = Arrays.asList(body.getSpans(0, body.length() - 1, URLSpan.class)); final Collection urlWrappers = @@ -140,11 +80,10 @@ public class MyLinkify { s == null ? null : new UrlWrapper(body.getSpanStart(s), s.getURL())), - uw -> uw != null); - List sorted = ImmutableList.sortedCopyOf( - (a, b) -> Integer.compare(a.position, b.position), urlWrappers); + Objects::nonNull); + List sorted = + ImmutableList.sortedCopyOf(Comparator.comparingInt(a -> a.position), urlWrappers); return Lists.transform(sorted, uw -> uw.url); - } private static class UrlWrapper { diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 99b63fe2746b8aa9995f22647e7a3f7af2a26230..bee0734676d1d252365f7ad3815bdd3110a01b5f 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -149,7 +149,7 @@ public class ShareUtil { } public static String getLinkScheme(final SpannableStringBuilder body) { - MyLinkify.addLinks(body, false); + MyLinkify.addLinks(body); for (final String url : MyLinkify.extractLinks(body)) { final Uri uri = Uri.parse(url); if ("xmpp".equals(uri.getScheme())) { diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java index 46de870ad3d527a5cf726e1baf84c8a07c0bb4cb..03cc78cee3f9252de5669a3722071a1467ec112d 100644 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java @@ -5,148 +5,167 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; - -import org.osmdroid.util.GeoPoint; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversational; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ShareLocationActivity; import eu.siacs.conversations.ui.ShowLocationActivity; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.regex.Matcher; +import org.osmdroid.util.GeoPoint; public class GeoHelper { - private static final String SHARE_LOCATION_PACKAGE_NAME = "eu.siacs.conversations.location.request"; - private static final String SHOW_LOCATION_PACKAGE_NAME = "eu.siacs.conversations.location.show"; - - public static Pattern GEO_URI = Pattern.compile("geo:(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)(?:,-?\\d+(?:\\.\\d+)?)?(?:;crs=[\\w-]+)?(?:;u=\\d+(?:\\.\\d+)?)?(?:;[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+)*(\\?z=\\d+)?", Pattern.CASE_INSENSITIVE); - - public static boolean isLocationPluginInstalled(Context context) { - return new Intent(SHARE_LOCATION_PACKAGE_NAME).resolveActivity(context.getPackageManager()) != null; - } - - public static boolean isLocationPluginInstalledAndDesired(Context context) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - final boolean configured = preferences.getBoolean("use_share_location_plugin", context.getResources().getBoolean(R.bool.use_share_location_plugin)); - return configured && isLocationPluginInstalled(context); - } - - public static Intent getFetchIntent(Context context) { - if (isLocationPluginInstalledAndDesired(context)) { - return new Intent(SHARE_LOCATION_PACKAGE_NAME); - } else { - return new Intent(context, ShareLocationActivity.class); - } - } - - public static GeoPoint parseGeoPoint(final Uri uri) { - return parseGeoPoint(uri.toString()); - } - - private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException { - final Matcher matcher = GEO_URI.matcher(body); - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid geo uri"); - } - final double latitude; - final double longitude; - try { - latitude = Double.parseDouble(matcher.group(1)); - if (latitude > 90.0 || latitude < -90.0) { - throw new IllegalArgumentException("Invalid geo uri"); - } - longitude = Double.parseDouble(matcher.group(2)); - if (longitude > 180.0 || longitude < -180.0) { - throw new IllegalArgumentException("Invalid geo uri"); - } - } catch (final NumberFormatException e) { - throw new IllegalArgumentException("Invalid geo uri",e); - } - return new GeoPoint(latitude, longitude); - } - - public static ArrayList createGeoIntentsFromMessage(Context context, Message message) { - final ArrayList intents = new ArrayList<>(); - final GeoPoint geoPoint; - try { - geoPoint = parseGeoPoint(message.getBody()); - } catch (IllegalArgumentException e) { - return intents; - } - final Conversational conversation = message.getConversation(); - final String label = getLabel(context, message); - - if (isLocationPluginInstalledAndDesired(context)) { - Intent locationPluginIntent = new Intent(SHOW_LOCATION_PACKAGE_NAME); - locationPluginIntent.putExtra("latitude", geoPoint.getLatitude()); - locationPluginIntent.putExtra("longitude", geoPoint.getLongitude()); - if (message.getStatus() != Message.STATUS_RECEIVED) { - locationPluginIntent.putExtra("jid", conversation.getAccount().getJid().toString()); - locationPluginIntent.putExtra("name", conversation.getAccount().getJid().getLocal()); - } else { - Contact contact = message.getContact(); - if (contact != null) { - locationPluginIntent.putExtra("name", contact.getDisplayName()); - locationPluginIntent.putExtra("jid", contact.getJid().toString()); - } else { - locationPluginIntent.putExtra("name", UIHelper.getDisplayedMucCounterpart(message.getCounterpart())); - } - } - intents.add(locationPluginIntent); - } else { - Intent intent = new Intent(context, ShowLocationActivity.class); - intent.setAction(SHOW_LOCATION_PACKAGE_NAME); - intent.putExtra("latitude", geoPoint.getLatitude()); - intent.putExtra("longitude", geoPoint.getLongitude()); - intents.add(intent); - } - - intents.add(geoIntent(geoPoint, label)); - - Intent httpIntent = new Intent(Intent.ACTION_VIEW); - httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+ geoPoint.getLatitude() + "," + geoPoint.getLongitude() +label)); - intents.add(httpIntent); - return intents; - } - - public static void view(Context context, Message message) { - final GeoPoint geoPoint = parseGeoPoint(message.getBody()); - final String label = getLabel(context, message); - context.startActivity(geoIntent(geoPoint,label)); - } - - private static Intent geoIntent(GeoPoint geoPoint, String label) { - Intent geoIntent = new Intent(Intent.ACTION_VIEW); - geoIntent.setData(Uri.parse("geo:" + geoPoint.getLatitude() + "," + geoPoint.getLongitude() + "?q=" + geoPoint.getLatitude() + "," + geoPoint.getLongitude() + "("+ label+")")); - return geoIntent; - } - - public static boolean openInOsmAnd(Context context, Message message) { - try { - final GeoPoint geoPoint = parseGeoPoint(message.getBody()); - final String label = getLabel(context, message); - return geoIntent(geoPoint, label).resolveActivity(context.getPackageManager()) != null; - } catch (IllegalArgumentException e) { - return false; - } - } - - private static String getLabel(Context context, Message message) { - if(message.getStatus() == Message.STATUS_RECEIVED) { - try { - return URLEncoder.encode(UIHelper.getMessageDisplayName(message),"UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - } else { - return context.getString(R.string.me); - } - } + private static final String SHARE_LOCATION_PACKAGE_NAME = + "eu.siacs.conversations.location.request"; + private static final String SHOW_LOCATION_PACKAGE_NAME = "eu.siacs.conversations.location.show"; + + public static boolean isLocationPluginInstalled(Context context) { + return new Intent(SHARE_LOCATION_PACKAGE_NAME).resolveActivity(context.getPackageManager()) + != null; + } + + public static boolean isLocationPluginInstalledAndDesired(Context context) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + final boolean configured = + preferences.getBoolean( + "use_share_location_plugin", + context.getResources().getBoolean(R.bool.use_share_location_plugin)); + return configured && isLocationPluginInstalled(context); + } + + public static Intent getFetchIntent(Context context) { + if (isLocationPluginInstalledAndDesired(context)) { + return new Intent(SHARE_LOCATION_PACKAGE_NAME); + } else { + return new Intent(context, ShareLocationActivity.class); + } + } + + public static GeoPoint parseGeoPoint(final Uri uri) { + return parseGeoPoint(uri.toString()); + } + + private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException { + final Matcher matcher = Patterns.URI_GEO.matcher(body); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid geo uri"); + } + final double latitude; + final double longitude; + try { + latitude = Double.parseDouble(matcher.group(1)); + if (latitude > 90.0 || latitude < -90.0) { + throw new IllegalArgumentException("Invalid geo uri"); + } + longitude = Double.parseDouble(matcher.group(2)); + if (longitude > 180.0 || longitude < -180.0) { + throw new IllegalArgumentException("Invalid geo uri"); + } + } catch (final NumberFormatException e) { + throw new IllegalArgumentException("Invalid geo uri", e); + } + return new GeoPoint(latitude, longitude); + } + + public static ArrayList createGeoIntentsFromMessage(Context context, Message message) { + final ArrayList intents = new ArrayList<>(); + final GeoPoint geoPoint; + try { + geoPoint = parseGeoPoint(message.getBody()); + } catch (IllegalArgumentException e) { + return intents; + } + final Conversational conversation = message.getConversation(); + final String label = getLabel(context, message); + + if (isLocationPluginInstalledAndDesired(context)) { + Intent locationPluginIntent = new Intent(SHOW_LOCATION_PACKAGE_NAME); + locationPluginIntent.putExtra("latitude", geoPoint.getLatitude()); + locationPluginIntent.putExtra("longitude", geoPoint.getLongitude()); + if (message.getStatus() != Message.STATUS_RECEIVED) { + locationPluginIntent.putExtra("jid", conversation.getAccount().getJid().toString()); + locationPluginIntent.putExtra( + "name", conversation.getAccount().getJid().getLocal()); + } else { + Contact contact = message.getContact(); + if (contact != null) { + locationPluginIntent.putExtra("name", contact.getDisplayName()); + locationPluginIntent.putExtra("jid", contact.getJid().toString()); + } else { + locationPluginIntent.putExtra( + "name", UIHelper.getDisplayedMucCounterpart(message.getCounterpart())); + } + } + intents.add(locationPluginIntent); + } else { + Intent intent = new Intent(context, ShowLocationActivity.class); + intent.setAction(SHOW_LOCATION_PACKAGE_NAME); + intent.putExtra("latitude", geoPoint.getLatitude()); + intent.putExtra("longitude", geoPoint.getLongitude()); + intents.add(intent); + } + + intents.add(geoIntent(geoPoint, label)); + + Intent httpIntent = new Intent(Intent.ACTION_VIEW); + httpIntent.setData( + Uri.parse( + "https://maps.google.com/maps?q=loc:" + + geoPoint.getLatitude() + + "," + + geoPoint.getLongitude() + + label)); + intents.add(httpIntent); + return intents; + } + + public static void view(Context context, Message message) { + final GeoPoint geoPoint = parseGeoPoint(message.getBody()); + final String label = getLabel(context, message); + context.startActivity(geoIntent(geoPoint, label)); + } + + private static Intent geoIntent(GeoPoint geoPoint, String label) { + Intent geoIntent = new Intent(Intent.ACTION_VIEW); + geoIntent.setData( + Uri.parse( + "geo:" + + geoPoint.getLatitude() + + "," + + geoPoint.getLongitude() + + "?q=" + + geoPoint.getLatitude() + + "," + + geoPoint.getLongitude() + + "(" + + label + + ")")); + return geoIntent; + } + + public static boolean openInOsmAnd(Context context, Message message) { + try { + final GeoPoint geoPoint = parseGeoPoint(message.getBody()); + final String label = getLabel(context, message); + return geoIntent(geoPoint, label).resolveActivity(context.getPackageManager()) != null; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static String getLabel(Context context, Message message) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + try { + return URLEncoder.encode(UIHelper.getMessageDisplayName(message), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } else { + return context.getString(R.string.me); + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index 026951b22eed95f2943f46c8a44266e1dabe8058..b4eaef9660f0ef3718a19847297f7a77c637596b 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -1,513 +1,24 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * - * Download latest version here: - * https://android.googlesource.com/platform/frameworks/base.git/+/master/core/java/android/util/Patterns.java - * - * - */ package eu.siacs.conversations.utils; -import java.util.regex.Matcher; + import java.util.regex.Pattern; -/** - * Commonly used regular expression patterns. - */ + public class Patterns { - public static final Pattern XMPP_PATTERN = Pattern - .compile("xmpp\\:(?:(?:[" - + Patterns.GOOD_IRI_CHAR - + "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])" - + "|(?:\\%[a-fA-F0-9]{2}))+"); + public static final Pattern URI_GENERIC = + Pattern.compile( + "(?<=^|\\s)(tel|xmpp|http|https|geo|mailto):[a-zA-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); - /** - * Regular expression to match all IANA top-level domains. - * List accurate as of 2011/07/18. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py - * - * @deprecated Due to the recent profileration of gTLDs, this API is - * expected to become out-of-date very quickly. Therefore it is now - * deprecated. - */ - @Deprecated - public static final String TOP_LEVEL_DOMAIN_STR = - "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])" - + "|(biz|b[abdefghijmnorstvwyz])" - + "|(cat|com|coop|c[acdfghiklmnoruvxyz])" - + "|d[ejkmoz]" - + "|(edu|e[cegrstu])" - + "|f[ijkmor]" - + "|(gov|g[abdefghilmnpqrstuwy])" - + "|h[kmnrtu]" - + "|(info|int|i[delmnoqrst])" - + "|(jobs|j[emop])" - + "|k[eghimnprwyz]" - + "|l[abcikrstuvy]" - + "|(mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" - + "|(name|net|n[acefgilopruz])" - + "|(org|om)" - + "|(pro|p[aefghklmnrstwy])" - + "|qa" - + "|r[eosuw]" - + "|s[abcdeghijklmnortuvyz]" - + "|(tel|travel|t[cdfghjklmnoprtvwz])" - + "|u[agksyz]" - + "|v[aceginu]" - + "|w[fs]" - + "|(\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)" - + "|y[et]" - + "|z[amw])"; - /** - * Regular expression pattern to match all IANA top-level domains. - * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}. - */ - @Deprecated - public static final Pattern TOP_LEVEL_DOMAIN = - Pattern.compile(TOP_LEVEL_DOMAIN_STR); - /** - * Regular expression to match all IANA top-level domains for WEB_URL. - * List accurate as of 2011/07/18. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py - * - * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}. - */ - @Deprecated - public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL = - "(?:" - + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" - + "|(?:biz|b[abdefghijmnorstvwyz])" - + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" - + "|d[ejkmoz]" - + "|(?:edu|e[cegrstu])" - + "|f[ijkmor]" - + "|(?:gov|g[abdefghilmnpqrstuwy])" - + "|h[kmnrtu]" - + "|(?:info|int|i[delmnoqrst])" - + "|(?:jobs|j[emop])" - + "|k[eghimnprwyz]" - + "|l[abcikrstuvy]" - + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])" - + "|(?:name|net|n[acefgilopruz])" - + "|(?:org|om)" - + "|(?:pro|p[aefghklmnrstwy])" - + "|qa" - + "|r[eosuw]" - + "|s[abcdeghijklmnortuvyz]" - + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" - + "|u[agksyz]" - + "|v[aceginu]" - + "|w[fs]" - + "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)" - + "|y[et]" - + "|z[amw]))"; - /** - * Regular expression to match all IANA top-level domains. - * - * List accurate as of 2015/11/24. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py - * - * @hide - */ - static final String IANA_TOP_LEVEL_DOMAINS = - "(?:" - + "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active" - + "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam" - + "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates" - + "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])" - + "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva" - + "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black" - + "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique" - + "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business" - + "|buzz|bzh|b[abdefghijmnorstvwyz])" - + "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards" - + "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo" - + "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco" - + "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach" - + "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos" - + "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses" - + "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])" - + "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta" - + "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount" - + "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])" - + "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises" - + "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed" - + "|express|e[cegrstu])" - + "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film" - + "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth" - + "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi" - + "|f[ijkmor])" - + "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving" - + "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger" - + "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])" - + "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings" - + "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai" - + "|h[kmnrtu])" - + "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute" - + "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])" - + "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])" - + "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])" - + "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc" - + "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live" - + "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])" - + "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba" - + "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda" - + "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar" - + "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])" - + "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk" - + "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])" - + "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka" - + "|otsuka|ovh|om)" - + "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography" - + "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing" - + "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property" - + "|protection|pub|p[aefghklmnrstwy])" - + "|(?:qpon|quebec|qa)" - + "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals" - + "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks" - + "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])" - + "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo" - + "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security" - + "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski" - + "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting" - + "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies" - + "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])" - + "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica" - + "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools" - + "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])" - + "|(?:ubs|university|uno|uol|u[agksyz])" - + "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin" - + "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])" - + "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill" - + "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])" - + "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434" - + "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d" - + "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431" - + "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648" - + "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629" - + "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646" - + "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633" - + "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629" - + "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646" - + "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627" - + "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924" - + "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4" - + "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd" - + "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22" - + "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c" - + "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71" - + "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063" - + "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c" - + "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c" - + "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f" - + "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc" - + "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137" - + "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox" - + "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g" - + "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim" - + "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks" - + "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a" - + "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd" - + "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h" - + "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s" - + "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c" - + "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i" - + "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d" - + "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt" - + "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e" - + "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab" - + "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema" - + "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh" - + "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c" - + "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb" - + "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a" - + "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o" - + "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)" - + "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])" - + "|(?:zara|zip|zone|zuerich|z[amw]))"; - /** - * Kept for backward compatibility reasons. - * - * @deprecated Deprecated since it does not include all IRI characters defined in RFC 3987 - */ - @Deprecated - public static final String GOOD_IRI_CHAR = - "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF"; - public static final Pattern IP_ADDRESS - = Pattern.compile( - "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9]))"); + public static final Pattern URI_TEL = + Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); - /** - * IPv6 address matcher for - * IPv6 addresses - * zero compressed IPv6 addresses (section 2.2 of rfc5952) - * link-local IPv6 addresses with zone index (section 11 of rfc4007) - * IPv4-Embedded IPv6 Address (section 2 of rfc6052) - * IPv4-mapped IPv6 addresses (section 2.1 of rfc2765) - * IPv4-translated addresses (section 2.1 of rfc2765) - * - * Taken from https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses/17871737#17871737 - */ - public static final Pattern IP6_ADDRESS - = Pattern.compile( - "\\[" + - "(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|" + - "([0-9a-fA-F]{1,4}:){1,7}:|" + - "([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|" + - "([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|" + - "([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|" + - "([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|" + - "([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|" + - "[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|" + - ":((:[0-9a-fA-F]{1,4}){1,7}|:)|" + - "fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|" + - "::(ffff(:0{1,4}){0,1}:){0,1}" + - "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + - "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|" + - "([0-9a-fA-F]{1,4}:){1,4}:" + - "((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}" + - "(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))" + - "\\]" - ); - /** - * Valid UCS characters defined in RFC 3987. Excludes space characters. - */ - private static final String UCS_CHAR = "[" + - "\u00A0-\uD7FF" + - "\uF900-\uFDCF" + - "\uFDF0-\uFFEF" + - "\uD800\uDC00-\uD83F\uDFFD" + - "\uD840\uDC00-\uD87F\uDFFD" + - "\uD880\uDC00-\uD8BF\uDFFD" + - "\uD8C0\uDC00-\uD8FF\uDFFD" + - "\uD900\uDC00-\uD93F\uDFFD" + - "\uD940\uDC00-\uD97F\uDFFD" + - "\uD980\uDC00-\uD9BF\uDFFD" + - "\uD9C0\uDC00-\uD9FF\uDFFD" + - "\uDA00\uDC00-\uDA3F\uDFFD" + - "\uDA40\uDC00-\uDA7F\uDFFD" + - "\uDA80\uDC00-\uDABF\uDFFD" + - "\uDAC0\uDC00-\uDAFF\uDFFD" + - "\uDB00\uDC00-\uDB3F\uDFFD" + - "\uDB44\uDC00-\uDB7F\uDFFD" + - "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]"; - /** - * Valid characters for IRI label defined in RFC 3987. - */ - private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR; - /** - * Valid characters for IRI TLD defined in RFC 3987. - */ - private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR; - /** - * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets. - */ - private static final String IRI_LABEL = - "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "\\-]{0,61}[" + LABEL_CHAR + "]){0,1}"; - /** - * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters. - */ - private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w"; - private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")"; - private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD; - public static final Pattern DOMAIN_NAME - = Pattern.compile("(" + HOST_NAME + "|" + IP6_ADDRESS + "|" + IP_ADDRESS +")"); - private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/"; - /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */ - private static final String WORD_BOUNDARY = "(?:\\b|$|^)"; - private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" - + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" - + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@"; - private static final String PORT_NUMBER = "\\:\\d{1,5}"; - private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR - + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params - + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_\\$])|(?:\\%[a-fA-F0-9]{2}))*"; - /** - * Regular expression pattern to match most part of RFC 3987 - * Internationalized URLs, aka IRIs. - */ - public static final Pattern WEB_URL = Pattern.compile("(" - + "(" - + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?" - + "(?:" + DOMAIN_NAME + ")" - + "(?:" + PORT_NUMBER + ")?" - + ")" - + "(" + PATH_AND_QUERY + ")?" - + WORD_BOUNDARY - + ")"); - /** - * Regular expression that matches known TLDs and punycode TLDs - */ - private static final String STRICT_TLD = "(?:" + - IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")"; - /** - * Regular expression that matches host names using {@link #STRICT_TLD} - */ - private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+" - + STRICT_TLD + ")"; - /** - * Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or - * {@link #IP_ADDRESS} - */ - private static final Pattern STRICT_DOMAIN_NAME - = Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")"); - /** - * Regular expression that matches domain names without a TLD - */ - private static final String RELAXED_DOMAIN_NAME = - "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + "|" + IP6_ADDRESS + ")"; - /** - * Regular expression to match strings that do not start with a supported protocol. The TLDs - * are expected to be one of the known TLDs. - */ - private static final String WEB_URL_WITHOUT_PROTOCOL = "(" - + WORD_BOUNDARY - + "(? The pattern matches the following: - *

    - *
  • Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes - * may follow. - *
  • Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes. - *
  • A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes. - *
- */ - public static final Pattern PHONE - = Pattern.compile( // sdd = space, dot, or dash - "(\\+[0-9]+[\\- \\.]*)?" // +* - + "(\\([0-9]+\\)[\\- \\.]*)?" // ()* - + "([0-9][0-9\\- \\.]+[0-9])"); // + - /** - * Convenience method to take all of the non-null matching groups in a - * regex Matcher and return them as a concatenated string. - * - * @param matcher The Matcher object from which grouped text will - * be extracted - * - * @return A String comprising all of the non-null matched - * groups concatenated together - */ - public static final String concatGroups(Matcher matcher) { - StringBuilder b = new StringBuilder(); - final int numGroups = matcher.groupCount(); - for (int i = 1; i <= numGroups; i++) { - String s = matcher.group(i); - if (s != null) { - b.append(s); - } - } - return b.toString(); - } - /** - * Convenience method to return only the digits and plus signs - * in the matching string. - * - * @param matcher The Matcher object from which digits and plus will - * be extracted - * - * @return A String comprising all of the digits and plus in - * the match - */ - public static final String digitsAndPlusOnly(Matcher matcher) { - StringBuilder buffer = new StringBuilder(); - String matchingRegion = matcher.group(); - for (int i = 0, size = matchingRegion.length(); i < size; i++) { - char character = matchingRegion.charAt(i); - if (character == '+' || Character.isDigit(character)) { - buffer.append(character); - } - } - return buffer.toString(); + public static final Pattern URI_HTTP = Pattern.compile("https?://\\S+"); + + public static Pattern URI_GEO = + Pattern.compile( + "geo:(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)(?:,-?\\d+(?:\\.\\d+)?)?(?:;crs=[\\w-]+)?(?:;u=\\d+(?:\\.\\d+)?)?(?:;[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+)*(\\?z=\\d+)?", + Pattern.CASE_INSENSITIVE); + + private Patterns() { + throw new AssertionError("Do not instantiate me"); } - /** - * Do not create this static utility class. - */ - private Patterns() {} } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 654a7e7e8b52e7a975dd9c7da6980a21a10732cc..3bb877721a6eafa2ef454864589fec4b29031d71 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -1034,7 +1034,7 @@ public class XmppConnection implements Runnable { if (Strings.isNullOrEmpty(text)) { throw new StateChangingException(Account.State.UNAUTHORIZED); } - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text); + final Matcher matcher = Patterns.URI_HTTP.matcher(text); if (matcher.find()) { final HttpUrl url; try { @@ -1925,7 +1925,7 @@ public class XmppConnection implements Runnable { if (url != null) { setAccountCreationFailed(url); } else if (instructions != null) { - final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(instructions); + final Matcher matcher = Patterns.URI_HTTP.matcher(instructions); if (matcher.find()) { setAccountCreationFailed( instructions.substring(matcher.start(), matcher.end())); From c007e253aea85e915003a2ea72b6770c35572af9 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 28 Mar 2025 15:11:52 +0100 Subject: [PATCH 090/169] move IP patterns to Patterns class --- .../java/eu/siacs/conversations/utils/IP.java | 27 ++++--------------- .../siacs/conversations/utils/Patterns.java | 16 +++++++++++ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java index 8b07c98f66eadfa8e259e6458303d4314c177ad5..33e989ee3d189d83e2c055b87b768c26b0cff91b 100644 --- a/src/main/java/eu/siacs/conversations/utils/IP.java +++ b/src/main/java/eu/siacs/conversations/utils/IP.java @@ -2,33 +2,16 @@ package eu.siacs.conversations.utils; import com.google.common.net.InetAddresses; import java.net.InetAddress; -import java.util.regex.Pattern; public class IP { - private static final Pattern PATTERN_IPV4 = - Pattern.compile( - "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = - Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" - + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_6HEX4DEC = - Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); - private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = - Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); - private static final Pattern PATTERN_IPV6 = - Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); - public static boolean matches(final String server) { return server != null - && (PATTERN_IPV4.matcher(server).matches() - || PATTERN_IPV6.matcher(server).matches() - || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() - || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() - || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); + && (Patterns.IPV4.matcher(server).matches() + || Patterns.IPV6.matcher(server).matches() + || Patterns.IPV6_6HEX4DEC.matcher(server).matches() + || Patterns.IPV6_HEX4_DECOMPRESSED.matcher(server).matches() + || Patterns.IPV6_HEX_COMPRESSED.matcher(server).matches()); } public static String wrapIPv6(final String host) { diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index b4eaef9660f0ef3718a19847297f7a77c637596b..61d9bb3da035ad6c8f7392c878f9515dd56e70e9 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -18,6 +18,22 @@ public class Patterns { "geo:(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)(?:,-?\\d+(?:\\.\\d+)?)?(?:;crs=[\\w-]+)?(?:;u=\\d+(?:\\.\\d+)?)?(?:;[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+)*(\\?z=\\d+)?", Pattern.CASE_INSENSITIVE); + public static final Pattern IPV4 = + Pattern.compile( + "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + public static final Pattern IPV6 = + Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + public static final Pattern IPV6_HEX4_DECOMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" + + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + public static final Pattern IPV6_6HEX4DEC = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + public static final Pattern IPV6_HEX_COMPRESSED = + Pattern.compile( + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private Patterns() { throw new AssertionError("Do not instantiate me"); } From 4c7119cd694db1bc9886939c6d1f68858d60f832 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Thu, 27 Mar 2025 11:22:54 +0000 Subject: [PATCH 091/169] Translated using Weblate (Polish) Currently translated at 100.0% (1064 of 1064 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 3cb55cc8f9f49f9702f10928f56b4fc5de204386..440b9c3274aef97b74fc82b0bc15fa2263c0c8db 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -846,7 +846,7 @@ Przywróć kopię zapasową Przywróć Wpisz swoje hasło do konta %s aby przywrócić kopię zapasową. - Nie używaj kopii zapasowej aby klonować (uruchamiać równolegle) instalację. Przywracanie kopii jest przeznaczone tylko do migracji albo kiedy urządzenie zostało zgubione. + Nie przywracaj kluczy OMEMO aby klonować (uruchamiać równocześnie) instalację. Przywracanie kluczy OMEMO jest przeznaczone tylko do migracji albo kiedy urządzenie zostało zgubione. Nie można przywrócić kopii zapasowej. Nie można odszyfrować kopii zapasowej. Czy hasło jest poprawne? Kopia i Przywracanie @@ -1019,7 +1019,7 @@ Nie można usunąć konta z serwera Przeszukaj rozmowy grupowe Rozmowy grupowe - Nie próbuj przywracać kopii zapasowych, których nie utworzono samodzielnie! + Przywracaj jedynie kopie zapasowe, które samodzielnie utworzyłeś. Próbujesz zaimportować plik kopii zapasowej o przestarzałym formacie Audiobook Połącz się ponownie na innym hoście @@ -1144,4 +1144,6 @@ Spróbuj ponownie używając P2P dokument Microsoft Word Przywiązywanie kanału niedostępne + Przywróć klucze OMEMO + Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im From 25bf0332956e0511a5346d802eac2ad1818b530f Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Thu, 27 Mar 2025 11:26:17 +0000 Subject: [PATCH 092/169] Translated using Weblate (Polish) Currently translated at 54.7% (46 of 84 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/changelogs/42042.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/pl-PL/changelogs/42042.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/42042.txt b/fastlane/metadata/android/pl-PL/changelogs/42042.txt new file mode 100644 index 0000000000000000000000000000000000000000..98c1eae90a7e669e1bb02bdb7bf0cf7500f08e46 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/42042.txt @@ -0,0 +1,2 @@ +* Naprawienie pętli wysyłania na serwerach, które obsługują jedynie sm:2 +* Pokazywanie „Przełącz na wideo” tylko gdy druga strona obsługuje wideo From 87a2ef96b85961e82628162f8ba9ab87fe1caba1 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 27 Mar 2025 18:32:29 +0000 Subject: [PATCH 093/169] Translated using Weblate (Indonesian) Currently translated at 41.2% (439 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/id/ --- src/main/res/values-id/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index 181ceabb45119386750ec654605b637c3d65031d..e03a944e663831e22fbdfbc47e64a360c56cdd25 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -472,4 +472,4 @@ alamat XMPP Buat channel publik... Sibuk - \ No newline at end of file + From 775cd90298263df9c8603dd0cec91afb63f43e08 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 27 Mar 2025 18:32:31 +0000 Subject: [PATCH 094/169] Translated using Weblate (Japanese) Currently translated at 98.9% (1054 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 859fb10b227cf96a82710039fedd6f51e5a2c203..0292ddf6741943d5e998095d29dbd30ccf571fa9 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -317,7 +317,7 @@ フォアグラウンドサービス OSが接続を切断するのを防止します バックアップを作成 - バックアップファイルは %s に保存されます + バックアップファイルは %s に保存されます バックアップファイルを作成しています バックアップを作成しました バックアップファイルは %s に保存されました From 35a2a603e510fedf5a65541ac0248bc14624c266 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Thu, 27 Mar 2025 18:32:38 +0000 Subject: [PATCH 095/169] Translated using Weblate (Slovak) Currently translated at 44.4% (473 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sk/ --- src/main/res/values-sk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-sk/strings.xml b/src/main/res/values-sk/strings.xml index 436c087304b00301f55d63f6126b5a9f7b6c1f37..b5c02d325d4d06c72b11599170d51b878b7be02a 100644 --- a/src/main/res/values-sk/strings.xml +++ b/src/main/res/values-sk/strings.xml @@ -500,4 +500,4 @@ Zašifrované s OMEMO Zlyhané doručenia Viac možnosťí - \ No newline at end of file + From 428c917de853638a8e7bf6990e1e15e1451a76a5 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Thu, 27 Mar 2025 19:25:30 +0000 Subject: [PATCH 096/169] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index d99c72bc6c788232a79a05f9f7ca8b12078d1112..961785d50a17be87058e9c4cc9fe16ed3cb34aeb 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -1162,4 +1162,5 @@ документ Word Відновити ключі OMEMO Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im + Розташування резервних копій From 14ee2895dfff1d5adbfff137fa2c38fc126f46eb Mon Sep 17 00:00:00 2001 From: "lucasmz.dev" Date: Fri, 28 Mar 2025 01:07:51 +0000 Subject: [PATCH 097/169] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 24fac1e6892a0de84fc2c86168d2452813a16e82..ebad1bf21e15d1d50d6f08ebfa6fb036dfd9841e 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -326,7 +326,7 @@ Serviço ativo Impede que o sistema operacional encerre sua conexão Criar backup - Os arquivos de backup serão armazenados em %s + Os backups serão armazenados em %s Criando arquivos de backup O seu backup foi criado Os arquivos de backup foram armazenados em %s @@ -837,7 +837,7 @@ Restaurar o backup Restaurar Digite sua senha para a conta %s para restaurar o backup. - Não use o recurso de restaurar um backup para tentar clonar (rodar simultaneamente) uma instalação. A restauração de backups é destinada a migrações ou caso você tenha perdido o dispositivo original. + Não restaure chaves OMEMO na tentativa de clonar (rodar simultaneamente) uma instalação. A restauração de chaves OMEMO é destinada a migrações ou caso você tenha perdido o dispositivo original. Não foi possível restaurar o backup. Não foi possível descriptografar o backup. A senha está correta? Backup & Restauração @@ -1042,7 +1042,7 @@ Esconder notificação Ao atuar como um distribuidor UnifiedPush, a conexão persistente, estável, e amigável à bateria do XMPP será usada para alertar outros apps compatíveis com o UnifiedPush, como o Tusky, Ltt.RS, FluffyChat, e mais. Conversas correspondentes arquivadas. - Não tente restaurar backups que você não criou! + Restaure somente backups que você mesmo criou. O contato não está disponível O vídeo está ativado. Toque para desativar. A descoberta de canais usa um serviço de terceiros chamado <a href=https://search.jabber.network>search.jabber.network</a>.<br><br>Usar esta funcionalidade transmitirá seu endereço de IP e termos de pesquisa ao serviço. Leia sua <a href=https://search.jabber.network/privacy>Política de Privacidade</a> para mais informações. @@ -1130,4 +1130,7 @@ Tentar novamente com P2P Vínculo de canal indisponível Documento do Word + Restaurar as chaves OMEMO + O Quicksy só pode restaurar backups de contas quicksy.im + Local do backup From 784d35ea6253b4afbf9dabfc604d91c51454560c Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 27 Mar 2025 18:36:04 +0000 Subject: [PATCH 098/169] Translated using Weblate (Albanian) Currently translated at 99.0% (1055 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index c5a81a977a69994deeb7eb9ec61daa7a7337d282..a8d0fa01af287bbca03ec6aaf835e68eb51f60d8 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -253,7 +253,7 @@ Riprovoni Shërbim në prapaskenë Krijo kopjeruajtje - Kartelat kopjeruajtje do të depozitohen në %s + Kopjeruajtjet do të depozitohen në %s Po krijohen kartela kopjeruajtje Kopjeruajtja juaj u krijua Po rikthehet kopjeruajtje @@ -1124,4 +1124,5 @@ Dokument Word Rikthe kyçe OMEMO Quicksy mund të rikthejë vetëm kopjeruajtje për llogari quicksy.im + Vendndodhje kopjeruajtjesh From 9d69af59791d54bae4fa8deca20fe0d8aeb2d57d Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Fri, 28 Mar 2025 01:08:58 +0000 Subject: [PATCH 099/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 8cde6a33adde7044c57e25af89116911aaaf42c0..c091eba9d98d6c787c1d891e6858f91367a9a398 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -333,7 +333,7 @@ 前台服务 防止操作系统中断连接 创建备份 - 备份文件将存储在 %s + 备份将存储在 %s 正在创建备份文件 备份已创建 此备份文件已存储在 %s @@ -404,7 +404,7 @@ 公开频道配置 私人,仅成员 对任何参与者显示用户 XMPP 地址 - 开启频道发言审核 + 启用频道发言审核 您没有发言权 群聊配置修改成功! 无法修改群聊配置 @@ -1109,4 +1109,5 @@ Word 文档 恢复 OMEMO 密钥 Quicksy 只能恢复 quicksy.im 账号的备份 + 备份位置 From 76b6b39fb5a6c6b3defa01a4b84cdb355f2402f0 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Fri, 28 Mar 2025 06:15:20 +0000 Subject: [PATCH 100/169] Translated using Weblate (German) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 1d2423c06b8989c7bd84600eaa88c4f17f1ba43e..813617bcf976babe7e47306fad17a581e662550b 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -321,7 +321,7 @@ Vordergrunddienst Verhindert, dass das Betriebssystem deine Verbindung unterbricht Sicherung erstellen - Sicherungsdateien werden gespeichert in %s + Sicherungen werden gespeichert in %s Erstelle Sicherungsdateien Deine Sicherung wurde erstellt Die Sicherungsdateien wurden gespeichert in %s @@ -1114,4 +1114,5 @@ Word-Dokument OMEMO-Schlüssel wiederherstellen Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen + Sicherungsort From 8f16094ea8443b59029733bb2a8ebb6174d7de2b Mon Sep 17 00:00:00 2001 From: ghose Date: Fri, 28 Mar 2025 05:31:20 +0000 Subject: [PATCH 101/169] Translated using Weblate (Galician) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index df3ef3958306d0f43b4e0b77a4b5c24b9c3e3983..f71b61f29d117abdad2b303f3ce1142f49957c9a 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -321,7 +321,7 @@ Servizo en primeiro plano Evita que o sistema operativo corte a conexión Crear copia de apoio - Os ficheiros de copia gardaranse en %s + As copias vanse gardar en %s Creando ficheiros de apoio Creouse o ficheiro Os ficheiros de apoio gardáronse en %s @@ -1114,4 +1114,5 @@ Documento de Word Restaurar claves OMEMO Quicksy só pode restaurar copias de apoio de contas quicksy.im + Localización das copias From b13af5071147c858d700b9cc2ea86d6549533ab4 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Fri, 28 Mar 2025 07:06:17 +0000 Subject: [PATCH 102/169] Translated using Weblate (Polish) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 440b9c3274aef97b74fc82b0bc15fa2263c0c8db..65d7696c8e10912832de06bc95f97924f6798c8b 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -325,7 +325,7 @@ Usługa na pierwszym planie Uniemożliwia systemowi przerwanie połączenia Utwórz kopię zapasową - Kopia zapasowa będzie zapisana w %s + Kopie zapasowe będą przechowywane w %s Tworzenie kopii zapasowej Kopia zapasowa została utworzona Kopia zapasowa zapisana w %s @@ -1146,4 +1146,5 @@ Przywiązywanie kanału niedostępne Przywróć klucze OMEMO Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im + Lokalizacja kopii zapasowej From 5f8c8b1061228098b40184ea72b0d82a5e23b9fe Mon Sep 17 00:00:00 2001 From: solokot Date: Fri, 28 Mar 2025 05:43:11 +0000 Subject: [PATCH 103/169] Translated using Weblate (Russian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 82d96bd49d176c46dd24c2af21579916c55907b3..25dfa43980575c5e1d4ff6179a92e4100db0d4eb 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -332,7 +332,7 @@ Процесс переднего плана Не позволять операционной системе закрывать ваше соединение Создать резервную копию - Файлы резервной копии будут сохранены в %s + Резервные копии будут сохранены в %s Создание резервной копии Ваша резервная копия создана Файлы резервной копии сохранены в %s @@ -1161,4 +1161,5 @@ Документ Word Восстановить ключи OMEMO Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im + Расположение резервной копии From 7ff8348b8facf0b3dfc46b6c01566a242304fa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 28 Mar 2025 07:51:12 +0000 Subject: [PATCH 104/169] Translated using Weblate (Estonian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/et/ --- src/main/res/values-et/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index fbfdc7e2568b1637d3a88df300213155e32b1098..65e041ecfdd85341946f845d78f46e6ceda9de6e 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -441,7 +441,7 @@ Teenus esiplaanil See eelistus takistab operatsioonisüsteemil sinu võrguühenduse sulgemist Tee varukoopia - Varukoopia failide salvestamise asukoht: %s + Varukoopiate salvestamise asukoht: %s Sinu varukoopia on tehtud Teeme varukoopiat Varukoopia failid on salvestatud siin kaustas: %s @@ -1134,4 +1134,5 @@ Wordi-dokument Taasta OMEMO võtmed Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid + Varukoopia asukoht From 62f04aa41fbec5949cff596bbd9b9a04f10649fa Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 28 Mar 2025 17:02:34 +0100 Subject: [PATCH 105/169] version bump to 2.18.0-beta --- CHANGELOG.md | 5 +++++ build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/4213604.txt | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/4213604.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index d048e41491a2b6c7be8e58ad7a8d07f6375b7782..739c328d05491164e1e5ed0c3e81bc4c98922d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### Version 2.18.0 + +* Add ability to pick backup location +* More more URIs (tel:, mailto:) clickable + ### Version 2.17.12 * Fix crash on file transfer in fi translation diff --git a/build.gradle b/build.gradle index 36445df0d16f6ddc03697856ca21783e9c7f7651..6500d21ed67c240b10f0006b88c9c195f78d7895 100644 --- a/build.gradle +++ b/build.gradle @@ -113,8 +113,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42135 - versionName "2.17.12" + versionCode 42136 + versionName "2.18.0-beta" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/en-US/changelogs/4213604.txt b/fastlane/metadata/android/en-US/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..f9e97cb91c99dcd499e9c2afeb34a29ec1f8f4ef --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Add ability to pick backup location +* More more URIs (tel:, mailto:) clickable From afa9b2c7a5a0895eed09f9d94ea1ada5c55a3470 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 30 Mar 2025 10:12:13 +0200 Subject: [PATCH 106/169] run xmpp uris through XmppUri parser before displaying them --- .../eu/siacs/conversations/ui/util/MyLinkify.java | 3 +++ .../java/eu/siacs/conversations/utils/XmppUri.java | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index 3e7db20d03c05d6ad7ee1c9c1a16c9c74b02b9de..e502780b73c8f1096d5006a335ddf9c48c41edbb 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -29,6 +29,7 @@ package eu.siacs.conversations.ui.util; +import android.net.Uri; import android.text.Editable; import android.text.style.URLSpan; import android.text.util.Linkify; @@ -39,6 +40,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.Patterns; +import eu.siacs.conversations.utils.XmppUri; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; @@ -59,6 +61,7 @@ public class MyLinkify { case "tel" -> Patterns.URI_TEL.matcher(match).matches(); case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); case "geo" -> Patterns.URI_GEO.matcher(match).matches(); + case "xmpp" -> new XmppUri(Uri.parse(match.toString())).isValidJid(); default -> true; }; }; diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index b6022cb694cebc2eb67808c26f20d11733c4cd23..d861b1d9211d5f0e7fb8335eb5fe18ab4a4fac0c 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -45,19 +45,19 @@ public class XmppUri { } } - public XmppUri(Uri uri) { + public XmppUri(final Uri uri) { parse(uri); } - public XmppUri(Uri uri, boolean safeSource) { + public XmppUri(final Uri uri, final boolean safeSource) { this.safeSource = safeSource; parse(uri); } - private static Map parseParameters(final String query, final char seperator) { + private static Map parseParameters(final String query, final char separator) { final ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); final String[] pairs = - query == null ? new String[0] : query.split(String.valueOf(seperator)); + query == null ? new String[0] : query.split(String.valueOf(separator)); for (String pair : pairs) { final String[] parts = pair.split("=", 2); if (parts.length == 0) { @@ -251,7 +251,8 @@ public class XmppUri { public final String fingerprint; final int deviceId; - public Fingerprint(FingerprintType type, String fingerprint, int deviceId) { + public Fingerprint( + final FingerprintType type, final String fingerprint, final int deviceId) { this.type = type; this.fingerprint = fingerprint; this.deviceId = deviceId; From c4e7f9fe30ed8ff655383317d40a70bed00932cb Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 30 Mar 2025 10:14:39 +0200 Subject: [PATCH 107/169] bump image cropper --- build.gradle | 2 +- .../persistance/FileBackend.java | 43 +++--- ...hooseAccountForProfilePictureActivity.java | 5 +- .../conversations/ui/EditAccountActivity.java | 4 + ...ublishGroupChatProfilePictureActivity.java | 50 ++++--- .../ui/PublishProfilePictureActivity.java | 135 ++++++++---------- src/main/res/values/strings.xml | 2 +- 7 files changed, 115 insertions(+), 126 deletions(-) diff --git a/build.gradle b/build.gradle index 6500d21ed67c240b10f0006b88c9c195f78d7895..7a375cdbbd7c8db18222adf538988b330b050aa1 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ dependencies { conversationsPlaystoreImplementation("com.android.installreferrer:installreferrer:2.2") quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.2.0' implementation 'com.github.open-keychain.open-keychain:openpgp-api:v5.7.1' - implementation("com.github.CanHub:Android-Image-Cropper:2.0.0") + implementation("com.vanniktech:android-image-cropper:4.6.0") implementation "androidx.sharetarget:sharetarget:1.2.0" implementation 'androidx.appcompat:appcompat:1.7.0' diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 179d8b7e7c632ca26bb1e2a03f67200e9f2bb283..c7f4e0d24555bf573bf7833b3cef04fd798f1f0c 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -1395,34 +1395,36 @@ public class FileBackend { return Uri.fromFile(getAvatarFile(avatar)); } - public Bitmap cropCenterSquare(Uri image, int size) { + public Bitmap cropCenterSquare(final Uri image, final int size) { if (image == null) { return null; } - InputStream is = null; + final BitmapFactory.Options options = new BitmapFactory.Options(); try { - BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image, size); - is = mXmppConnectionService.getContentResolver().openInputStream(image); + } catch (final IOException | SecurityException e) { + Log.d(Config.LOGTAG, "unable to calculate sample size for " + image, e); + return null; + } + try (final InputStream is = + mXmppConnectionService.getContentResolver().openInputStream(image)) { if (is == null) { return null; } - Bitmap input = BitmapFactory.decodeStream(is, null, options); - if (input == null) { + final var originalBitmap = BitmapFactory.decodeStream(is, null, options); + if (originalBitmap == null) { return null; } else { - input = rotate(input, getRotation(image)); - return cropCenterSquare(input, size); + final var bitmap = rotate(originalBitmap, getRotation(image)); + return cropCenterSquare(bitmap, size); } - } catch (FileNotFoundException | SecurityException e) { - Log.d(Config.LOGTAG, "unable to open file " + image.toString(), e); + } catch (final SecurityException | IOException e) { + Log.d(Config.LOGTAG, "unable to open file " + image, e); return null; - } finally { - close(is); } } - public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + public Bitmap cropCenter(final Uri image, final int newHeight, final int newWidth) { if (image == null) { return null; } @@ -1458,7 +1460,7 @@ public class FileBackend { return dest; } catch (SecurityException e) { return null; // android 6.0 with revoked permissions for example - } catch (FileNotFoundException e) { + } catch (IOException e) { return null; } finally { close(is); @@ -1486,15 +1488,14 @@ public class FileBackend { return output; } - private int calcSampleSize(Uri image, int size) - throws FileNotFoundException, SecurityException { + private int calcSampleSize(final Uri image, int size) throws IOException, SecurityException { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - final InputStream inputStream = - mXmppConnectionService.getContentResolver().openInputStream(image); - BitmapFactory.decodeStream(inputStream, null, options); - close(inputStream); - return calcSampleSize(options, size); + try (final InputStream inputStream = + mXmppConnectionService.getContentResolver().openInputStream(image)) { + BitmapFactory.decodeStream(inputStream, null, options); + return calcSampleSize(options, size); + } } public void updateFileParams(final Message message) { diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java index ff60e6419b2abde89bbc53b6fce4417bb90bef7b..767109a36675111e2a89460d0a05736eab715c80 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseAccountForProfilePictureActivity.java @@ -24,7 +24,7 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity { } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); final ActivityManageAccountsBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_manage_accounts); @@ -64,11 +64,12 @@ public class ChooseAccountForProfilePictureActivity extends XmppActivity { } } - private void goToProfilePictureActivity(Account account) { + private void goToProfilePictureActivity(final Account account) { final Intent startIntent = getIntent(); final Uri uri = startIntent == null ? null : startIntent.getData(); if (uri != null) { Intent intent = new Intent(this, PublishProfilePictureActivity.class); + intent.setAction(Intent.ACTION_ATTACH_DATA); intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString()); intent.setData(uri); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 78b962be510c8535ca26ea6536fe12715a71ec26..728af3cd4a752eb19d04340dfc42661b147dc8e9 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1202,6 +1202,10 @@ public class EditAccountActivity extends OmemoActivity this.binding.accountPassword.setFocusableInTouchMode(editPassword); this.binding.accountPassword.setCursorVisible(editPassword); this.binding.accountPassword.setEnabled(editPassword); + if (!editPassword && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + this.binding.accountJid.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); + this.binding.accountPassword.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO); + } if (!mInitMode) { this.binding.avater.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java index 85a2b8b727909c8aa584f948857d7a9df1a97c04..ab06c24b471bde00c07712bac67e13f34a582987 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishGroupChatProfilePictureActivity.java @@ -29,18 +29,17 @@ package eu.siacs.conversations.ui; -import static eu.siacs.conversations.ui.PublishProfilePictureActivity.REQUEST_CHOOSE_PICTURE; - -import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; -import com.canhub.cropper.CropImage; +import com.canhub.cropper.CropImageContract; +import com.canhub.cropper.CropImageContractOptions; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityPublishProfilePictureBinding; @@ -55,6 +54,15 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity private Conversation conversation; private Uri uri; + final ActivityResultLauncher cropImage = + registerForActivityResult( + new CropImageContract(), + cropResult -> { + if (cropResult.isSuccessful()) { + onAvatarPicked(cropResult.getUriContent()); + } + }); + @Override protected void refreshUiReal() {} @@ -94,8 +102,8 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity configureActionBar(getSupportActionBar()); this.binding.cancelButton.setOnClickListener((v) -> this.finish()); this.binding.secondaryHint.setVisibility(View.GONE); - this.binding.accountImage.setOnClickListener( - (v) -> PublishProfilePictureActivity.chooseAvatar(this)); + this.binding.accountImage.setOnClickListener((v) -> pickAvatar()); + final var intent = getIntent(); final var uuid = intent == null ? null : intent.getStringExtra("uuid"); if (uuid != null) { @@ -111,26 +119,16 @@ public class PublishGroupChatProfilePictureActivity extends XmppActivity xmppConnectionService.publishMucAvatar(conversation, uri, this); } - @Override - public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { - final CropImage.ActivityResult result = CropImage.getActivityResult(data); - if (resultCode == RESULT_OK) { - this.uri = result == null ? null : result.getUri(); - if (xmppConnectionServiceBound) { - reloadAvatar(); - } - } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { - final var error = result == null ? null : result.getError(); - if (error != null) { - Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - } else if (requestCode == REQUEST_CHOOSE_PICTURE) { - if (resultCode == RESULT_OK) { - PublishProfilePictureActivity.cropUri(this, data.getData()); - } + public void pickAvatar() { + this.cropImage.launch( + new CropImageContractOptions( + null, PublishProfilePictureActivity.getCropImageOptions())); + } + + private void onAvatarPicked(final Uri uri) { + this.uri = uri; + if (xmppConnectionServiceBound) { + reloadAvatar(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index ebc8225d80ee0140657b660a0e1f202e25346e44..7901a39bffc853a02cbf5cd60c47ed860b5eeefb 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -1,10 +1,8 @@ package eu.siacs.conversations.ui; -import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.Menu; @@ -12,10 +10,13 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnLongClickListener; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.databinding.DataBindingUtil; -import com.canhub.cropper.CropImage; +import com.canhub.cropper.CropImageContract; +import com.canhub.cropper.CropImageContractOptions; +import com.canhub.cropper.CropImageOptions; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -24,7 +25,6 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; import eu.siacs.conversations.utils.PhoneHelper; -import java.util.concurrent.atomic.AtomicBoolean; public class PublishProfilePictureActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate, OnAvatarPublication { @@ -37,7 +37,6 @@ public class PublishProfilePictureActivity extends XmppActivity private Account account; private boolean support = false; private boolean publishing = false; - private final AtomicBoolean handledExternalUri = new AtomicBoolean(false); private final OnLongClickListener backToDefaultListener = new OnLongClickListener() { @@ -50,6 +49,15 @@ public class PublishProfilePictureActivity extends XmppActivity }; private boolean mInitialAccountSetup; + final ActivityResultLauncher cropImage = + registerForActivityResult( + new CropImageContract(), + cropResult -> { + if (cropResult.isSuccessful()) { + onAvatarPicked(cropResult.getUriContent()); + } + }); + @Override public void onAvatarPublicationSucceeded() { runOnUiThread( @@ -85,6 +93,7 @@ public class PublishProfilePictureActivity extends XmppActivity @Override public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); this.binding = @@ -123,12 +132,10 @@ public class PublishProfilePictureActivity extends XmppActivity } finish(); }); - this.binding.accountImage.setOnClickListener(v -> chooseAvatar(this)); + this.binding.accountImage.setOnClickListener(v -> pickAvatar(null)); this.defaultUri = PhoneHelper.getProfilePictureUri(getApplicationContext()); if (savedInstanceState != null) { this.avatarUri = savedInstanceState.getParcelable("uri"); - this.handledExternalUri.set( - savedInstanceState.getBoolean("handle_external_uri", false)); } } @@ -171,47 +178,32 @@ public class PublishProfilePictureActivity extends XmppActivity if (this.avatarUri != null) { outState.putParcelable("uri", this.avatarUri); } - outState.putBoolean("handle_external_uri", handledExternalUri.get()); super.onSaveInstanceState(outState); } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) { - CropImage.ActivityResult result = CropImage.getActivityResult(data); - if (resultCode == RESULT_OK) { - this.avatarUri = result.getUri(); - if (xmppConnectionServiceBound) { - loadImageIntoPreview(this.avatarUri); - } - } else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) { - Exception error = result.getError(); - if (error != null) { - Toast.makeText(this, error.getMessage(), Toast.LENGTH_SHORT).show(); - } - } - } else if (requestCode == REQUEST_CHOOSE_PICTURE) { - if (resultCode == RESULT_OK) { - cropUri(this, data.getData()); - } - } + public void pickAvatar(final Uri image) { + this.cropImage.launch(new CropImageContractOptions(image, getCropImageOptions())); } - public static void chooseAvatar(final Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - final Intent intent = new Intent(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - activity.startActivityForResult( - Intent.createChooser( - intent, activity.getString(R.string.attach_choose_picture)), - REQUEST_CHOOSE_PICTURE); + public static CropImageOptions getCropImageOptions() { + final var cropImageOptions = new CropImageOptions(); + cropImageOptions.aspectRatioX = 1; + cropImageOptions.aspectRatioY = 1; + cropImageOptions.fixAspectRatio = true; + cropImageOptions.outputCompressFormat = Bitmap.CompressFormat.PNG; + cropImageOptions.imageSourceIncludeCamera = false; + cropImageOptions.minCropResultHeight = Config.AVATAR_SIZE; + cropImageOptions.minCropResultWidth = Config.AVATAR_SIZE; + return cropImageOptions; + } + + private void onAvatarPicked(final Uri uri) { + Log.d(Config.LOGTAG, "onAvatarPicked(" + uri + ")"); + this.avatarUri = uri; + if (xmppConnectionServiceBound) { + loadImageIntoPreview(uri); } else { - CropImage.activity() - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(1, 1) - .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE) - .start(activity); + Log.d(Config.LOGTAG, "not ready during avatarPick"); } } @@ -243,61 +235,54 @@ public class PublishProfilePictureActivity extends XmppActivity public void onStart() { super.onStart(); final Intent intent = getIntent(); - this.mInitialAccountSetup = intent != null && intent.getBooleanExtra("setup", false); - - final Uri uri = intent != null ? intent.getData() : null; + if (intent == null) { + return; + } + this.mInitialAccountSetup = intent.getBooleanExtra("setup", false); - if (uri != null && handledExternalUri.compareAndSet(false, true)) { - cropUri(this, uri); + final var data = intent.getData(); + final var account = intent.getStringExtra(EXTRA_ACCOUNT); + if (Intent.ACTION_ATTACH_DATA.equals(intent.getAction()) + && data != null + && account != null) { + pickAvatar(data); + final var replacement = new Intent(Intent.ACTION_MAIN); + replacement.putExtra(EXTRA_ACCOUNT, account); + setIntent(replacement); return; } if (this.mInitialAccountSetup) { this.binding.cancelButton.setText(R.string.skip); } - configureActionBar( - getSupportActionBar(), !this.mInitialAccountSetup && !handledExternalUri.get()); - } - - public static void cropUri(final Activity activity, final Uri uri) { - CropImage.activity(uri) - .setOutputCompressFormat(Bitmap.CompressFormat.PNG) - .setAspectRatio(1, 1) - .setMinCropResultSize(Config.AVATAR_SIZE, Config.AVATAR_SIZE) - .start(activity); + configureActionBar(getSupportActionBar(), !this.mInitialAccountSetup); } protected void loadImageIntoPreview(final Uri uri) { - - Bitmap bm = null; + Log.d(Config.LOGTAG, "loadImageIntoPreview(" + uri + ")"); + final Bitmap bitmap; if (uri == null) { - bm = + bitmap = avatarService() .get( account, (int) getResources().getDimension(R.dimen.publish_avatar_size)); } else { - try { - bm = - xmppConnectionService - .getFileBackend() - .cropCenterSquare( - uri, - (int) - getResources() - .getDimension(R.dimen.publish_avatar_size)); - } catch (final Exception e) { - Log.d(Config.LOGTAG, "unable to load bitmap into image view", e); - } + bitmap = + xmppConnectionService + .getFileBackend() + .cropCenterSquare( + uri, + (int) getResources().getDimension(R.dimen.publish_avatar_size)); } - if (bm == null) { + if (bitmap == null) { togglePublishButton(false, R.string.publish); this.binding.hintOrWarning.setVisibility(View.VISIBLE); this.binding.hintOrWarning.setText(R.string.error_publish_avatar_converting); return; } - this.binding.accountImage.setImageBitmap(bm); + this.binding.accountImage.setImageBitmap(bitmap); if (support) { togglePublishButton(uri != null, R.string.publish); this.binding.hintOrWarning.setVisibility(View.INVISIBLE); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c8cfeb49493bbee6995daab50f06e1bfcbdc398a..cdc5d5b89a0846acd70d3a045a35838b38c8e0ac 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -843,7 +843,7 @@ e-book Original (uncompressed) Open with… - Conversations profile picture + Avatar Choose account Restore backup Restore From 58a94537418d48c8c01be04b24ee6e69d5806bf1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 30 Mar 2025 12:21:55 +0200 Subject: [PATCH 108/169] add support for fedilinks.org (aka the web+ap URI scheme) --- src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java | 1 + src/main/java/eu/siacs/conversations/utils/Patterns.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index e502780b73c8f1096d5006a335ddf9c48c41edbb..5e768692ac41ac5a32531acc10801955bdfc20b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -62,6 +62,7 @@ public class MyLinkify { case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); case "geo" -> Patterns.URI_GEO.matcher(match).matches(); case "xmpp" -> new XmppUri(Uri.parse(match.toString())).isValidJid(); + case "web+ap" -> Patterns.URI_WEB_AP.matcher(match).matches(); default -> true; }; }; diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index 61d9bb3da035ad6c8f7392c878f9515dd56e70e9..adaf77316ff5f4fc6e32a656a214042e5bd1600a 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -6,13 +6,15 @@ public class Patterns { public static final Pattern URI_GENERIC = Pattern.compile( - "(?<=^|\\s)(tel|xmpp|http|https|geo|mailto):[a-zA-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); + "(?<=^|\\s)(tel|xmpp|http|https|geo|mailto|web\\+ap):[a-zA-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); public static final Pattern URI_TEL = Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); public static final Pattern URI_HTTP = Pattern.compile("https?://\\S+"); + public static final Pattern URI_WEB_AP = Pattern.compile("web\\+ap://\\S+"); + public static Pattern URI_GEO = Pattern.compile( "geo:(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?)(?:,-?\\d+(?:\\.\\d+)?)?(?:;crs=[\\w-]+)?(?:;u=\\d+(?:\\.\\d+)?)?(?:;[\\w-]+=(?:[\\w-_.!~*'()]|%[\\da-f][\\da-f])+)*(\\?z=\\d+)?", From a4ea405c7937591c8baa20736c031ff3de6ec320 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Fri, 28 Mar 2025 16:26:26 +0000 Subject: [PATCH 109/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 10e4003f1cf3124d9e71c7ed9eaf5fb537ef5f3d..454ffa651fba5db1d147809f19f74db07a79a130 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -324,7 +324,7 @@ Serviciul activ în prim-plan Previne închiderea conexiunii de către sistemul de operare Creează o copie de siguranță - Fișierele copiei de siguranță vor fi salvate în %s + Copiile de siguranță vor fi salvate în %s Se creează copia de siguranță Copia de siguranță a fost creată Fișierele copiei de siguranță au fost salvate în %s @@ -1133,4 +1133,5 @@ Document Word Quicksy poate restaura doar copiile de rezervă pentru conturile quicksy.im Restaurare chei OMEMO + Locație copie de siguranță From 910cbe6ffdd996fda89b3389765ecc049bb96bff Mon Sep 17 00:00:00 2001 From: mmbd Date: Sat, 29 Mar 2025 21:31:07 +0000 Subject: [PATCH 110/169] Translated using Weblate (Japanese) Currently translated at 99.3% (1058 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ja/ --- src/main/res/values-ja/strings.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 0292ddf6741943d5e998095d29dbd30ccf571fa9..ef567ca2d36b63cb82196215a286cd0b09cbcc0d 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -242,7 +242,7 @@ グループチャットに参加しています… 退出 連絡先があなたを連絡先リストに追加しました - 戻りを追加 + 連絡先を追加 %s はここまで読みました %s はここまで読みました %1$s +%2$d人がここまで読みました @@ -1092,4 +1092,8 @@ 背景色、文字サイズ、プロフィール画像など ふきだし 接続タイムアウト + OMEMO鍵を復元 + Quicksyはquicksy.imのアカウントのバックアップしか復元できません + バックアップの保存先 + Word 文書 From c48650d98c831f4ae3191448d3788e72aaed6612 Mon Sep 17 00:00:00 2001 From: "lucasmz.dev" Date: Sat, 29 Mar 2025 16:26:26 +0000 Subject: [PATCH 111/169] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index ebad1bf21e15d1d50d6f08ebfa6fb036dfd9841e..6a2573a67c34313acb7ca3ef459f15e0cf1ef2d4 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -281,7 +281,7 @@ Aviso: Enviar isso sem atualizações mútuas de presença pode provocar problemas inesperados.\n\nVerifique nos detalhes do contato suas inscrições de presença. Segurança Correção de mensagem - Permita que seus contatos editem suas mensagens retroativamente. + Permita que seus contatos editem suas mensagens retroativamente Configurações avançadas Por favor, tenha cuidado com isso Sobre o %s From f174db25e2070ebcd14263c11e224ed90eedf243 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Sun, 30 Mar 2025 11:02:02 +0000 Subject: [PATCH 112/169] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index 961785d50a17be87058e9c4cc9fe16ed3cb34aeb..d4edb18cd01f8fcf366e80c40ba1eb3b0b56db36 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -808,7 +808,7 @@ Електронна книга Оригінал (нестиснений) Відкрити… - Зображення профілю для Conversations + Аватар Виберіть обліковий запис Відновити з резервної копії Відновити From 318dc70bd5349792773aac0944411abc5e677e8f Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Sun, 30 Mar 2025 11:02:13 +0000 Subject: [PATCH 113/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index c091eba9d98d6c787c1d891e6858f91367a9a398..f2d022609f24adde04d817a0e769b59a071362fe 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -819,7 +819,7 @@ 电子书 未压缩(原始) 打开… - Conversations 个人资料照片 + 头像 选择账号 恢复备份 恢复 From 3c58b656949bd978a73be613027ebf82eb032b36 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Sun, 30 Mar 2025 11:04:38 +0000 Subject: [PATCH 114/169] Translated using Weblate (Ukrainian) Currently translated at 100.0% (85 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/uk/ --- fastlane/metadata/android/uk/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/4213604.txt diff --git a/fastlane/metadata/android/uk/changelogs/4213604.txt b/fastlane/metadata/android/uk/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..56173813372b4a11484992d791704aa750a65315 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Додано можливість вибору місця для резервних копій +* Більше URI, які можна натиснути (tel:, mailto:) From f09f13270cb486f7835b9146ad6e8b6f55a5620d Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Sun, 30 Mar 2025 11:05:28 +0000 Subject: [PATCH 115/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (85 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/zh_Hans/ --- fastlane/metadata/android/zh-CN/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/zh-CN/changelogs/4213604.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4213604.txt b/fastlane/metadata/android/zh-CN/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..9035ef7dd9eb278412791369472a84253451d012 --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* 增加备份位置选择功能 +* 增加更多可点击的 URI(tel:、mailto:) From bc4c1f30817eff98ae801fd9f076633f001d35e1 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 31 Mar 2025 15:11:40 +0200 Subject: [PATCH 116/169] match URIs for all letters not just ascii --- src/main/java/eu/siacs/conversations/utils/Patterns.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index adaf77316ff5f4fc6e32a656a214042e5bd1600a..5edc6be439e829b2f9473d64549d7b5b105704a9 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -6,7 +6,7 @@ public class Patterns { public static final Pattern URI_GENERIC = Pattern.compile( - "(?<=^|\\s)(tel|xmpp|http|https|geo|mailto|web\\+ap):[a-zA-Z0-9\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); + "(?<=^|\\s)(tel|xmpp|http|https|geo|mailto|web\\+ap):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); public static final Pattern URI_TEL = Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); From 8bfe9a3f76e15cf06c64d26e8d01098d8f2e5e16 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 31 Mar 2025 15:12:44 +0200 Subject: [PATCH 117/169] fix rotation in import backup screen --- .../conversations/ui/ImportBackupActivity.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java index 3a81fcd0a4921c4d55f97e71e47c0df07c209d41..3be499904e12bebe32d911a1b215e151ae767673 100644 --- a/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ImportBackupActivity.java @@ -54,6 +54,7 @@ public class ImportBackupActivity extends ActionBarActivity private BackupFileAdapter backupFileAdapter; private LiveData inProgressImport; + private Uri currentRestoreDialog; private UUID currentWorkRequest; private final ActivityResultLauncher requestPermissions = @@ -92,12 +93,19 @@ public class ImportBackupActivity extends ActionBarActivity if (currentWorkRequest != null) { this.currentWorkRequest = UUID.fromString(currentWorkRequest); } + final var currentRestoreDialog = savedInstanceState.getString("current-restore-dialog"); + if (currentRestoreDialog != null) { + this.currentRestoreDialog = Uri.parse(currentRestoreDialog); + } } monitorWorkRequest(this.currentWorkRequest); this.backupFileAdapter = new BackupFileAdapter(); this.binding.list.setAdapter(this.backupFileAdapter); this.backupFileAdapter.setOnItemClickedListener(this); + if (this.currentRestoreDialog != null) { + openBackupFileFromUri(this.currentRestoreDialog, false); + } } @Override @@ -115,6 +123,9 @@ public class ImportBackupActivity extends ActionBarActivity if (this.currentWorkRequest != null) { bundle.putString("current-work-request", this.currentWorkRequest.toString()); } + if (this.currentRestoreDialog != null) { + bundle.putString("current-restore-dialog", this.currentRestoreDialog.toString()); + } super.onSaveInstanceState(bundle); } @@ -251,6 +262,7 @@ public class ImportBackupActivity extends ActionBarActivity private void showEnterPasswordDialog( final BackupFile backupFile, final boolean finishOnCancel) { + this.currentRestoreDialog = backupFile.getUri(); final DialogEnterPasswordBinding enterPasswordBinding = DataBindingUtil.inflate( LayoutInflater.from(this), R.layout.dialog_enter_password, null, false); @@ -265,6 +277,7 @@ public class ImportBackupActivity extends ActionBarActivity builder.setNegativeButton( R.string.cancel, (dialog, which) -> { + this.currentRestoreDialog = null; if (finishOnCancel) { finish(); } From 387abd5071160c315c2c27321d9081c179c01ee3 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Sun, 30 Mar 2025 14:20:12 +0000 Subject: [PATCH 118/169] Translated using Weblate (Albanian) Currently translated at 99.0% (1055 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index a8d0fa01af287bbca03ec6aaf835e68eb51f60d8..f41db4241bd6c91274da8b5cd18fb1ea2eadbb25 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -623,7 +623,7 @@ e-libër Origjinalja (e pangjeshur) Hape me… - Foto profili Conversations + Avatar Zgjidhni llogari Riktheje kopjeruajtjen Riktheje From fb72b26c926631c17c6256efde1053c9a7cc9d27 Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Sun, 30 Mar 2025 14:27:01 +0000 Subject: [PATCH 119/169] Translated using Weblate (Albanian) Currently translated at 96.4% (82 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/sq/ --- fastlane/metadata/android/sq/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/4213604.txt diff --git a/fastlane/metadata/android/sq/changelogs/4213604.txt b/fastlane/metadata/android/sq/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..3cb0120abf3b4cb64a1eae9e79b8e0117f81d460 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Shtim aftësie për zgjedhje vendndodhje kopjeruajtjesh +* Më tepër URI (tel:, mailto:) të klikueshme From b4b523946033de9d1ec619fd5b15b1568d3c1959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 30 Mar 2025 19:04:55 +0000 Subject: [PATCH 120/169] Translated using Weblate (Estonian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/et/ --- src/main/res/values-et/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 65e041ecfdd85341946f845d78f46e6ceda9de6e..9797d250f9ebb21589c7d994a696827d0778fa10 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -341,7 +341,7 @@ Osalejad Vaid omanik võib muuta vestlusrühma profiilipilte Kontakti nimi - Conversationsi profiilipilt + Tunnuspilt Varukoopiast taastamiseks sisesta %s kasutajakonto salasõna. Selleks, et need, kellel pole sind oma aadressiraamatus, saaksid teada, kes sa oled, siis palun lisa oma nimi. See kanal teeb sinu XMPP-aadressi avalikuks From 80383ee713c5d22e497e1e3dc6a51a92c7af360d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Sun, 30 Mar 2025 19:06:37 +0000 Subject: [PATCH 121/169] Translated using Weblate (Estonian) Currently translated at 100.0% (85 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/et/ --- fastlane/metadata/android/et/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/et/changelogs/4213604.txt diff --git a/fastlane/metadata/android/et/changelogs/4213604.txt b/fastlane/metadata/android/et/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..fd53b2a251c9a7082c6d157f2542dbe25f2e558d --- /dev/null +++ b/fastlane/metadata/android/et/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Lisasime võimaluse valida varukoopia asukohta +* Senisest palju enam URI-sid on klikatavad (tel:, mailto:) From 01376b2cba2dd310ac42f22c1096fd8810765487 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 31 Mar 2025 03:45:09 +0000 Subject: [PATCH 122/169] Translated using Weblate (Hebrew) Currently translated at 32.1% (342 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/he/ --- src/main/res/values-iw/strings.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 54b8e68b8fa91b3f8408bc1068aba468864edf73..4b763097dec19a72e447959caa5f88e8eaf5dc37 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -91,7 +91,7 @@ שם משתמש כבר בשימוש הרשמה הושלמה שרת לא מתאים - לא מוצפן + טקסט נקי OTR OpenPGP OMEMO @@ -343,4 +343,16 @@ האם אתה בטוח שברצונך למחוק את הקובץ הזה?\n\nאזהרה: פעולה זו לא תמחק עותקים של קובץ זה המאוחסנים במכשירים או שרתים אחרים. רינגטון לשיחות נכנסות תקופת החסד + הרישום אינו נתמך על ידי השרת + לקוח לא תואם + אסימון רישום לא חוקי + משא ומתן TLS נכשל + שגיאת פתיחת זרם + משך זמן השתקת התראות לאחר זיהוי פעילות באחד מהמכשירים האחרים שלך. + UI + מפתח הצפנה שגוי. + שגיאה + האפליקציה שבה השתמשת כדי לבחור תמונה זו לא סיפקה מספיק הרשאות לקרוא את הקובץ.\n\nהשתמש במנהל קבצים אחר כדי לבחור תמונה. + עטיפת ערוץ אינה זמינה + הסר את המפתח הציבורי של OpenPGP From 9072fbe420d8de02118c05df60af1e15cdacf057 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 31 Mar 2025 03:22:29 +0000 Subject: [PATCH 123/169] Translated using Weblate (Hebrew) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-conversations/he/ --- .../android/iw-IL/full_description.txt | 40 +++++++++++++++++++ .../android/iw-IL/short_description.txt | 1 + 2 files changed, 41 insertions(+) create mode 100644 src/conversations/fastlane/metadata/android/iw-IL/full_description.txt create mode 100644 src/conversations/fastlane/metadata/android/iw-IL/short_description.txt diff --git a/src/conversations/fastlane/metadata/android/iw-IL/full_description.txt b/src/conversations/fastlane/metadata/android/iw-IL/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..7c0275e7b984d8210432e14985a3ea4fc11f23a6 --- /dev/null +++ b/src/conversations/fastlane/metadata/android/iw-IL/full_description.txt @@ -0,0 +1,40 @@ +קל לשימוש, אמין, ידידותי לסוללה. עם תמיכה מובנית בתמונות, צ'אטים קבוצתיים והצפנת e2e. + +עקרונות עיצוב: + +* עיצוב כמה שיותר יפה וקליל לשימוש מבלי להתפשר על אבטחה או פרטיות +* מסתמך על פרוטוקולים קיימים ומבוססים היטב +* אין צורך בחשבון Google או ספציפית Google Cloud Messaging (GCM) + +* משתמש בכמה שפחות הרשאות + +תכונות: + +* הצפנה מקצה לקצה באמצעות OMEMO או OpenPGP +* שליחת וקבלת תמונות +* שיחות שמע ווידאו מוצפנות (DTLS-SRTP) +* ממשק משתמש אינטואיטיבי העומד בהנחיות לעיצוב אנדרואיד +* תמונות / אווטארים עבור אנשי הקשר שלך +* מסתנכרן עם לקוח שולחן העבודה +* ועידות (עם תמיכה בסימניות) +* שילוב ספר כתובות +* מספר חשבונות / תיבת דואר נכנס מאוחדת +* השפעה נמוכה מאוד על חיי הסוללה + +Conversations מקלה מאוד על יצירת חשבון בשרת conversations.im החינמי. עם זאת, שיחות יעבדו גם עם כל שרת XMPP אחר. שרתי XMPP רבים מנוהלים על ידי מתנדבים והם ללא תשלום. + +תכונות XMPP: + +שיחות עובדות עם כל שרת XMPP בחוץ. עם זאת XMPP הוא פרוטוקול הניתן להרחבה. הרחבות אלה סטנדרטיות גם במה שנקרא XEP's. שיחות תומכות בכמה כאלה כדי לשפר את חווית המשתמש הכוללת. יש סיכוי ששרת ה-XMPP הנוכחי שלך אינו תומך בהרחבות אלו. לכן כדי להפיק את המרב מיישום זה, עליך לשקול לעבור לשרת XMPP שעושה זאת או - אפילו טוב יותר - ליצור שרת XMPP משלך עבורך ועבור חבריך. + +XEPs אלה הם - נכון לעכשיו: + +* XEP-0065: SOCKS5 Bytestreams (או mod_proxy65). ישמש להעברת קבצים אם שני הצדדים נמצאים מאחורי חומת אש או NAT. +* XEP-0163: פרוטוקול אירועים אישיים לאוואטרים +* XEP-0191: פקודת חסימה מאפשרת לך לרשום שולחי דואר זבל או לחסום אנשי קשר מבלי להסיר אותם מהסגל שלך. +* XEP-0198: ניהול זרמים מאפשר ל-XMPP לשרוד הפסקות רשת קטנות ושינויים בחיבור ה-TCP הבסיסי. +* XEP-0280: Message Carbons שמסנכרן אוטומטית את ההודעות שאתה שולח ללקוח שולחן העבודה שלך ובכך מאפשר לך לעבור בצורה חלקה מהלקוח הנייד שלך ללקוח שולחן העבודה שלך ובחזרה תוך שיחה אחת. +* XEP-0237: גרסת רוסטר בעיקר כדי לחסוך ברוחב פס בחיבורים ניידים גרועים +* XEP-0313: ניהול ארכיון הודעות סנכרן את היסטוריית ההודעות עם השרת. התעדכן בהודעות שנשלחו בזמן ששיחות היו במצב לא מקוון. +* XEP-0352: חיווי מצב לקוח מאפשר לשרת לדעת אם שיחות נמצאות ברקע או לא. מאפשר לשרת לחסוך ברוחב פס על ידי מניעת חבילות לא חשובות. +* XEP-0363: העלאת קבצי HTTP מאפשרת לך לשתף קבצים בוועידות ועם אנשי קשר לא מקוונים. דורש רכיב נוסף בשרת שלך. diff --git a/src/conversations/fastlane/metadata/android/iw-IL/short_description.txt b/src/conversations/fastlane/metadata/android/iw-IL/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..f70c43a0288c7ec669b13b28119601f0da3e77fc --- /dev/null +++ b/src/conversations/fastlane/metadata/android/iw-IL/short_description.txt @@ -0,0 +1 @@ +מסנג'ר XMPP מיידי מוצפן וקל לשימוש עבור המכשיר הנייד שלך From 8c1a629a0e90fbd44e6200e35b3b4c52f8c95337 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 31 Mar 2025 06:49:16 +0000 Subject: [PATCH 124/169] Translated using Weblate (German) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 813617bcf976babe7e47306fad17a581e662550b..36316f8e32a297913645891163287fe581663e3d 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -819,7 +819,7 @@ E-Book Original (unkomprimiert) Öffnen mit… - Conversations Profilbild + Profilbild Konto auswählen Sicherung wiederherstellen Wiederherstellung From 0765a37f5714bcbe6d99a3e154fc84468bc2f56d Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 31 Mar 2025 04:06:44 +0000 Subject: [PATCH 125/169] Translated using Weblate (Hebrew) Currently translated at 33.8% (360 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/he/ --- src/main/res/values-iw/strings.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/res/values-iw/strings.xml b/src/main/res/values-iw/strings.xml index 4b763097dec19a72e447959caa5f88e8eaf5dc37..49c2b84ec85f2703b373438c520e7c2a519e9d5c 100644 --- a/src/main/res/values-iw/strings.xml +++ b/src/main/res/values-iw/strings.xml @@ -355,4 +355,22 @@ האפליקציה שבה השתמשת כדי לבחור תמונה זו לא סיפקה מספיק הרשאות לקרוא את הקובץ.\n\nהשתמש במנהל קבצים אחר כדי לבחור תמונה. עטיפת ערוץ אינה זמינה הסר את המפתח הציבורי של OpenPGP + האם אתה בטוח שברצונך להסיר את מפתח OpenPGP הציבורי שלך מהודעת הנוכחות שלך?\nאנשי הקשר שלך לא יוכלו יותר לשלוח לך הודעות מוצפנות OpenPGP. + מפתח ציבורי OpenPGP פורסם. + האם אתה בטוח שברצונך למחוק את חשבונך? מחיקת החשבון שלך מוחקת את כל היסטוריית הצ\'אט שלך + כתובת XMPP + האם ברצונך להוסיף את %s לפנקס הכתובות שלך? + חסום כתובת XMPP + זו אינה כתובת XMPP חוקית + נגמר הזיכרון. תמונה גדולה מדי + XEP-0215: גילוי שירות חיצוני + XEP-0357: דחיפה + נראה לאחרונה לפני דקה + נראה לאחרונה לפני שעה + נראה לאחרונה לפני יום אחד + הודעה מוצפנת. אנא התקן את OpenKeychain כדי לפענח אותו. + נמצאו הודעות מוצפנות חדשות של OpenPGP + מזהה מפתח OpenPGP + XEP-0386: כריכה 2 + XEP-0388: פרופיל SASL הניתן להרחבה From 798af97c06b60f62fc11a9d0c3f19c4a60488f3c Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 31 Mar 2025 08:56:12 +0000 Subject: [PATCH 126/169] Translated using Weblate (Russian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index 25dfa43980575c5e1d4ff6179a92e4100db0d4eb..f8560f4042a97d353ccccbded9079a71d9f4970e 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -847,7 +847,7 @@ Электронная книга Оригинал (без сжатия) Открыть с помощью… - Изображение профиля Conversations + Аватар Выбрать аккаунт Восстановить из резервной копии Восстановить From 1b0b05f448b0f3c17c0d0185cc9a5b8d715da3fa Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Mon, 31 Mar 2025 07:53:22 +0000 Subject: [PATCH 127/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 454ffa651fba5db1d147809f19f74db07a79a130..79e351ebb5b62a7ab89e9adee3c2e2f4a3608090 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -833,7 +833,7 @@ carte electronică Original (necompresat) Deschide cu… - Poză profil Conversations + Poză profil Alegeți contul Restaurează o copie de siguranță Restaurează From e456c7aca77811f643353ba3f2ac94eb50de2487 Mon Sep 17 00:00:00 2001 From: nautilusx Date: Mon, 31 Mar 2025 06:50:20 +0000 Subject: [PATCH 128/169] Translated using Weblate (German) Currently translated at 100.0% (85 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/de/ --- fastlane/metadata/android/de-DE/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/de-DE/changelogs/4213604.txt diff --git a/fastlane/metadata/android/de-DE/changelogs/4213604.txt b/fastlane/metadata/android/de-DE/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd76e882770e35aeedd35465144044c4c3e60f42 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Möglichkeit, den Speicherort für die Datensicherung zu wählen +* Mehr URIs (tel:, mailto:) anklickbar From acaa3b0b94c826233dbcbdb44b40d6ebeae4f4ef Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 31 Mar 2025 08:59:20 +0000 Subject: [PATCH 129/169] Translated using Weblate (Russian) Currently translated at 36.4% (31 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/ru/ --- fastlane/metadata/android/ru-RU/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/ru-RU/changelogs/4213604.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/4213604.txt b/fastlane/metadata/android/ru-RU/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..f87feae64738141a0340423e0b7ace13ff70009f --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Добавлена возможность выбора местоположения резервной копии +* Ещё больше интерактивных URI (tel:, mailto:) From ebaf199cd413b845265a811803a564a77b257118 Mon Sep 17 00:00:00 2001 From: elid34 Date: Mon, 31 Mar 2025 03:56:59 +0000 Subject: [PATCH 130/169] Translated using Weblate (Hebrew) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-quicksy/he/ --- .../android/iw-IL/full_description.txt | 38 +++++++++++++++++++ .../android/iw-IL/short_description.txt | 1 + 2 files changed, 39 insertions(+) create mode 100644 src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt create mode 100644 src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt diff --git a/src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt b/src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..b7ece76fdd4e02268b145fd3a6a90ed35d9747b1 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/iw-IL/full_description.txt @@ -0,0 +1,38 @@ +קל לשימוש, אמין, ידידותי לסוללה. עם תמיכה מובנית בתמונות, צ'אטים קבוצתיים והצפנת e2e. + +עקרונות עיצוב: + +* היה כמה שיותר יפה וקל לשימוש מבלי לוותר על אבטחה או פרטיות +* הסתמכו על פרוטוקולים קיימים ומבוססים היטב +* אין צורך בחשבון Google או ספציפית Google Cloud Messaging (GCM) +* דרוש כמה שפחות הרשאות + +תכונות: + +* הצפנה מקצה לקצה באמצעות OMEMO או OpenPGP +* שליחת וקבלת תמונות +* שיחות שמע ווידאו מוצפנות (DTLS-SRTP) +* ממשק משתמש אינטואיטיבי העומד בהנחיות לעיצוב אנדרואיד +* תמונות / אווטארים עבור אנשי הקשר שלך +* מסתנכרן עם לקוח שולחן העבודה * ועידות (עם תמיכה בסימניות) +* שילוב ספר כתובות +* מספר חשבונות / תיבת דואר נכנס מאוחדת +* השפעה נמוכה מאוד על חיי הסוללה + +Conversations מקלה מאוד על יצירת חשבון בשרת conversations.im החינמי. עם זאת, שיחות יעבדו גם עם כל שרת XMPP אחר. שרתי XMPP רבים מנוהלים על ידי מתנדבים והם ללא תשלום. + +תכונות XMPP: + +Conversations עובדות עם כל שרת XMPP בחוץ. עם זאת XMPP הוא פרוטוקול הניתן להרחבה. הרחבות אלה סטנדרטיות גם במה שנקרא XEP's. שיחות תומכות בכמה כאלה כדי לשפר את חווית המשתמש הכוללת. יש סיכוי ששרת ה-XMPP הנוכחי שלך אינו תומך בהרחבות אלו. כן כדי להפיק את המרב משיחות, עליך לשקול לעבור לשרת XMPP שעושה זאת או - אפילו טוב יותר - להפעיל שרת XMPP משלך עבורך ועבור חבריך. + +XEPs אלה הם - נכון לעכשיו: + +* XEP-0065: SOCKS5 Bytestreams (או mod_proxy65). ישמש להעברת קבצים אם שני הצדדים נמצאים מאחורי חומת אש או NAT. +* XEP-0163: פרוטוקול אירועים אישיים לאוואטרים +* XEP-0191: פקודת חסימה מאפשרת לך לרשום שולחי דואר זבל או לחסום אנשי קשר מבלי להסיר אותם מהסגל שלך. +* XEP-0198: ניהול זרמים מאפשר ל-XMPP לשרוד הפסקות רשת קטנות ושינויים בחיבור ה-TCP הבסיסי. +* XEP-0280: Message Carbons שמסנכרן אוטומטית את ההודעות שאתה שולח ללקוח שולחן העבודה שלך ובכך מאפשר לך לעבור בצורה חלקה מהלקוח הנייד שלך ללקוח שולחן העבודה שלך ובחזרה תוך שיחה אחת. +* XEP-0237: גרסת רוסטר בעיקר כדי לחסוך ברוחב פס בחיבורים ניידים גרועים +* XEP-0313: ניהול ארכיון הודעות סנכרן את היסטוריית ההודעות עם השרת. התעדכן בהודעות שנשלחו בזמן ששיחות היו במצב לא מקוון. +* XEP-0352: חיווי מצב לקוח מאפשר לשרת לדעת אם שיחות נמצאות ברקע או לא. מאפשר לשרת לחסוך ברוחב פס על ידי מניעת חבילות לא חשובות. +* XEP-0363: העלאת קבצי HTTP מאפשרת לך לשתף קבצים בוועידות ועם אנשי קשר לא מקוונים. דורש רכיב נוסף בשרת שלך. diff --git a/src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt b/src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt new file mode 100644 index 0000000000000000000000000000000000000000..7f96c680a2eef1412811cf4472b187953ed3ae48 --- /dev/null +++ b/src/quicksy/fastlane/metadata/android/iw-IL/short_description.txt @@ -0,0 +1 @@ +Jabber/XMPP עם כניסה קלה וגילוי קל From fb4e98242e9f9c712f9050acc052373509b5f1fb Mon Sep 17 00:00:00 2001 From: random_r Date: Tue, 1 Apr 2025 07:04:22 +0000 Subject: [PATCH 131/169] Translated using Weblate (Italian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/it/ --- src/main/res/values-it/strings.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 20b925442ab2de50c5341f62247d1a6b2b5993ef..3e021d6993ae39593b69a20fab3a23d8867920a1 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -322,7 +322,7 @@ Servizio in primo piano Evita che il sistema operativo chiuda la connessione Crea un backup - I file di backup verranno salvati in %s + I backup verranno salvati in %s Creazione dei file di backup Il tuo backup è stato creato I file di backup sono stati salvati in %s @@ -828,7 +828,7 @@ e-book Originale (non compresso) Apri con… - Immagine profilo di Conversations + Avatar Scegli un profilo Ripristina backup Ripristina @@ -1128,4 +1128,5 @@ Documento Word Ripristina chiavi OMEMO Quicksy può ripristinare backup solo per profili quicksy.im + Percorso backup From 4805fba6e46a87280e488182d4ac814ac0695bd5 Mon Sep 17 00:00:00 2001 From: user11 Date: Mon, 31 Mar 2025 19:18:12 +0000 Subject: [PATCH 132/169] Translated using Weblate (Serbian) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sr/ --- src/main/res/values-sr/strings.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index f885961afddb736259ab0a147aa856877e414281..97a6f556749b896171f9d5226d78a3b46924ce8c 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -720,9 +720,9 @@ Прикажи садржај Учесници Прегледач садржаја - Conversations профилна слика + Аватар Отвори користећи… - Не покушавај да вратиш резервну копију коју ниси направио/ла сам! + Враћај само оне резервне копије које си сам/а направио/ла. Направи јавни канал Дозволи било коме да измени тему Подели резервне копије @@ -1005,7 +1005,7 @@ Инсталирај Orbot Врати Унеси своју лозинку за налог %s да вратиш резервну копију. - Не употребљавај функцију враћања резервне копије ради клонирања инсталације (за рад у паралели). Враћање резервне копије је предвиђено само за миграције или у случају да си изгубио/ла оригинални уређај. + Не враћај резервну копију OMEMO кључева ради клонирања инсталације (за рад у паралели). Враћање резервне копије OMEMO кључева је предвиђено само за миграције или у случају да си изгубио/ла оригинални уређај. Није могуће вратити резервну копију. Није могуће дешифровати резервну копију. Да ли је лозинка исправна? Резервна копија и Враћање @@ -1146,4 +1146,7 @@ Покушај поново са P2P Везивање канала недоступно Word документ + Quicksy може да врати резервне копије само за quicksy.im налоге + Локација резервних копија + Врати OMEMO кључеве From 77e94c13341e80dadb6594bde83b9d5f4397ab03 Mon Sep 17 00:00:00 2001 From: random_r Date: Tue, 1 Apr 2025 07:06:06 +0000 Subject: [PATCH 133/169] Translated using Weblate (Italian) Currently translated at 100.0% (85 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/it/ --- fastlane/metadata/android/it-IT/changelogs/4213604.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/it-IT/changelogs/4213604.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4213604.txt b/fastlane/metadata/android/it-IT/changelogs/4213604.txt new file mode 100644 index 0000000000000000000000000000000000000000..c7ac3ec2cb8b4432d4f51ab14d80fb1949a17715 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/4213604.txt @@ -0,0 +1,2 @@ +* Aggiunta la possibilità di scegliere il percorso dei backup +* Più URI (tel:, mailto:) cliccabili From 56d60e9581749618bf1f23aab94432f5f9914166 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Apr 2025 10:25:04 +0200 Subject: [PATCH 134/169] version bump to 2.18.0-beta.2 --- build.gradle | 4 ++-- .../android/de-DE/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/en-US/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/et/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/it-IT/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/ru-RU/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/sq/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/uk/changelogs/{4213604.txt => 4213704.txt} | 0 .../android/zh-CN/changelogs/{4213604.txt => 4213704.txt} | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/de-DE/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/et/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/it-IT/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/ru-RU/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/sq/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{4213604.txt => 4213704.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4213604.txt => 4213704.txt} (100%) diff --git a/build.gradle b/build.gradle index 7a375cdbbd7c8db18222adf538988b330b050aa1..aeda2676648ac706168d1611b7b85895d6f5150a 100644 --- a/build.gradle +++ b/build.gradle @@ -113,8 +113,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42136 - versionName "2.18.0-beta" + versionCode 42137 + versionName "2.18.0-beta.2" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/de-DE/changelogs/4213604.txt b/fastlane/metadata/android/de-DE/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/de-DE/changelogs/4213604.txt rename to fastlane/metadata/android/de-DE/changelogs/4213704.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4213604.txt b/fastlane/metadata/android/en-US/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4213604.txt rename to fastlane/metadata/android/en-US/changelogs/4213704.txt diff --git a/fastlane/metadata/android/et/changelogs/4213604.txt b/fastlane/metadata/android/et/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/et/changelogs/4213604.txt rename to fastlane/metadata/android/et/changelogs/4213704.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4213604.txt b/fastlane/metadata/android/it-IT/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/it-IT/changelogs/4213604.txt rename to fastlane/metadata/android/it-IT/changelogs/4213704.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/4213604.txt b/fastlane/metadata/android/ru-RU/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/ru-RU/changelogs/4213604.txt rename to fastlane/metadata/android/ru-RU/changelogs/4213704.txt diff --git a/fastlane/metadata/android/sq/changelogs/4213604.txt b/fastlane/metadata/android/sq/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/sq/changelogs/4213604.txt rename to fastlane/metadata/android/sq/changelogs/4213704.txt diff --git a/fastlane/metadata/android/uk/changelogs/4213604.txt b/fastlane/metadata/android/uk/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/4213604.txt rename to fastlane/metadata/android/uk/changelogs/4213704.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4213604.txt b/fastlane/metadata/android/zh-CN/changelogs/4213704.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4213604.txt rename to fastlane/metadata/android/zh-CN/changelogs/4213704.txt From 8beb32936a0cafe4ee5ecf9b46ded2b6da6f9a46 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Apr 2025 13:58:07 +0200 Subject: [PATCH 135/169] modify URI regex to better cope with () --- src/main/java/eu/siacs/conversations/utils/Patterns.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/eu/siacs/conversations/utils/Patterns.java index 5edc6be439e829b2f9473d64549d7b5b105704a9..3b858da9ad954f5551c1644e5690758820b8d5a0 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/eu/siacs/conversations/utils/Patterns.java @@ -6,7 +6,7 @@ public class Patterns { public static final Pattern URI_GENERIC = Pattern.compile( - "(?<=^|\\s)(tel|xmpp|http|https|geo|mailto|web\\+ap):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'()*+,;=%]+"); + "(?<=^|\\s|\\()(tel|xmpp|http|https|geo|mailto|web\\+ap):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); public static final Pattern URI_TEL = Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); From 49afa38b71b7b64d549660ff9cb19eaaa70cb662 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 1 Apr 2025 19:10:39 +0200 Subject: [PATCH 136/169] wildcard certificates only match one label Names may contain the wildcard character * which is considered to match any single domain name component or component fragment. E.g., *.a.com matches foo.a.com but not bar.foo.a.com --- .../crypto/DomainHostnameVerifier.java | 11 ----- .../crypto/XmppDomainVerifier.java | 41 ++++++++----------- 2 files changed, 16 insertions(+), 36 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java diff --git a/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java b/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java deleted file mode 100644 index 4349db45ebb624d62def3269e0295b0b038ad0fa..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/crypto/DomainHostnameVerifier.java +++ /dev/null @@ -1,11 +0,0 @@ -package eu.siacs.conversations.crypto; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; - -public interface DomainHostnameVerifier extends HostnameVerifier { - - boolean verify(String domain, String hostname, SSLSession sslSession) throws SSLPeerUnverifiedException; - -} diff --git a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java index 4e43dd93ab487f9159c7261d3aca2c8509b9b5a4..a0b36985e3cd3fe6d298ee69335711dc34e0ec12 100644 --- a/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java +++ b/src/main/java/eu/siacs/conversations/crypto/XmppDomainVerifier.java @@ -2,22 +2,8 @@ package eu.siacs.conversations.crypto; import android.util.Log; import android.util.Pair; - import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; - -import org.bouncycastle.asn1.ASN1Object; -import org.bouncycastle.asn1.ASN1Primitive; -import org.bouncycastle.asn1.ASN1TaggedObject; -import org.bouncycastle.asn1.DERIA5String; -import org.bouncycastle.asn1.DERUTF8String; -import org.bouncycastle.asn1.DLSequence; -import org.bouncycastle.asn1.x500.RDN; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x500.style.IETFUtils; -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; - import java.io.IOException; import java.net.IDN; import java.security.cert.Certificate; @@ -28,9 +14,19 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; - import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1TaggedObject; +import org.bouncycastle.asn1.DERIA5String; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.DLSequence; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; public class XmppDomainVerifier { @@ -82,16 +78,11 @@ public class XmppDomainVerifier { public static boolean matchDomain(final String needle, final List haystack) { for (final String entry : haystack) { if (entry.startsWith("*.")) { - int offset = 0; - while (offset < needle.length()) { - int i = needle.indexOf('.', offset); - if (i < 0) { - break; - } - if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) { - return true; - } - offset = i + 1; + // https://www.rfc-editor.org/rfc/rfc6125#section-6.4.3 + // wild cards can only be in the left most label and don’t match '.' + final int i = needle.indexOf('.'); + if (i != -1 && needle.substring(i).equalsIgnoreCase(entry.substring(1))) { + return true; } } else { if (entry.equalsIgnoreCase(needle)) { From e2b6e9feb919c51bfc4c146426a2f5b1d3abc206 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 2 Apr 2025 16:06:17 +0200 Subject: [PATCH 137/169] support gemini URI scheme; start URIs on punct --- src/main/java/de/gultsch/common/MiniUri.java | 110 ++++++++++++++++++ .../utils => de/gultsch/common}/Patterns.java | 4 +- .../siacs/conversations/entities/Message.java | 2 +- .../ui/ConversationFragment.java | 2 +- .../conversations/ui/util/MyLinkify.java | 2 +- .../siacs/conversations/utils/GeoHelper.java | 1 + .../java/eu/siacs/conversations/utils/IP.java | 1 + .../conversations/xmpp/XmppConnection.java | 3 +- .../java/de/gultsch/common/MiniUriTest.java | 62 ++++++++++ .../java/de/gultsch/common/PatternTest.java | 94 +++++++++++++++ 10 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 src/main/java/de/gultsch/common/MiniUri.java rename src/main/java/{eu/siacs/conversations/utils => de/gultsch/common}/Patterns.java (86%) create mode 100644 src/test/java/de/gultsch/common/MiniUriTest.java create mode 100644 src/test/java/de/gultsch/common/PatternTest.java diff --git a/src/main/java/de/gultsch/common/MiniUri.java b/src/main/java/de/gultsch/common/MiniUri.java new file mode 100644 index 0000000000000000000000000000000000000000..7768e2da60f96e03768759bc544889e317df4ba5 --- /dev/null +++ b/src/main/java/de/gultsch/common/MiniUri.java @@ -0,0 +1,110 @@ +package de.gultsch.common; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.NonNull; + +public class MiniUri { + + private static final String EMPTY_STRING = ""; + + private final String scheme; + private final String authority; + private final String path; + private final Map parameter; + + public MiniUri(final String uri) { + final var schemeAndRest = Splitter.on(':').limit(2).splitToList(uri); + if (schemeAndRest.size() < 2) { + this.scheme = uri; + this.authority = null; + this.path = null; + this.parameter = Collections.emptyMap(); + return; + } + this.scheme = schemeAndRest.get(0); + final var rest = schemeAndRest.get(1); + final var authorityPathAndQuery = Splitter.on('?').limit(2).splitToList(rest); + final var authorityPath = authorityPathAndQuery.get(0); + System.out.println("authorityPath " + authorityPath); + if (authorityPath.length() >= 2 && authorityPath.startsWith("//")) { + final var authorityPathParts = + Splitter.on('/').limit(2).splitToList(authorityPath.substring(2)); + this.authority = authorityPathParts.get(0); + this.path = authorityPathParts.size() == 2 ? authorityPathParts.get(1) : null; + } else { + this.authority = null; + // TODO path ; style path components from something like geo uri + this.path = authorityPath; + } + if (authorityPathAndQuery.size() == 2) { + this.parameter = parseParameters(authorityPathAndQuery.get(1), getDelimiter(scheme)); + } else { + this.parameter = Collections.emptyMap(); + } + } + + private static char getDelimiter(final String scheme) { + return switch (scheme) { + case "xmpp", "geo" -> ';'; + default -> '&'; + }; + } + + private static Map parseParameters(final String query, final char separator) { + final ImmutableMap.Builder builder = new ImmutableMap.Builder<>(); + for (final String pair : Splitter.on(separator).split(query)) { + final String[] parts = pair.split("=", 2); + if (parts.length == 0) { + continue; + } + final String key = parts[0].toLowerCase(Locale.US); + if (parts.length == 2) { + try { + builder.put(key, URLDecoder.decode(parts[1], "UTF-8")); + } catch (final UnsupportedEncodingException e) { + builder.put(key, EMPTY_STRING); + } + } else { + builder.put(key, EMPTY_STRING); + } + } + return builder.build(); + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("scheme", scheme) + .add("authority", authority) + .add("path", path) + .add("parameter", parameter) + .toString(); + } + + public String getScheme() { + return this.scheme; + } + + public String getAuthority() { + return this.authority; + } + + public String getPath() { + return Strings.isNullOrEmpty(this.path) || this.authority == null + ? this.path + : '/' + this.path; + } + + public Map getParameter() { + return this.parameter; + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/Patterns.java b/src/main/java/de/gultsch/common/Patterns.java similarity index 86% rename from src/main/java/eu/siacs/conversations/utils/Patterns.java rename to src/main/java/de/gultsch/common/Patterns.java index 3b858da9ad954f5551c1644e5690758820b8d5a0..617a6b39ce0e023b3ce65acd901b2bc6f20f0b09 100644 --- a/src/main/java/eu/siacs/conversations/utils/Patterns.java +++ b/src/main/java/de/gultsch/common/Patterns.java @@ -1,4 +1,4 @@ -package eu.siacs.conversations.utils; +package de.gultsch.common; import java.util.regex.Pattern; @@ -6,7 +6,7 @@ public class Patterns { public static final Pattern URI_GENERIC = Pattern.compile( - "(?<=^|\\s|\\()(tel|xmpp|http|https|geo|mailto|web\\+ap):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); + "(?<=^|\\p{Zs}|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); public static final Pattern URI_TEL = Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 7516e8caa38fab6cec2b9933d888539cec797b24..c8f3fa0f35b97a7a7c36ab8142a5b0d6c9f4686b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -8,6 +8,7 @@ import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableSet; import com.google.common.primitives.Longs; +import de.gultsch.common.Patterns; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; @@ -18,7 +19,6 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.Emoticons; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.Jid; import java.lang.ref.WeakReference; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index bd3a34555c25f54bd56a757368d380bf7cbdf7be..4bb0ab83b137b3328aab4b7dee7732b27aafc8b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -65,6 +65,7 @@ import androidx.databinding.DataBindingUtil; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import de.gultsch.common.Patterns; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -112,7 +113,6 @@ import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MessageUtils; import eu.siacs.conversations.utils.NickValidityChecker; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PermissionUtils; import eu.siacs.conversations.utils.QuickLoader; import eu.siacs.conversations.utils.StylingHelper; diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index 5e768692ac41ac5a32531acc10801955bdfc20b7..9964b4d9f30f01960a8f0a11673acd39bc849219 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -38,8 +38,8 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import de.gultsch.common.Patterns; import eu.siacs.conversations.ui.text.FixedURLSpan; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.XmppUri; import java.util.Arrays; import java.util.Collection; diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java index 03cc78cee3f9252de5669a3722071a1467ec112d..0616b11f5979693ad95b37f7fe3283ed26027e9b 100644 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; +import de.gultsch.common.Patterns; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversational; diff --git a/src/main/java/eu/siacs/conversations/utils/IP.java b/src/main/java/eu/siacs/conversations/utils/IP.java index 33e989ee3d189d83e2c055b87b768c26b0cff91b..b259ec3dcbb874ae1d9ea4d6f750bb9597ce3a5f 100644 --- a/src/main/java/eu/siacs/conversations/utils/IP.java +++ b/src/main/java/eu/siacs/conversations/utils/IP.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.utils; import com.google.common.net.InetAddresses; +import de.gultsch.common.Patterns; import java.net.InetAddress; public class IP { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 3bb877721a6eafa2ef454864589fec4b29031d71..953e6e1cc176725eb53f6dce0b75e318a03fdd1f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -21,6 +21,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; +import de.gultsch.common.Patterns; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; @@ -49,7 +50,6 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.Patterns; import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Resolver; import eu.siacs.conversations.utils.SSLSockets; @@ -97,7 +97,6 @@ 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; diff --git a/src/test/java/de/gultsch/common/MiniUriTest.java b/src/test/java/de/gultsch/common/MiniUriTest.java new file mode 100644 index 0000000000000000000000000000000000000000..977fcc3710a16167c1af5811f42134a70b488e67 --- /dev/null +++ b/src/test/java/de/gultsch/common/MiniUriTest.java @@ -0,0 +1,62 @@ +package de.gultsch.common; + +import com.google.common.collect.ImmutableMap; +import org.junit.Assert; +import org.junit.Test; + +public class MiniUriTest { + + @Test + public void httpsUrl() { + final var miniUri = new MiniUri("https://example.com"); + Assert.assertEquals("https", miniUri.getScheme()); + Assert.assertEquals("example.com", miniUri.getAuthority()); + Assert.assertNull(miniUri.getPath()); + } + + @Test + public void httpsUrlHtml() { + final var miniUri = new MiniUri("https://example.com/test.html"); + Assert.assertEquals("https", miniUri.getScheme()); + Assert.assertEquals("example.com", miniUri.getAuthority()); + Assert.assertEquals("/test.html", miniUri.getPath()); + } + + @Test + public void httpsUrlCgiFooBar() { + final var miniUri = new MiniUri("https://example.com/test.cgi?foo=bar"); + Assert.assertEquals("https", miniUri.getScheme()); + Assert.assertEquals("example.com", miniUri.getAuthority()); + Assert.assertEquals("/test.cgi", miniUri.getPath()); + Assert.assertEquals(ImmutableMap.of("foo", "bar"), miniUri.getParameter()); + } + + @Test + public void xmppUri() { + final var miniUri = new MiniUri("xmpp:user@example.com"); + Assert.assertEquals("xmpp", miniUri.getScheme()); + Assert.assertNull(miniUri.getAuthority()); + Assert.assertEquals("user@example.com", miniUri.getPath()); + } + + @Test + public void xmppUriJoin() { + final var miniUri = new MiniUri("xmpp:room@chat.example.com?join"); + Assert.assertEquals("xmpp", miniUri.getScheme()); + Assert.assertNull(miniUri.getAuthority()); + Assert.assertEquals("room@chat.example.com", miniUri.getPath()); + Assert.assertEquals(ImmutableMap.of("join", ""), miniUri.getParameter()); + } + + @Test + public void xmppUriMessage() { + final var miniUri = + new MiniUri("xmpp:romeo@montague.net?message;body=Here%27s%20a%20test%20message"); + Assert.assertEquals("xmpp", miniUri.getScheme()); + Assert.assertNull(miniUri.getAuthority()); + Assert.assertEquals("romeo@montague.net", miniUri.getPath()); + Assert.assertEquals( + ImmutableMap.of("message", "", "body", "Here's a test message"), + miniUri.getParameter()); + } +} diff --git a/src/test/java/de/gultsch/common/PatternTest.java b/src/test/java/de/gultsch/common/PatternTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fbff97269fa32bb4ed2e64baaed350b1abbd7391 --- /dev/null +++ b/src/test/java/de/gultsch/common/PatternTest.java @@ -0,0 +1,94 @@ +package de.gultsch.common; + +import com.google.common.collect.ImmutableList; +import java.util.Arrays; +import java.util.regex.MatchResult; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; + +public class PatternTest { + + @Test + public void shortImMessage() { + final var message = + "Hi. I'm refactoring how URIs are linked in Conversations. We now support more URI" + + " schemes like mailto:user@example.com and tel:+1-269-555-0107 and obviously" + + " maintain support for things like" + + " xmpp:conversations@conference.siacs.eu?join and https://example.com however" + + " we no longer link domains that aren't actual URIs like example.com to avoid" + + " some false positives."; + + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertEquals( + Arrays.asList( + "mailto:user@example.com", + "tel:+1-269-555-0107", + "xmpp:conversations@conference.siacs.eu?join", + "https://example.com"), + matches); + } + + @Test + public void ambiguous() { + final var message = + "Please find more information in the corresponding page on Wikipedia" + + " (https://en.wikipedia.org/wiki/Ambiguity_(disambiguation)). Let me know if" + + " you have questions!"; + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertEquals( + ImmutableList.of("https://en.wikipedia.org/wiki/Ambiguity_(disambiguation)"), + matches); + } + + @Test + public void parenthesis() { + final var message = "Daniel is on Mastodon (https://gultsch.social/@daniel)"; + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertEquals(ImmutableList.of("https://gultsch.social/@daniel"), matches); + } + + @Test + public void fullWidthSpace() { + final var message = "\u3000https://conversations.im"; + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches); + } + + @Test + public void fullWidthColon() { + final var message = "\uFF1Ahttps://conversations.im"; + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches); + } +} From 3deea5211f6979a9a003a343455805589ac0b657 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Wed, 2 Apr 2025 20:24:29 +0200 Subject: [PATCH 138/169] make copy uri context menu work with new schemes --- build.gradle | 4 + src/main/java/de/gultsch/common/MiniUri.java | 6 ++ src/main/java/de/gultsch/common/Patterns.java | 2 +- .../ui/ConversationFragment.java | 24 ++++-- .../conversations/ui/util/MyLinkify.java | 74 +++++++------------ .../conversations/ui/util/ShareUtil.java | 73 ++++++++---------- src/main/res/values/strings.xml | 8 ++ .../java/de/gultsch/common/PatternTest.java | 26 +++++++ 8 files changed, 120 insertions(+), 97 deletions(-) diff --git a/build.gradle b/build.gradle index aeda2676648ac706168d1611b7b85895d6f5150a..b9986e588fd557816da36d13e470427873ba41f3 100644 --- a/build.gradle +++ b/build.gradle @@ -99,6 +99,10 @@ dependencies { implementation 'com.google.guava:guava:33.4.0-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.13.52' implementation 'im.conversations.webrtc:webrtc-android:129.0.0' + + //Testing + testImplementation 'junit:junit:4.13.2' + } ext { diff --git a/src/main/java/de/gultsch/common/MiniUri.java b/src/main/java/de/gultsch/common/MiniUri.java index 7768e2da60f96e03768759bc544889e317df4ba5..6ea24a90dd6d1dd497f03813e64cbc7dda7bf7e1 100644 --- a/src/main/java/de/gultsch/common/MiniUri.java +++ b/src/main/java/de/gultsch/common/MiniUri.java @@ -15,12 +15,14 @@ public class MiniUri { private static final String EMPTY_STRING = ""; + private final String raw; private final String scheme; private final String authority; private final String path; private final Map parameter; public MiniUri(final String uri) { + this.raw = uri; final var schemeAndRest = Splitter.on(':').limit(2).splitToList(uri); if (schemeAndRest.size() < 2) { this.scheme = uri; @@ -104,6 +106,10 @@ public class MiniUri { : '/' + this.path; } + public String getRaw() { + return this.raw; + } + public Map getParameter() { return this.parameter; } diff --git a/src/main/java/de/gultsch/common/Patterns.java b/src/main/java/de/gultsch/common/Patterns.java index 617a6b39ce0e023b3ce65acd901b2bc6f20f0b09..36245b4d8aa80bb4f50af79a8d2676eac3532859 100644 --- a/src/main/java/de/gultsch/common/Patterns.java +++ b/src/main/java/de/gultsch/common/Patterns.java @@ -6,7 +6,7 @@ public class Patterns { public static final Pattern URI_GENERIC = Pattern.compile( - "(?<=^|\\p{Zs}|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); + "(?<=^|\\p{Z}|\\s|\\p{P})(tel|xmpp|http|https|geo|mailto|web\\+ap|gemini):[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+(\\([\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]+\\))*[\\p{L}\\p{M}\\p{N}\\-._~:/?#\\[\\]@!$&'*+,;=%]*"); public static final Pattern URI_TEL = Pattern.compile("^tel:\\+?(\\d{1,4}[-./()\\s]?)*\\d{1,4}(;.*)?$"); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 4bb0ab83b137b3328aab4b7dee7732b27aafc8b7..cc6dfcd19178e3f7745010fa3cb17f94f3a6362d 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -31,7 +31,6 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.text.Editable; -import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; @@ -65,6 +64,7 @@ import androidx.databinding.DataBindingUtil; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import de.gultsch.common.Patterns; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -100,6 +100,7 @@ import eu.siacs.conversations.ui.util.EditMessageActionModeCallback; import eu.siacs.conversations.ui.util.ListViewUtils; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; +import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.ScrollState; @@ -1347,13 +1348,22 @@ public class ConversationFragment extends XmppFragment && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && !MessageUtils.prepareQuote(m).isEmpty()); - final String scheme = - ShareUtil.getLinkScheme(new SpannableStringBuilder(m.getBody())); - if ("xmpp".equals(scheme)) { - copyLink.setTitle(R.string.copy_jabber_id); - copyLink.setVisible(true); - } else if (scheme != null) { + final var firstUri = Iterables.getFirst(MyLinkify.getLinks(m.getBody()), null); + if (firstUri != null) { + final var scheme = firstUri.getScheme(); + final @StringRes int resForScheme = + switch (scheme) { + case "xmpp" -> R.string.copy_jabber_id; + case "http", "https", "gemini" -> R.string.copy_link; + case "geo" -> R.string.copy_geo_uri; + case "tel" -> R.string.copy_telephone_number; + case "mailto" -> R.string.copy_email_address; + default -> R.string.copy_URI; + }; + copyLink.setTitle(resForScheme); copyLink.setVisible(true); + } else { + copyLink.setVisible(false); } } if (m.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED && !deleted) { diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index 9964b4d9f30f01960a8f0a11673acd39bc849219..d9d2055e4c4a44770588983c2a21e36c7a16ba4c 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -31,72 +31,50 @@ package eu.siacs.conversations.ui.util; import android.net.Uri; import android.text.Editable; -import android.text.style.URLSpan; import android.text.util.Linkify; import com.google.common.base.Splitter; -import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; +import de.gultsch.common.MiniUri; import de.gultsch.common.Patterns; import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.XmppUri; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; import java.util.List; -import java.util.Objects; public class MyLinkify { private static final Linkify.MatchFilter MATCH_FILTER = - (s, start, end) -> { - final var match = s.subSequence(start, end); - final var scheme = - Iterables.getFirst(Splitter.on(':').limit(2).splitToList(match), null); - if (scheme == null) { - return false; - } - return switch (scheme) { - case "tel" -> Patterns.URI_TEL.matcher(match).matches(); - case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); - case "geo" -> Patterns.URI_GEO.matcher(match).matches(); - case "xmpp" -> new XmppUri(Uri.parse(match.toString())).isValidJid(); - case "web+ap" -> Patterns.URI_WEB_AP.matcher(match).matches(); - default -> true; - }; - }; + (s, start, end) -> isPassAdditionalValidation(s.subSequence(start, end).toString()); + + private static boolean isPassAdditionalValidation(final String match) { + final var scheme = Iterables.getFirst(Splitter.on(':').limit(2).splitToList(match), null); + if (scheme == null) { + return false; + } + return switch (scheme) { + case "tel" -> Patterns.URI_TEL.matcher(match).matches(); + case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); + case "geo" -> Patterns.URI_GEO.matcher(match).matches(); + case "xmpp" -> new XmppUri(Uri.parse(match)).isValidJid(); + case "web+ap" -> Patterns.URI_WEB_AP.matcher(match).matches(); + default -> true; + }; + } public static void addLinks(final Editable body) { Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null); FixedURLSpan.fix(body); } - public static List extractLinks(final Editable body) { - MyLinkify.addLinks(body); - final Collection spans = - Arrays.asList(body.getSpans(0, body.length() - 1, URLSpan.class)); - final Collection urlWrappers = - Collections2.filter( - Collections2.transform( - spans, - s -> - s == null - ? null - : new UrlWrapper(body.getSpanStart(s), s.getURL())), - Objects::nonNull); - List sorted = - ImmutableList.sortedCopyOf(Comparator.comparingInt(a -> a.position), urlWrappers); - return Lists.transform(sorted, uw -> uw.url); - } - - private static class UrlWrapper { - private final int position; - private final String url; - - private UrlWrapper(int position, String url) { - this.position = position; - this.url = url; + public static List getLinks(final String body) { + final var builder = new ImmutableList.Builder(); + final var matcher = Patterns.URI_GENERIC.matcher(body); + while (matcher.find()) { + final var match = matcher.group(); + if (isPassAdditionalValidation(match)) { + builder.add(new MiniUri(match)); + } } + return builder.build(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index bee0734676d1d252365f7ad3815bdd3110a01b5f..1602b225597ab29851acc876c93d6fe0ff18bd6f 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -31,20 +31,23 @@ package eu.siacs.conversations.ui.util; import android.content.ActivityNotFoundException; import android.content.Intent; -import android.net.Uri; -import android.text.SpannableStringBuilder; import android.widget.Toast; +import androidx.annotation.StringRes; +import com.google.common.collect.Iterables; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.XmppActivity; -import eu.siacs.conversations.utils.XmppUri; -import eu.siacs.conversations.xmpp.Jid; +import java.util.Arrays; +import java.util.Collection; public class ShareUtil { + private static final Collection SCHEMES_COPY_PATH_ONLY = + Arrays.asList("xmpp", "mailto", "tel"); + public static void share(XmppActivity activity, Message message) { Intent shareIntent = new Intent(); shareIntent.setAction(Intent.ACTION_SEND); @@ -120,44 +123,32 @@ public class ShareUtil { } public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final SpannableStringBuilder body = new SpannableStringBuilder(message.getBody()); - for (final String url : MyLinkify.extractLinks(body)) { - final Uri uri = Uri.parse(url); - if ("xmpp".equals(uri.getScheme())) { - try { - final Jid jid = new XmppUri(uri).getJid(); - if (activity.copyTextToClipboard( - jid.asBareJid().toString(), R.string.account_settings_jabber_id)) { - Toast.makeText( - activity, - R.string.jabber_id_copied_to_clipboard, - Toast.LENGTH_SHORT) - .show(); - } - return; - } catch (final Exception e) { - return; - } - } else { - if (activity.copyTextToClipboard(url, R.string.web_address)) { - Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT) - .show(); - } - return; - } + final var firstUri = Iterables.getFirst(MyLinkify.getLinks(message.getBody()), null); + if (firstUri == null) { + return; } - } - - public static String getLinkScheme(final SpannableStringBuilder body) { - MyLinkify.addLinks(body); - for (final String url : MyLinkify.extractLinks(body)) { - final Uri uri = Uri.parse(url); - if ("xmpp".equals(uri.getScheme())) { - return uri.getScheme(); - } else { - return "http"; - } + final String clip; + if (SCHEMES_COPY_PATH_ONLY.contains(firstUri.getScheme())) { + clip = firstUri.getPath(); + } else { + clip = firstUri.getRaw(); + } + final @StringRes int label = + switch (firstUri.getScheme()) { + case "http", "https", "gemini" -> R.string.web_address; + case "xmpp" -> R.string.account_settings_jabber_id; + default -> R.string.uri; + }; + final @StringRes int toast = + switch (firstUri.getScheme()) { + case "http", "https", "gemini", "web+ap" -> R.string.url_copied_to_clipboard; + case "xmpp" -> R.string.jabber_id_copied_to_clipboard; + case "tel" -> R.string.copied_phone_number; + case "mailto" -> R.string.copied_email_address; + default -> R.string.uri_copied_to_clipboard; + }; + if (activity.copyTextToClipboard(clip, label)) { + Toast.makeText(activity, toast, Toast.LENGTH_SHORT).show(); } - return null; } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index cdc5d5b89a0846acd70d3a045a35838b38c8e0ac..af782605d13a0e14dcb03da9f019dee80291f4e3 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -319,6 +319,8 @@ Retry with P2P File URL Copied URL to clipboard + Copied URI to clipboard + URI Copied XMPP address to clipboard Copied error message to clipboard web address @@ -747,7 +749,13 @@ Share Location Plugin Use the Share Location Plugin instead of the built-in map Copy web address + Copy URI + Copy phone number + Copy geo location Copy XMPP address + Copy email address + Copied email address to clipboard + Copied phone number to clipboard HTTP File Sharing for S3 Direct Search At ‘New chat’ screen open keyboard and place cursor in search field diff --git a/src/test/java/de/gultsch/common/PatternTest.java b/src/test/java/de/gultsch/common/PatternTest.java index fbff97269fa32bb4ed2e64baaed350b1abbd7391..db18fc6c98322e530a0bd71a003389d5d2f36077 100644 --- a/src/test/java/de/gultsch/common/PatternTest.java +++ b/src/test/java/de/gultsch/common/PatternTest.java @@ -91,4 +91,30 @@ public class PatternTest { Assert.assertEquals(ImmutableList.of("https://conversations.im"), matches); } + + @Test + public void newLine() { + final var message = "\nxmpp:example.com"; + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertEquals(ImmutableList.of("xmpp:example.com"), matches); + } + + @Test + public void code() { + final var message = "`xmpp:example.com`"; + final var matches = + Patterns.URI_GENERIC + .matcher(message) + .results() + .map(MatchResult::group) + .collect(Collectors.toList()); + + Assert.assertTrue(matches.isEmpty()); + } } From 9e7835558d0d612aaf3880f4513fe0e4a2d65b0c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 3 Apr 2025 08:40:39 +0200 Subject: [PATCH 139/169] toast after copy to clipboard is unnecessary on Android 13+ --- .../conversations/ui/ConversationFragment.java | 14 ++++++++------ .../eu/siacs/conversations/ui/OmemoActivity.java | 6 ++++-- .../eu/siacs/conversations/ui/util/ShareUtil.java | 14 +++++++++----- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index cc6dfcd19178e3f7745010fa3cb17f94f3a6362d..85c96e0aad375a3ae2f8f135425f24dfb879f052 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -2185,12 +2185,14 @@ public class ConversationFragment extends XmppFragment builder.setNegativeButton( R.string.copy_to_clipboard, (dialog, which) -> { - activity.copyTextToClipboard(displayError, R.string.error_message); - Toast.makeText( - activity, - R.string.error_message_copied_to_clipboard, - Toast.LENGTH_SHORT) - .show(); + if (activity.copyTextToClipboard(displayError, R.string.error_message) + && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Toast.makeText( + activity, + R.string.error_message_copied_to_clipboard, + Toast.LENGTH_SHORT) + .show(); + } }); builder.setPositiveButton(R.string.confirm, null); builder.create().show(); diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index f3225b9ecda2db88eed0bec3f9525a8599f4dc36..c78cf4dbc6be4972fb5ffabf3def9c8cf136158d 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.ui; import android.content.Intent; +import android.os.Build; import android.view.ContextMenu; import android.view.MenuItem; import android.view.View; @@ -92,8 +93,9 @@ public abstract class OmemoActivity extends XmppActivity { protected void copyOmemoFingerprint(String fingerprint) { if (copyTextToClipboard( - CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), - R.string.omemo_fingerprint)) { + CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), + R.string.omemo_fingerprint) + && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { Toast.makeText(this, R.string.toast_message_omemo_fingerprint, Toast.LENGTH_SHORT) .show(); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 1602b225597ab29851acc876c93d6fe0ff18bd6f..0a4707074ac88e616489d6c97719a6ab4f099b54 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -31,6 +31,7 @@ package eu.siacs.conversations.ui.util; import android.content.ActivityNotFoundException; import android.content.Intent; +import android.os.Build; import android.widget.Toast; import androidx.annotation.StringRes; import com.google.common.collect.Iterables; @@ -93,14 +94,15 @@ public class ShareUtil { } } - public static void copyToClipboard(XmppActivity activity, Message message) { - if (activity.copyTextToClipboard(message.getBody(), R.string.message)) { + public static void copyToClipboard(final XmppActivity activity, final Message message) { + if (activity.copyTextToClipboard(message.getBody(), R.string.message) + && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT) .show(); } } - public static void copyUrlToClipboard(XmppActivity activity, Message message) { + public static void copyUrlToClipboard(final XmppActivity activity, final Message message) { final String url; final int resId; if (message.isGeoUri()) { @@ -117,7 +119,8 @@ public class ShareUtil { : message.getBody().trim(); resId = R.string.file_url; } - if (activity.copyTextToClipboard(url, resId)) { + if (activity.copyTextToClipboard(url, resId) + && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } } @@ -147,7 +150,8 @@ public class ShareUtil { case "mailto" -> R.string.copied_email_address; default -> R.string.uri_copied_to_clipboard; }; - if (activity.copyTextToClipboard(clip, label)) { + if (activity.copyTextToClipboard(clip, label) + && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { Toast.makeText(activity, toast, Toast.LENGTH_SHORT).show(); } } From b9248f2581487c2ea18460a08936f5cc4596f4e1 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 1 Apr 2025 11:18:40 +0000 Subject: [PATCH 140/169] Translated using Weblate (Spanish) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/es/ --- src/main/res/values-es/strings.xml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 878c959ef6b3f6d95efce98d40f9fafa75ae9bef..8315f5b3b9f901dfd4add28d94822f3b20eb8742 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -320,7 +320,7 @@ Servicio en primer plano Mantener el servicio en primer plano previene que el sistema cierre la conexión Crear una copia de respaldo - Los ficheros de respaldo serán almacenados en %s + Las copias de seguridad se almacenarán en %s Creando los ficheros de respaldo Tu copia de respaldo ha sido creada Los ficheros de respaldo han sido almacenados en %s @@ -826,12 +826,12 @@ e-book Original (sin comprimir) Abrir con… - Foto de perfil de Conversations + Avatar Elige una cuenta Restaurar copia de respaldo Restaurar Introduce tu contraseña para la cuenta %s para restaurar la copia de respaldo. - No utilices la opción de restaurar una copia de respaldo para clonar (ejecutar simultáneamente) una instalación. Restaurar una copia de respaldo se debe utilizar solo para migraciones o en caso de que hayas perdido el dispositivo original. + No restaures claves OMEMO en un intento de clonar (ejecutar simultáneamente) una instalación. La restauración de claves OMEMO solo está pensada para migraciones o en caso de que hayas perdido el dispositivo original. No se ha podido restaurar la copia de respaldo. No se ha podido descifrar la copia de respaldo. ¿Es la contraseña correcta? Respaldar & Restaurar @@ -999,7 +999,7 @@ No se pudo eliminar la cuenta del servidor Conversaciones grupales Buscar un grupo de chats - ¡No intentes restaurar las copias de seguridad que no creaste tu mismo! + Restaura solo las copias de seguridad que hayas creado personalmente. Estás intentando importar un formato de copia de seguridad obsoleto Audiolibro Reconectarse a otros hosts @@ -1123,4 +1123,8 @@ Se agotó el tiempo de espera de la conexión Reintentar con P2P documento de Word + Quicksy solo puede restaurar las copias de seguridad de las cuentas quicksy.im + Ubicación de la copia de seguridad + Restaurar claves OMEMO + Canal no disponible From 70de80d0f98f571845d075ef8bba786df7c50ed4 Mon Sep 17 00:00:00 2001 From: ghose Date: Wed, 2 Apr 2025 05:37:38 +0000 Subject: [PATCH 141/169] Translated using Weblate (Galician) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index f71b61f29d117abdad2b303f3ce1142f49957c9a..dda046bf0177c8e3f95d847662fe4e9e4c13f26d 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -819,7 +819,7 @@ e-book Orixinal (non comprimido) Abrir con… - Imaxe de perfil en Conversations + Avatar Elixir conta Restablecer copia de apoio Restablecer From 8b21af41cdebb72f526cf1fb26c0a08bcba13d7d Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Tue, 1 Apr 2025 10:26:50 +0000 Subject: [PATCH 142/169] Translated using Weblate (Polish) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 65d7696c8e10912832de06bc95f97924f6798c8b..25960ea742bf7aaf6b61cad2fd2b050d9a2c7424 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -841,7 +841,7 @@ e-book Oryginalne (nieskompresowane) Otwórz za pomocą… - Obrazek profilowy Conversations + Awatar Wybierz konto Przywróć kopię zapasową Przywróć From 3f13b0b4a413fe8d017f9723fa62abb9b6c2d7e5 Mon Sep 17 00:00:00 2001 From: "lucasmz.dev" Date: Wed, 2 Apr 2025 01:02:18 +0000 Subject: [PATCH 143/169] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1065 of 1065 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index 6a2573a67c34313acb7ca3ef459f15e0cf1ef2d4..ac4f1ab512cea0975441556089d48fd00267e73c 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -832,7 +832,7 @@ e-book Original (não comprimido) Abrir com… - Imagem de perfil do Conversations + Avatar Selecione a conta Restaurar o backup Restaurar From eebfae6f6155dc5fdfd295cd8a883fcdbd831036 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Tue, 1 Apr 2025 10:27:39 +0000 Subject: [PATCH 144/169] Translated using Weblate (Polish) Currently translated at 55.2% (47 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/pl/ --- fastlane/metadata/android/pl-PL/changelogs/4213704.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/pl-PL/changelogs/4213704.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/4213704.txt b/fastlane/metadata/android/pl-PL/changelogs/4213704.txt new file mode 100644 index 0000000000000000000000000000000000000000..f5a19e8e06e37d785e42ff23ebadb5a80e17eef1 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/4213704.txt @@ -0,0 +1,2 @@ +* Dodanie możliwości wybierania lokalizacji kopii zapasowej +* Więcej klikalnych URI (tel:, mailto:) From b0d1b4acbeb1ef57136900b70b4b2bec1341b2fb Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Tue, 1 Apr 2025 08:51:34 +0000 Subject: [PATCH 145/169] Translated using Weblate (Albanian) Currently translated at 98.8% (84 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/sq/ --- fastlane/metadata/android/sq/changelogs/4213304.txt | 1 + fastlane/metadata/android/sq/changelogs/4213404.txt | 1 + 2 files changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/sq/changelogs/4213304.txt create mode 100644 fastlane/metadata/android/sq/changelogs/4213404.txt diff --git a/fastlane/metadata/android/sq/changelogs/4213304.txt b/fastlane/metadata/android/sq/changelogs/4213304.txt new file mode 100644 index 0000000000000000000000000000000000000000..37d2a41be0bd9832b3fcf406dfab2e74577394c6 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4213304.txt @@ -0,0 +1 @@ +* ndreqje të metash të vockla diff --git a/fastlane/metadata/android/sq/changelogs/4213404.txt b/fastlane/metadata/android/sq/changelogs/4213404.txt new file mode 100644 index 0000000000000000000000000000000000000000..f91f419eee918a32010b3be145aec8c355c685d0 --- /dev/null +++ b/fastlane/metadata/android/sq/changelogs/4213404.txt @@ -0,0 +1 @@ +* Ndreqje vithisjeje te shpërngulje kartelash nën përkthimin fi From 716cb17c4d0a56ec72ec55fea1c934af5db3d8b8 Mon Sep 17 00:00:00 2001 From: ghose Date: Wed, 2 Apr 2025 05:37:08 +0000 Subject: [PATCH 146/169] Translated using Weblate (Galician) Currently translated at 67.0% (57 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/gl/ --- fastlane/metadata/android/gl-ES/changelogs/4213704.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 fastlane/metadata/android/gl-ES/changelogs/4213704.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4213704.txt b/fastlane/metadata/android/gl-ES/changelogs/4213704.txt new file mode 100644 index 0000000000000000000000000000000000000000..0bee968adc39303afd3eb9f98068ccabd24083ec --- /dev/null +++ b/fastlane/metadata/android/gl-ES/changelogs/4213704.txt @@ -0,0 +1,2 @@ +* Elixir onde se garda a copia de apoio +* Máis URIs (tel:, mailto:) seleccionables From 6f2cf30bbda71a201d1d9a26497d30a734f541aa Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 1 Apr 2025 11:22:58 +0000 Subject: [PATCH 147/169] Translated using Weblate (Spanish) Currently translated at 100.0% (2 of 2 strings) Translation: Conversations/App Store Metadata (Conversations) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata-conversations/es/ --- .../fastlane/metadata/android/es-ES/short_description.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversations/fastlane/metadata/android/es-ES/short_description.txt b/src/conversations/fastlane/metadata/android/es-ES/short_description.txt index 7ed04767246cd7fab459ea0ebc43836a01ddd32f..cbbe5b2a935801ec7b35f8e4cfe5bc0eefc1a466 100644 --- a/src/conversations/fastlane/metadata/android/es-ES/short_description.txt +++ b/src/conversations/fastlane/metadata/android/es-ES/short_description.txt @@ -1 +1 @@ -Mensajería instantánea XMPP cifrada y fácil de usar para tu dispositivo móvil +Un cliente XMPP encriptado y fácil de usar para tu dispositivo móvil From 6c68abb710bd9cbe98820ec3bd5bfa310b24c9cc Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 3 Apr 2025 10:04:03 +0200 Subject: [PATCH 148/169] simplify code around bundled trust manager --- .../crypto/BundledTrustManager.java | 65 ----------------- .../crypto/CombiningTrustManager.java | 23 ++---- .../conversations/crypto/TrustManagers.java | 27 ++++--- .../http/HttpConnectionManager.java | 72 ++++++++++--------- .../services/MemorizingTrustManager.java | 20 ++---- 5 files changed, 67 insertions(+), 140 deletions(-) delete mode 100644 src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java diff --git a/src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java b/src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java deleted file mode 100644 index 9eb20cc300f412914dc9b8a2b97ecc72b92ce143..0000000000000000000000000000000000000000 --- a/src/main/java/eu/siacs/conversations/crypto/BundledTrustManager.java +++ /dev/null @@ -1,65 +0,0 @@ -package eu.siacs.conversations.crypto; - -import java.io.IOException; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.X509TrustManager; - -public class BundledTrustManager implements X509TrustManager { - - private final X509TrustManager delegate; - - private BundledTrustManager(final KeyStore keyStore) - throws NoSuchAlgorithmException, KeyStoreException { - this.delegate = TrustManagers.createTrustManager(keyStore); - } - - public static Builder builder() throws KeyStoreException { - return new Builder(); - } - - @Override - public void checkClientTrusted(final X509Certificate[] chain, final String authType) - throws CertificateException { - this.delegate.checkClientTrusted(chain, authType); - } - - @Override - public void checkServerTrusted(final X509Certificate[] chain, final String authType) - throws CertificateException { - this.delegate.checkServerTrusted(chain, authType); - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return this.delegate.getAcceptedIssuers(); - } - - public static class Builder { - - private KeyStore keyStore; - - private Builder() {} - - public Builder loadKeyStore(final InputStream inputStream, final String password) - throws CertificateException, IOException, NoSuchAlgorithmException, - KeyStoreException { - if (this.keyStore != null) { - throw new IllegalStateException("KeyStore has already been loaded"); - } - final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(inputStream, password.toCharArray()); - this.keyStore = keyStore; - return this; - } - - public BundledTrustManager build() throws NoSuchAlgorithmException, KeyStoreException { - return new BundledTrustManager(keyStore); - } - } -} diff --git a/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java b/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java index 0f3c0e04415e506748908ad818613064bc3cbc50..7126004877cf49388dcf003c29cc29eea7f8b0ed 100644 --- a/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java +++ b/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java @@ -1,9 +1,9 @@ package eu.siacs.conversations.crypto; +import android.annotation.SuppressLint; import android.util.Log; - import com.google.common.collect.ImmutableList; - +import eu.siacs.conversations.Config; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; @@ -11,12 +11,10 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Iterator; import java.util.List; - import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.Config; - -public class CombiningTrustManager implements X509TrustManager { +@SuppressLint("CustomX509TrustManager") +public final class CombiningTrustManager implements X509TrustManager { private final List trustManagers; @@ -32,6 +30,7 @@ public class CombiningTrustManager implements X509TrustManager { final X509TrustManager trustManager = iterator.next(); try { trustManager.checkClientTrusted(chain, authType); + return; } catch (final CertificateException certificateException) { if (iterator.hasNext()) { continue; @@ -39,6 +38,7 @@ public class CombiningTrustManager implements X509TrustManager { throw certificateException; } } + throw new CertificateException("No trust managers configured"); } @Override @@ -50,29 +50,20 @@ public class CombiningTrustManager implements X509TrustManager { + " is configured with " + this.trustManagers.size() + " TrustManagers"); - int i = 0; for (final Iterator iterator = this.trustManagers.iterator(); iterator.hasNext(); ) { final X509TrustManager trustManager = iterator.next(); try { trustManager.checkServerTrusted(chain, authType); - Log.d( - Config.LOGTAG, - "certificate check passed on " + trustManager.getClass().getName()+". chain length was "+chain.length); return; } catch (final CertificateException certificateException) { - Log.d( - Config.LOGTAG, - "failed to verify in [" + i + "]/" + trustManager.getClass().getName(), - certificateException); if (iterator.hasNext()) { continue; } throw certificateException; - } finally { - ++i; } } + throw new CertificateException("No trust managers configured"); } @Override diff --git a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java b/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java index 4fc11eecf349979d57a77b61c73a5015fcff7e18..f70545fc950065c81a068d210b76e4a8eb5048fb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java +++ b/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java @@ -1,25 +1,23 @@ package eu.siacs.conversations.crypto; import android.content.Context; - import androidx.annotation.Nullable; - import com.google.common.collect.Iterables; - +import eu.siacs.conversations.R; import java.io.IOException; +import java.io.InputStream; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.Arrays; - import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import eu.siacs.conversations.R; - public final class TrustManagers { + private static final char[] BUNDLED_KEYSTORE_PASSWORD = "letsencrypt".toCharArray(); + private TrustManagers() { throw new IllegalStateException("Do not instantiate me"); } @@ -40,16 +38,17 @@ public final class TrustManagers { return createTrustManager(null); } - public static X509TrustManager defaultWithBundledLetsEncrypt(final Context context) + public static X509TrustManager createDefaultWithBundledLetsEncrypt(final Context context) throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { - final BundledTrustManager bundleTrustManager = - BundledTrustManager.builder() - .loadKeyStore( - context.getResources().openRawResource(R.raw.letsencrypt), - "letsencrypt") - .build(); + final var bundleTrustManager = + createWithKeyStore(context.getResources().openRawResource(R.raw.letsencrypt)); return CombiningTrustManager.combineWithDefault(bundleTrustManager); } - + private static X509TrustManager createWithKeyStore(final InputStream inputStream) + throws CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException { + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(inputStream, BUNDLED_KEYSTORE_PASSWORD); + return TrustManagers.createTrustManager(keyStore); + } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 13126f7eb401cc2835671890b67a645708d3d3d5..3bc65eb8bf1b31f0c912d37e5d39de1854e33486 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -5,7 +5,6 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; import android.os.Build; import android.util.Log; - import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.TrustManagers; @@ -14,14 +13,6 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.TLSSocketFactory; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.ResponseBody; - -import org.apache.http.conn.ssl.StrictHostnameVerifier; - import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; @@ -37,9 +28,13 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; - import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.apache.http.conn.ssl.StrictHostnameVerifier; public class HttpConnectionManager extends AbstractConnectionManager { @@ -51,18 +46,20 @@ public class HttpConnectionManager extends AbstractConnectionManager { private static final OkHttpClient OK_HTTP_CLIENT; static { - OK_HTTP_CLIENT = new OkHttpClient.Builder() - .addInterceptor(chain -> { - final Request original = chain.request(); - final Request modified = original.newBuilder() - .header("User-Agent", getUserAgent()) - .build(); - return chain.proceed(modified); - }) - .build(); + OK_HTTP_CLIENT = + new OkHttpClient.Builder() + .addInterceptor( + chain -> { + final Request original = chain.request(); + final Request modified = + original.newBuilder() + .header("User-Agent", getUserAgent()) + .build(); + return chain.proceed(modified); + }) + .build(); } - public static String getUserAgent() { return String.format("%s/%s", BuildConfig.APP_NAME, BuildConfig.VERSION_NAME); } @@ -74,7 +71,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { public static Proxy getProxy() { final InetAddress localhost; try { - localhost = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); + localhost = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); } catch (final UnknownHostException e) { throw new IllegalStateException(e); } @@ -93,7 +90,10 @@ public class HttpConnectionManager extends AbstractConnectionManager { synchronized (this.downloadConnections) { for (HttpDownloadConnection connection : this.downloadConnections) { if (connection.getMessage() == message) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": download already in progress"); + Log.d( + Config.LOGTAG, + message.getConversation().getAccount().getJid().asBareJid() + + ": download already in progress"); return; } } @@ -107,11 +107,18 @@ public class HttpConnectionManager extends AbstractConnectionManager { synchronized (this.uploadConnections) { for (HttpUploadConnection connection : this.uploadConnections) { if (connection.getMessage() == message) { - Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": upload already in progress"); + Log.d( + Config.LOGTAG, + message.getConversation().getAccount().getJid().asBareJid() + + ": upload already in progress"); return; } } - HttpUploadConnection connection = new HttpUploadConnection(message, Method.determine(message.getConversation().getAccount()), this); + HttpUploadConnection connection = + new HttpUploadConnection( + message, + Method.determine(message.getConversation().getAccount()), + this); connection.init(delay); this.uploadConnections.add(connection); } @@ -133,7 +140,8 @@ public class HttpConnectionManager extends AbstractConnectionManager { return buildHttpClient(url, account, 30, interactive); } - OkHttpClient buildHttpClient(final HttpUrl url, final Account account, int readTimeout, boolean interactive) { + OkHttpClient buildHttpClient( + final HttpUrl url, final Account account, int readTimeout, boolean interactive) { final String slotHostname = url.host(); final boolean onionSlot = slotHostname.endsWith(".onion"); final OkHttpClient.Builder builder = OK_HTTP_CLIENT.newBuilder(); @@ -154,7 +162,8 @@ public class HttpConnectionManager extends AbstractConnectionManager { trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive(); } try { - final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM); + final SSLSocketFactory sf = + new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); builder.sslSocketFactory(sf, trustManager); builder.hostnameVerifier(new StrictHostnameVerifier()); } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { @@ -179,13 +188,12 @@ public class HttpConnectionManager extends AbstractConnectionManager { return body.byteStream(); } - public static OkHttpClient okHttpClient(final Context context) { final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); try { final X509TrustManager trustManager; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context); + trustManager = TrustManagers.createDefaultWithBundledLetsEncrypt(context); } else { trustManager = TrustManagers.createDefaultTrustManager(); } @@ -193,10 +201,10 @@ public class HttpConnectionManager extends AbstractConnectionManager { new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); builder.sslSocketFactory(socketFactory, trustManager); } catch (final IOException - | KeyManagementException - | NoSuchAlgorithmException - | KeyStoreException - | CertificateException e) { + | KeyManagementException + | NoSuchAlgorithmException + | KeyStoreException + | CertificateException e) { Log.d(Config.LOGTAG, "not reconfiguring service to work with bundled LetsEncrypt"); throw new RuntimeException(e); } diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index d05fa4ac3dfe2b98d3d61f99427a12eea60a4f08..0d036ca9badb596da8cb7eaa9e3c4537c89ab6af 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -39,30 +39,20 @@ import android.preference.PreferenceManager; import android.util.Base64; import android.util.Log; import android.util.SparseArray; - import androidx.appcompat.app.AppCompatActivity; - import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; - import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.BundledTrustManager; -import eu.siacs.conversations.crypto.CombiningTrustManager; import eu.siacs.conversations.crypto.TrustManagers; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.entities.MTMDecision; import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.MemorizingActivity; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -86,10 +76,12 @@ import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; - import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; /** * A X509 trust manager implementation which asks the user about invalid certificates and memorizes @@ -115,7 +107,8 @@ public class MemorizingTrustManager { "\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile( - "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + "\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)" + + " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile( "\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); @@ -176,7 +169,8 @@ public class MemorizingTrustManager { this.appTrustManager = getTrustManager(appKeyStore); try { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - this.defaultTrustManager = TrustManagers.defaultWithBundledLetsEncrypt(context); + this.defaultTrustManager = + TrustManagers.createDefaultWithBundledLetsEncrypt(context); } else { this.defaultTrustManager = TrustManagers.createDefaultTrustManager(); } From d03deaa78eb9acf4db079c284bda7c2a55f43991 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 3 Apr 2025 10:04:37 +0200 Subject: [PATCH 149/169] version bump to 2.18.0-beta.3 --- build.gradle | 4 ++-- .../android/de-DE/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/en-US/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/et/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/gl-ES/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/it-IT/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/pl-PL/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/ru-RU/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/sq/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/uk/changelogs/{4213704.txt => 4213804.txt} | 0 .../android/zh-CN/changelogs/{4213704.txt => 4213804.txt} | 0 11 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/de-DE/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/et/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/gl-ES/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/it-IT/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/pl-PL/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/ru-RU/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/sq/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{4213704.txt => 4213804.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4213704.txt => 4213804.txt} (100%) diff --git a/build.gradle b/build.gradle index b9986e588fd557816da36d13e470427873ba41f3..e78f3c4ed1989f025b200486b396114db44809e3 100644 --- a/build.gradle +++ b/build.gradle @@ -117,8 +117,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42137 - versionName "2.18.0-beta.2" + versionCode 42138 + versionName "2.18.0-beta.3" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/de-DE/changelogs/4213704.txt b/fastlane/metadata/android/de-DE/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/de-DE/changelogs/4213704.txt rename to fastlane/metadata/android/de-DE/changelogs/4213804.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4213704.txt b/fastlane/metadata/android/en-US/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4213704.txt rename to fastlane/metadata/android/en-US/changelogs/4213804.txt diff --git a/fastlane/metadata/android/et/changelogs/4213704.txt b/fastlane/metadata/android/et/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/et/changelogs/4213704.txt rename to fastlane/metadata/android/et/changelogs/4213804.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4213704.txt b/fastlane/metadata/android/gl-ES/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/gl-ES/changelogs/4213704.txt rename to fastlane/metadata/android/gl-ES/changelogs/4213804.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4213704.txt b/fastlane/metadata/android/it-IT/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/it-IT/changelogs/4213704.txt rename to fastlane/metadata/android/it-IT/changelogs/4213804.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/4213704.txt b/fastlane/metadata/android/pl-PL/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/pl-PL/changelogs/4213704.txt rename to fastlane/metadata/android/pl-PL/changelogs/4213804.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/4213704.txt b/fastlane/metadata/android/ru-RU/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/ru-RU/changelogs/4213704.txt rename to fastlane/metadata/android/ru-RU/changelogs/4213804.txt diff --git a/fastlane/metadata/android/sq/changelogs/4213704.txt b/fastlane/metadata/android/sq/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/sq/changelogs/4213704.txt rename to fastlane/metadata/android/sq/changelogs/4213804.txt diff --git a/fastlane/metadata/android/uk/changelogs/4213704.txt b/fastlane/metadata/android/uk/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/4213704.txt rename to fastlane/metadata/android/uk/changelogs/4213804.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4213704.txt b/fastlane/metadata/android/zh-CN/changelogs/4213804.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4213704.txt rename to fastlane/metadata/android/zh-CN/changelogs/4213804.txt From 64be4b8385903bfc93d9588817396d759f64e6ef Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 3 Apr 2025 11:45:55 +0200 Subject: [PATCH 150/169] rewrite fedilinks to https when no app handles web+ap --- src/main/java/de/gultsch/common/MiniUri.java | 13 +++++++ .../conversations/ui/text/FixedURLSpan.java | 37 ++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/gultsch/common/MiniUri.java b/src/main/java/de/gultsch/common/MiniUri.java index 6ea24a90dd6d1dd497f03813e64cbc7dda7bf7e1..6c5e123bb649e221de68e2b06e72c7b49d414726 100644 --- a/src/main/java/de/gultsch/common/MiniUri.java +++ b/src/main/java/de/gultsch/common/MiniUri.java @@ -1,5 +1,6 @@ package de.gultsch.common; +import android.net.Uri; import com.google.common.base.MoreObjects; import com.google.common.base.Splitter; import com.google.common.base.Strings; @@ -7,6 +8,7 @@ import com.google.common.collect.ImmutableMap; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; import org.checkerframework.checker.nullness.qual.NonNull; @@ -33,6 +35,7 @@ public class MiniUri { } this.scheme = schemeAndRest.get(0); final var rest = schemeAndRest.get(1); + // TODO add fragment parser final var authorityPathAndQuery = Splitter.on('?').limit(2).splitToList(rest); final var authorityPath = authorityPathAndQuery.get(0); System.out.println("authorityPath " + authorityPath); @@ -106,10 +109,20 @@ public class MiniUri { : '/' + this.path; } + public List getPathSegments() { + return Strings.isNullOrEmpty(this.path) + ? Collections.emptyList() + : Splitter.on('/').splitToList(this.path); + } + public String getRaw() { return this.raw; } + public Uri asUri() { + return Uri.parse(this.raw); + } + public Map getParameter() { return this.parameter; } diff --git a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java index aba355214b764ab28db0d0ea641e68837717aee4..c76494736fccdfd8819b92acd4456d3bfca0dc1b 100644 --- a/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java +++ b/src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java @@ -37,9 +37,13 @@ import android.net.Uri; import android.text.Editable; import android.text.Spanned; import android.text.style.URLSpan; +import android.util.Log; import android.view.SoundEffectConstants; import android.view.View; import android.widget.Toast; +import com.google.common.base.Joiner; +import de.gultsch.common.MiniUri; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.ShowLocationActivity; @@ -67,26 +71,47 @@ public class FixedURLSpan extends URLSpan { @Override public void onClick(final View widget) { - final Uri uri = Uri.parse(getURL()); + final var uri = new MiniUri(getURL()); final Context context = widget.getContext(); final boolean candidateToProcessDirectly = "xmpp".equals(uri.getScheme()) || ("https".equals(uri.getScheme()) - && "conversations.im".equals(uri.getHost()) + && "conversations.im".equals(uri.getAuthority()) && uri.getPathSegments().size() > 1 && Arrays.asList("j", "i").contains(uri.getPathSegments().get(0))); - if (candidateToProcessDirectly && context instanceof ConversationsActivity) { - if (((ConversationsActivity) context).onXmppUriClicked(uri)) { + if (candidateToProcessDirectly + && context instanceof ConversationsActivity conversationsActivity) { + if (conversationsActivity.onXmppUriClicked(uri.asUri())) { + Log.d(Config.LOGTAG, "handled xmpp uri internally"); widget.playSoundEffect(SoundEffectConstants.CLICK); return; } } - final Intent intent = new Intent(Intent.ACTION_VIEW, uri); - if ("geo".equalsIgnoreCase(uri.getScheme())) { + final Intent intent = new Intent(Intent.ACTION_VIEW, uri.asUri()); + if ("web+ap".equals(uri.getScheme())) { + if (intent.resolveActivity(context.getPackageManager()) == null) { + Log.d(Config.LOGTAG, "no app found to handle web+ap"); + final var webApAsHttps = + Uri.parse( + String.format( + "https://%s/%s", + uri.getAuthority(), + Joiner.on('/').join(uri.getPathSegments()))); + final var viewHttpsIntent = new Intent(Intent.ACTION_VIEW, webApAsHttps); + startActivity(widget, viewHttpsIntent); + return; + } + } + if ("geo".equals(uri.getScheme())) { intent.setClass(context, ShowLocationActivity.class); } else { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); } + startActivity(widget, intent); + } + + private void startActivity(final View widget, final Intent intent) { + final var context = widget.getContext(); try { context.startActivity(intent); widget.playSoundEffect(SoundEffectConstants.CLICK); From 1be7d107ab4e6a80d1847fe955f8a585f2b5a27f Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Thu, 3 Apr 2025 17:09:11 +0200 Subject: [PATCH 151/169] add some validation to fedilink (no query section) --- .../eu/siacs/conversations/ui/util/MyLinkify.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java index d9d2055e4c4a44770588983c2a21e36c7a16ba4c..393e2081455eb342b6890cb143f1eb2354045f54 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java @@ -40,6 +40,7 @@ import de.gultsch.common.Patterns; import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.XmppUri; import java.util.List; +import java.util.Objects; public class MyLinkify { @@ -56,7 +57,15 @@ public class MyLinkify { case "http", "https" -> Patterns.URI_HTTP.matcher(match).matches(); case "geo" -> Patterns.URI_GEO.matcher(match).matches(); case "xmpp" -> new XmppUri(Uri.parse(match)).isValidJid(); - case "web+ap" -> Patterns.URI_WEB_AP.matcher(match).matches(); + case "web+ap" -> { + if (Patterns.URI_WEB_AP.matcher(match).matches()) { + final var webAp = new MiniUri(match); + // TODO once we have fragment support check that there aren't any + yield Objects.nonNull(webAp.getAuthority()) && webAp.getParameter().isEmpty(); + } else { + yield false; + } + } default -> true; }; } From 2c3ccd358fdac0850d92615033161bd50808f9e3 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Thu, 3 Apr 2025 08:00:25 +0000 Subject: [PATCH 152/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 79e351ebb5b62a7ab89e9adee3c2e2f4a3608090..26b52ff1e0e88098754037ebd1d63c0b124ca3ba 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1134,4 +1134,12 @@ Quicksy poate restaura doar copiile de rezervă pentru conturile quicksy.im Restaurare chei OMEMO Locație copie de siguranță + adresă + Copiere adresă + Copiere locație + Număr de telefon copiat în memorie + Copiere adresă e-mail + Adresă copiată în memorie + Copiere număr de telefon + Adresă e-mail copiată în memorie From 9682fd42cf0581a07a3e657b0c0ccb4d12a0547d Mon Sep 17 00:00:00 2001 From: Besnik_b Date: Thu, 3 Apr 2025 07:58:29 +0000 Subject: [PATCH 153/169] Translated using Weblate (Albanian) Currently translated at 99.0% (1063 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sq/ --- src/main/res/values-sq-rAL/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-sq-rAL/strings.xml b/src/main/res/values-sq-rAL/strings.xml index f41db4241bd6c91274da8b5cd18fb1ea2eadbb25..aa61160ab1df609586ac5276341f3a509c48e7f0 100644 --- a/src/main/res/values-sq-rAL/strings.xml +++ b/src/main/res/values-sq-rAL/strings.xml @@ -1125,4 +1125,12 @@ Rikthe kyçe OMEMO Quicksy mund të rikthejë vetëm kopjeruajtje për llogari quicksy.im Vendndodhje kopjeruajtjesh + URI + Kopjo numër telefoni + Kopjo vendndodhje gjeografike + Kopjo adresë email + Numri i telefonit u kopjua në të papastër + URI u kopjua në të papastër + Adresa email u kopjua në të papastër + Kopjo URI-n From 16087c033ce2d658d34d4270d799264c550ff8ff Mon Sep 17 00:00:00 2001 From: Outbreak2096 Date: Thu, 3 Apr 2025 07:11:22 +0000 Subject: [PATCH 154/169] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/zh_Hans/ --- src/main/res/values-zh-rCN/strings.xml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index f2d022609f24adde04d817a0e769b59a071362fe..cb8ebaa55e0d1209e4bec1f8d5ee8606236389ef 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -248,8 +248,8 @@ \n警告:将在服务器上完全移除频道。 无法解散群聊 无法解散频道 - 编辑群聊话题 - 话题 + 编辑群聊主题 + 主题 正在加入群聊… 离开 对方已将您添加到联系人列表 @@ -842,11 +842,11 @@ 此频道已存在 您已加入现有的频道 无法保存频道配置 - 允许参与者编辑话题 + 允许参与者编辑主题 允许参与者邀请他人 - 参与者可以编辑话题。 - 所有者可以编辑话题。 - 管理员可以编辑话题。 + 参与者可以编辑主题。 + 所有者可以编辑主题。 + 管理员可以编辑主题。 所有者可以邀请他人。 参与者可以邀请他人。 管理员可以查看用户 XMPP 地址。 @@ -1069,7 +1069,7 @@ 您的头像。点按即可从图库选择新头像。 无法禁用视频。 删除 OpenPGP 密钥 - 编辑名称和话题 + 编辑名称和主题 更改配置 更改通知设置 正在使用听筒进行通话。 @@ -1110,4 +1110,12 @@ 恢复 OMEMO 密钥 Quicksy 只能恢复 quicksy.im 账号的备份 备份位置 + URI 已复制到剪贴板 + 复制电子邮件地址 + 电子邮件地址已复制到剪贴板 + 电话号码已复制到剪贴板 + 复制地理位置 + URI + 复制 URI + 复制电话号码 From 21b8b52136a98eb3971a1ef44c87b6fc00f4fc8d Mon Sep 17 00:00:00 2001 From: nautilusx Date: Thu, 3 Apr 2025 20:12:07 +0000 Subject: [PATCH 155/169] Translated using Weblate (German) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/de/ --- src/main/res/values-de/strings.xml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 36316f8e32a297913645891163287fe581663e3d..a3e241e9032166a4ae7db260ec642ca46da3e063 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -309,8 +309,8 @@ Erneut senden Datei-URL URL in die Zwischenablage kopiert - XMPP-Adresse in Zwischenablage kopiert - Fehlermeldung in Zwischenablage kopiert + XMPP-Adresse in die Zwischenablage kopiert + Fehlermeldung in die Zwischenablage kopiert Internetadresse QR-Code scannen QR-Code anzeigen @@ -1115,4 +1115,12 @@ OMEMO-Schlüssel wiederherstellen Quicksy kann nur Sicherungen für quicksy.im-Konten wiederherstellen Sicherungsort + URI + Telefonnummer kopieren + Standort kopieren + E-Mail-Adresse kopieren + Telefonnummer in die Zwischenablage kopiert + URI in die Zwischenablage kopiert + URI kopieren + E-Mail-Adresse in die Zwischenablage kopiert From b4069a31d3da805b037595752e1b23891591e508 Mon Sep 17 00:00:00 2001 From: Grzegorz Szymaszek Date: Fri, 4 Apr 2025 06:28:24 +0000 Subject: [PATCH 156/169] Translated using Weblate (Polish) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pl/ --- src/main/res/values-pl/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 25960ea742bf7aaf6b61cad2fd2b050d9a2c7424..8decb19e21294aeea969558a183b41cd01f6f14f 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -1147,4 +1147,12 @@ Przywróć klucze OMEMO Quicksy potrafi przywracać kopie zapasowe jedynie dla kont quicksy.im Lokalizacja kopii zapasowej + URI + Kopiuj numer telefonu + Kopiuj lokalizację geograficzną + Kopiuj adres e‑mail + Skopiowano numer telefonu do schowka + Kopiuj URI + Skopiowano adres e‑mail do schowka + Skopiowano URI do schowka From ad7ca70f6b0c6de18b9343f3a5e5500f86daacce Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 3 Apr 2025 09:00:00 +0000 Subject: [PATCH 157/169] Translated using Weblate (Russian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ru/ --- src/main/res/values-ru/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml index f8560f4042a97d353ccccbded9079a71d9f4970e..8c4efe55f09178db11cd39275267a3e4315b3640 100644 --- a/src/main/res/values-ru/strings.xml +++ b/src/main/res/values-ru/strings.xml @@ -1162,4 +1162,12 @@ Восстановить ключи OMEMO Quicksy может восстанавливать резервные копии только для аккаунтов quicksy.im Расположение резервной копии + URI скопирован в буфер обмена + URI + Копировать номер телефона + Номер телефона скопирован в буфер обмена + Копировать местоположение + Копировать адрес почты + Копировать URI + Адрес электронной почты скопирован в буфер обмена From 69929f4bd7721649d525156a85ff249348de8ce6 Mon Sep 17 00:00:00 2001 From: SomeTr Date: Thu, 3 Apr 2025 08:44:28 +0000 Subject: [PATCH 158/169] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/uk/ --- src/main/res/values-uk/strings.xml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/res/values-uk/strings.xml b/src/main/res/values-uk/strings.xml index d4edb18cd01f8fcf366e80c40ba1eb3b0b56db36..893b5d02892f0b3dd71e36ece663d460d08a792f 100644 --- a/src/main/res/values-uk/strings.xml +++ b/src/main/res/values-uk/strings.xml @@ -305,7 +305,7 @@ URL файлу URL скопійовано Адресу XMPP скопійовано - Текст повідомлення про помилку скопійовано + Повідомлення про помилку скопійовано вебадреса Розпізнати QR-код Показати QR-код @@ -701,7 +701,7 @@ Доступ до місцезнаходження вимкнено Закріпити розташування Відкріпити розташування - Скопіювати місцезнаходження + Копіювати місцезнаходження Поділитися місцезнаходженням Напрямки Поділитися місцезнаходженням @@ -1163,4 +1163,12 @@ Відновити ключі OMEMO Quicksy може відновлювати резервні копії лише для облікових записів quicksy.im Розташування резервних копій + URI + Адресу Email скопійовано + Копіювати URI + Копіювати номер телефону + Копіювати місцезнаходження + Номер телефону скопійовано + Копіювати адресу Email + URI скопійовано From c369beb5d63ce064530e4efafd5d240c606cc813 Mon Sep 17 00:00:00 2001 From: "lucasmz.dev" Date: Thu, 3 Apr 2025 14:48:39 +0000 Subject: [PATCH 159/169] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/pt_BR/ --- src/main/res/values-pt-rBR/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index ac4f1ab512cea0975441556089d48fd00267e73c..484b1d650ac0f04a691d019fb76f9e817bdcea26 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -1133,4 +1133,12 @@ Restaurar as chaves OMEMO O Quicksy só pode restaurar backups de contas quicksy.im Local do backup + URI + Copiar URI + Copiar número de telefone + Copiar localização geográfica + Copiar endereço de e-mail + Endereço de e-mail copiado para a área de transferência + URI copiada para a área de transferência + Número de telefone copiado para a área de transferência From ce9924e2c484255f6636be4d4177705623c25e60 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Thu, 3 Apr 2025 10:18:48 +0000 Subject: [PATCH 160/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 26b52ff1e0e88098754037ebd1d63c0b124ca3ba..35371539a8fe6987ae669855f1b1063e67cc2f64 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -1137,9 +1137,9 @@ adresă Copiere adresă Copiere locație - Număr de telefon copiat în memorie - Copiere adresă e-mail - Adresă copiată în memorie - Copiere număr de telefon - Adresă e-mail copiată în memorie + Telefon copiat + Copiere e-mail + Adresă copiată + Copiere telefon + E-mail copiat From b9d02b63f91551efa19c83bc2a342a1a2a3d32f6 Mon Sep 17 00:00:00 2001 From: sakistzimas Date: Thu, 3 Apr 2025 20:26:15 +0000 Subject: [PATCH 161/169] Translated using Weblate (Greek) Currently translated at 100.0% (9 of 9 strings) Translation: Conversations/Android App (Quicksy) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-quicksy/el/ --- src/quicksy/res/values-el/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/quicksy/res/values-el/strings.xml b/src/quicksy/res/values-el/strings.xml index 46c41a729470d7e96d01bef0484a897a4aa5ae82..df63fb027b27da5fbd2066fe175a7749faf1ed20 100644 --- a/src/quicksy/res/values-el/strings.xml +++ b/src/quicksy/res/values-el/strings.xml @@ -1,9 +1,9 @@ - Ο χρόνος σίγασης ειδοποιήσεων του Quicksy αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας. + Ο χρόνος σίγασης ειδοποιήσεων του Quicksy αφότου ανιχνευθεί δραστηριότητα σε μια από τις άλλες συσκευές σας Στέλνοντας ίχνη στοίβας προωθείτε την συνεχόμενη ανάπτυξη του Quicksy Επιτρέψτε στις επαφές σας να γνωρίζουν πότε χρησιμοποιείτε το Quicksy - Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι σβηστή, χρειάζεται να προσθέσετε το Quicksy στον κατάλογο με τις προστατευμένες εφαρμογές. + Για να συνεχίσετε να λαμβάνετε ειδοποιήσεις, ακόμα κι όταν η οθόνη είναι κλειστή, χρειάζεται να προσθέσετε το Quicksy στον κατάλογο με τις προστατευμένες εφαρμογές. Φωτογραφία προφίλ του Quicksy Το Quicksy δεν είναι διαθέσιμο στην χώρα σας. Αδυναμία επαλήθευσης της ταυτότητας του διακομιστή. From cdbde830910f4c1d9982e0fbf5b5559771a55ebc Mon Sep 17 00:00:00 2001 From: Notmey Date: Thu, 3 Apr 2025 12:18:38 +0000 Subject: [PATCH 162/169] Translated using Weblate (French) Currently translated at 1.1% (1 of 85 strings) Translation: Conversations/App Store Metadata (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/app-store-metadata/fr/ --- fastlane/metadata/android/fr-FR/changelogs/351.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 fastlane/metadata/android/fr-FR/changelogs/351.txt diff --git a/fastlane/metadata/android/fr-FR/changelogs/351.txt b/fastlane/metadata/android/fr-FR/changelogs/351.txt new file mode 100644 index 0000000000000000000000000000000000000000..6b2e3d8ea188936383f77d9f8c72881e76ea0fe9 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/351.txt @@ -0,0 +1,3 @@ +* correctifs pour les transfers de fichiers Jingle IBB +* correctifs pour les corrections répétées qui remplissaient la base de donnée +* passage à Last Message Correction v1.1 From fddcc0cfd02bac7188fadddedac4b3f0cc2bfaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 4 Apr 2025 09:38:49 +0000 Subject: [PATCH 163/169] Translated using Weblate (Estonian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/et/ --- src/main/res/values-et/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-et/strings.xml b/src/main/res/values-et/strings.xml index 9797d250f9ebb21589c7d994a696827d0778fa10..a3e1991a94eff04be51677690a1e2dbb199b5873 100644 --- a/src/main/res/values-et/strings.xml +++ b/src/main/res/values-et/strings.xml @@ -1135,4 +1135,12 @@ Taasta OMEMO võtmed Quicksy saab taastada vaid quicksy.im teenuses asuvate kasutajakontode varukoopiaid Varukoopia asukoht + URI + Kopeeri URI + Kopeeri geograafilised koordinaadid + Kopeeri e-posti aadress + Kopeerisime e-posti aadressi lõikelauale + Kopeerisime URI lõikelauale + Kopeeri telefoninumber + Kopeerisime telefoninumbri lõikelauale From 26d78df2855d663d9d41704f362c1b8556a7edd3 Mon Sep 17 00:00:00 2001 From: user11 Date: Fri, 4 Apr 2025 20:43:18 +0000 Subject: [PATCH 164/169] Translated using Weblate (Serbian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/sr/ --- src/main/res/values-sr/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index 97a6f556749b896171f9d5226d78a3b46924ce8c..d579bbff5063baf9079340f0471636f0a3800c59 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -314,7 +314,7 @@ Копирај оригинални линк Пошаљи поново Линк ка фајлу - Линк је копиран у клипборд + Линк копиран у клипборд XMPP адреса копирана у клипборд Текст грешке копиран у клипборд веб адреса @@ -1149,4 +1149,12 @@ Quicksy може да врати резервне копије само за quicksy.im налоге Локација резервних копија Врати OMEMO кључеве + URI копиран у клипборд + URI + Копирај геолокацију + Копирај имејл адресу + Копирана имејл адреса у клипборд + Копирај број телефона + Копиран број телефона у клипборд + Копирај URI From ad2e484f5dd7a543a83b44770d3fbbbf7f66f3e0 Mon Sep 17 00:00:00 2001 From: ghose Date: Sat, 5 Apr 2025 04:39:08 +0000 Subject: [PATCH 165/169] Translated using Weblate (Galician) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/gl/ --- src/main/res/values-gl/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index dda046bf0177c8e3f95d847662fe4e9e4c13f26d..bdc0605895bb5025c61e2557f56a79d987cd4648 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -1115,4 +1115,12 @@ Restaurar claves OMEMO Quicksy só pode restaurar copias de apoio de contas quicksy.im Localización das copias + URI copiado ao portapapeis + URI + Copiar URI + Copiar xeolocalización + Copiar enderezo de correo + Copiouse o enderezo ao portapapeis + Copiouse o número ao portapapeis + Copiar número de teléfono From 21432be9d36604f763173385fe4bdf107bcfe155 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Apr 2025 16:27:21 +0200 Subject: [PATCH 166/169] move common code to de.gultsch.common (preperation for code share with Ltt.rs) --- .../gultsch/common}/CombiningTrustManager.java | 12 ++++-------- .../gultsch/common/Linkify.java} | 13 ++++--------- .../gultsch/common}/TrustManagers.java | 14 ++++++++++++-- .../conversations/http/HttpConnectionManager.java | 9 ++------- .../services/MemorizingTrustManager.java | 10 ++-------- .../ui/ConferenceDetailsActivity.java | 6 ++++-- .../conversations/ui/ConversationFragment.java | 4 ++-- .../conversations/ui/adapter/MessageAdapter.java | 6 ++++-- .../eu/siacs/conversations/ui/util/ShareUtil.java | 3 ++- 9 files changed, 36 insertions(+), 41 deletions(-) rename src/main/java/{eu/siacs/conversations/crypto => de/gultsch/common}/CombiningTrustManager.java (88%) rename src/main/java/{eu/siacs/conversations/ui/util/MyLinkify.java => de/gultsch/common/Linkify.java} (89%) rename src/main/java/{eu/siacs/conversations/crypto => de/gultsch/common}/TrustManagers.java (79%) diff --git a/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java b/src/main/java/de/gultsch/common/CombiningTrustManager.java similarity index 88% rename from src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java rename to src/main/java/de/gultsch/common/CombiningTrustManager.java index 7126004877cf49388dcf003c29cc29eea7f8b0ed..41535886f78bafc990b56416d6df488320863aca 100644 --- a/src/main/java/eu/siacs/conversations/crypto/CombiningTrustManager.java +++ b/src/main/java/de/gultsch/common/CombiningTrustManager.java @@ -1,9 +1,8 @@ -package eu.siacs.conversations.crypto; +package de.gultsch.common; import android.annotation.SuppressLint; import android.util.Log; import com.google.common.collect.ImmutableList; -import eu.siacs.conversations.Config; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; @@ -45,11 +44,8 @@ public final class CombiningTrustManager implements X509TrustManager { public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { Log.d( - Config.LOGTAG, - CombiningTrustManager.class.getSimpleName() - + " is configured with " - + this.trustManagers.size() - + " TrustManagers"); + CombiningTrustManager.class.getSimpleName(), + "configured with " + this.trustManagers.size() + " TrustManagers"); for (final Iterator iterator = this.trustManagers.iterator(); iterator.hasNext(); ) { final X509TrustManager trustManager = iterator.next(); @@ -77,7 +73,7 @@ public final class CombiningTrustManager implements X509TrustManager { return certificates.build().toArray(new X509Certificate[0]); } - public static X509TrustManager combineWithDefault(final X509TrustManager... trustManagers) + static X509TrustManager combineWithDefault(final X509TrustManager... trustManagers) throws NoSuchAlgorithmException, KeyStoreException { final ImmutableList.Builder builder = ImmutableList.builder(); builder.addAll(Arrays.asList(trustManagers)); diff --git a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java b/src/main/java/de/gultsch/common/Linkify.java similarity index 89% rename from src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java rename to src/main/java/de/gultsch/common/Linkify.java index 393e2081455eb342b6890cb143f1eb2354045f54..12ad2c67b3ec36964b6455b7a704a75d46646a95 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/MyLinkify.java +++ b/src/main/java/de/gultsch/common/Linkify.java @@ -27,24 +27,20 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package eu.siacs.conversations.ui.util; +package de.gultsch.common; import android.net.Uri; import android.text.Editable; -import android.text.util.Linkify; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; -import de.gultsch.common.MiniUri; -import de.gultsch.common.Patterns; -import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.utils.XmppUri; import java.util.List; import java.util.Objects; -public class MyLinkify { +public class Linkify { - private static final Linkify.MatchFilter MATCH_FILTER = + private static final android.text.util.Linkify.MatchFilter MATCH_FILTER = (s, start, end) -> isPassAdditionalValidation(s.subSequence(start, end).toString()); private static boolean isPassAdditionalValidation(final String match) { @@ -71,8 +67,7 @@ public class MyLinkify { } public static void addLinks(final Editable body) { - Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null); - FixedURLSpan.fix(body); + android.text.util.Linkify.addLinks(body, Patterns.URI_GENERIC, null, MATCH_FILTER, null); } public static List getLinks(final String body) { diff --git a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java b/src/main/java/de/gultsch/common/TrustManagers.java similarity index 79% rename from src/main/java/eu/siacs/conversations/crypto/TrustManagers.java rename to src/main/java/de/gultsch/common/TrustManagers.java index f70545fc950065c81a068d210b76e4a8eb5048fb..fe5398faf3ae0be008c735f47889d25c87052f5e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/TrustManagers.java +++ b/src/main/java/de/gultsch/common/TrustManagers.java @@ -1,6 +1,7 @@ -package eu.siacs.conversations.crypto; +package de.gultsch.common; import android.content.Context; +import android.os.Build; import androidx.annotation.Nullable; import com.google.common.collect.Iterables; import eu.siacs.conversations.R; @@ -33,12 +34,21 @@ public final class TrustManagers { X509TrustManager.class)); } + public static X509TrustManager createForAndroidVersion(final Context context) + throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { + return TrustManagers.createDefaultWithBundledLetsEncrypt(context); + } else { + return TrustManagers.createDefaultTrustManager(); + } + } + public static X509TrustManager createDefaultTrustManager() throws NoSuchAlgorithmException, KeyStoreException { return createTrustManager(null); } - public static X509TrustManager createDefaultWithBundledLetsEncrypt(final Context context) + private static X509TrustManager createDefaultWithBundledLetsEncrypt(final Context context) throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException { final var bundleTrustManager = createWithKeyStore(context.getResources().openRawResource(R.raw.letsencrypt)); diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 3bc65eb8bf1b31f0c912d37e5d39de1854e33486..b1138ba1a1323787cf91ff6d92d48bdf2a0923b0 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -5,9 +5,9 @@ import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; import android.os.Build; import android.util.Log; +import de.gultsch.common.TrustManagers; import eu.siacs.conversations.BuildConfig; import eu.siacs.conversations.Config; -import eu.siacs.conversations.crypto.TrustManagers; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AbstractConnectionManager; @@ -191,12 +191,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { public static OkHttpClient okHttpClient(final Context context) { final OkHttpClient.Builder builder = HttpConnectionManager.OK_HTTP_CLIENT.newBuilder(); try { - final X509TrustManager trustManager; - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - trustManager = TrustManagers.createDefaultWithBundledLetsEncrypt(context); - } else { - trustManager = TrustManagers.createDefaultTrustManager(); - } + final X509TrustManager trustManager = TrustManagers.createForAndroidVersion(context); final SSLSocketFactory socketFactory = new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); builder.sslSocketFactory(socketFactory, trustManager); diff --git a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java index 0d036ca9badb596da8cb7eaa9e3c4537c89ab6af..de519747dabe2ee2321c969e5c92b2e405fd874b 100644 --- a/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java +++ b/src/main/java/eu/siacs/conversations/services/MemorizingTrustManager.java @@ -33,7 +33,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; -import android.os.Build; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Base64; @@ -45,9 +44,9 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import com.google.common.io.CharStreams; +import de.gultsch.common.TrustManagers; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.TrustManagers; import eu.siacs.conversations.crypto.XmppDomainVerifier; import eu.siacs.conversations.entities.MTMDecision; import eu.siacs.conversations.http.HttpConnectionManager; @@ -168,12 +167,7 @@ public class MemorizingTrustManager { init(context); this.appTrustManager = getTrustManager(appKeyStore); try { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - this.defaultTrustManager = - TrustManagers.createDefaultWithBundledLetsEncrypt(context); - } else { - this.defaultTrustManager = TrustManagers.createDefaultTrustManager(); - } + this.defaultTrustManager = TrustManagers.createForAndroidVersion(context); } catch (final NoSuchAlgorithmException | KeyStoreException | CertificateException diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 4d6268c7070cdf5981a084f3c6122a17eae9ce09..baa0914eca9d9cc026d574a1877aafa20155a93b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -22,6 +22,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.databinding.DataBindingUtil; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.gultsch.common.Linkify; import eu.siacs.conversations.R; import eu.siacs.conversations.databinding.ActivityMucDetailsBinding; import eu.siacs.conversations.entities.Conversation; @@ -34,13 +35,13 @@ import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; import eu.siacs.conversations.ui.adapter.MediaAdapter; import eu.siacs.conversations.ui.adapter.UserPreviewAdapter; import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; +import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.ui.util.AvatarWorkerTask; import eu.siacs.conversations.ui.util.GridManager; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.MucConfiguration; import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; -import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.util.SoftKeyboardUtils; import eu.siacs.conversations.utils.AccountUtils; import eu.siacs.conversations.utils.Compatibility; @@ -558,7 +559,8 @@ public class ConferenceDetailsActivity extends XmppActivity if (printableValue(subject)) { SpannableStringBuilder spannable = new SpannableStringBuilder(subject); StylingHelper.format(spannable, this.binding.mucSubject.getCurrentTextColor()); - MyLinkify.addLinks(spannable); + Linkify.addLinks(spannable); + FixedURLSpan.fix(spannable); this.binding.mucSubject.setText(spannable); this.binding.mucSubject.setTextAppearance( subject.length() > (hasTitle ? 128 : 196) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 85c96e0aad375a3ae2f8f135425f24dfb879f052..d58c7a0fc524730e2297edd7865e431a099f4f5f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -65,6 +65,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import de.gultsch.common.Linkify; import de.gultsch.common.Patterns; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -100,7 +101,6 @@ import eu.siacs.conversations.ui.util.EditMessageActionModeCallback; import eu.siacs.conversations.ui.util.ListViewUtils; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; -import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.util.PendingItem; import eu.siacs.conversations.ui.util.PresenceSelector; import eu.siacs.conversations.ui.util.ScrollState; @@ -1348,7 +1348,7 @@ public class ConversationFragment extends XmppFragment && t == null) { copyMessage.setVisible(true); quoteMessage.setVisible(!showError && !MessageUtils.prepareQuote(m).isEmpty()); - final var firstUri = Iterables.getFirst(MyLinkify.getLinks(m.getBody()), null); + final var firstUri = Iterables.getFirst(Linkify.getLinks(m.getBody()), null); if (firstUri != null) { final var scheme = firstUri.getScheme(); final @StringRes int resForScheme = diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 00bf4b17ef5e6760ddd66a294c272a688fe1ea67..bf687333a5c5e8c91f8c776e4d5ab68c6bd30f92 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -43,6 +43,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; +import de.gultsch.common.Linkify; import eu.siacs.conversations.AppSettings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -70,10 +71,10 @@ import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.service.AudioPlayer; import eu.siacs.conversations.ui.text.DividerSpan; +import eu.siacs.conversations.ui.text.FixedURLSpan; import eu.siacs.conversations.ui.text.QuoteSpan; import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.ui.util.AvatarWorkerTask; -import eu.siacs.conversations.ui.util.MyLinkify; import eu.siacs.conversations.ui.util.QuoteHelper; import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; @@ -576,7 +577,8 @@ public class MessageAdapter extends ArrayAdapter { } StylingHelper.format(body, viewHolder.messageBody().getCurrentTextColor()); - MyLinkify.addLinks(body); + Linkify.addLinks(body); + FixedURLSpan.fix(body); if (highlightedTerm != null) { StylingHelper.highlight(viewHolder.messageBody(), body, highlightedTerm); } diff --git a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java index 0a4707074ac88e616489d6c97719a6ab4f099b54..cebd6524bf3423382bbf9082af27f668cc7c119f 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java +++ b/src/main/java/eu/siacs/conversations/ui/util/ShareUtil.java @@ -35,6 +35,7 @@ import android.os.Build; import android.widget.Toast; import androidx.annotation.StringRes; import com.google.common.collect.Iterables; +import de.gultsch.common.Linkify; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; @@ -126,7 +127,7 @@ public class ShareUtil { } public static void copyLinkToClipboard(final XmppActivity activity, final Message message) { - final var firstUri = Iterables.getFirst(MyLinkify.getLinks(message.getBody()), null); + final var firstUri = Iterables.getFirst(Linkify.getLinks(message.getBody()), null); if (firstUri == null) { return; } From 3cffced325ba775106ddacafe6de29f05f094cc4 Mon Sep 17 00:00:00 2001 From: licaon-kter Date: Sat, 5 Apr 2025 08:26:13 +0000 Subject: [PATCH 167/169] Translated using Weblate (Romanian) Currently translated at 100.0% (1073 of 1073 strings) Translation: Conversations/Android App (shared) Translate-URL: https://translate.codeberg.org/projects/conversations/android-app-shared/ro/ --- src/main/res/values-ro-rRO/strings.xml | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 35371539a8fe6987ae669855f1b1063e67cc2f64..4a3f55f8fe88ee976eca53474f4d3ac09e037ec2 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -173,7 +173,7 @@ OMEMO Șterge cont Dezactivare temporară - Publică avatar + Publică poză profil Publică cheia publică OpenPGP Șterge cheia publică OpenPGP Sigur doriți să vă ștergeți cheia publică OpenPGP din mesajele de prezență?\nContactele dumneavoastră nu vor mai putea să vă trimită mesaje criptate cu OpenPGP. @@ -196,7 +196,7 @@ XEP-0237: Creare de versiuni listă XEP-0198: Management flux XEP-0215: Descoperirea serviciilor externe - XEP-0163: PEP (Avatare / OMEMO) + XEP-0163: PEP (Poză profil / OMEMO) XEP-0363: Încărcare fișiere prin HTTP XEP-0357: Push disponibil @@ -252,13 +252,13 @@ %1$s și încă %2$d au citit până aici Toate persoanele au citit până aici Publică - Atingeți avatarul pentru a selecta o poză din galerie + Atingeți poza de profil pentru a selecta o poză din galerie Se publică… Acest server v-a refuzat publicarea Nu s-a putut face convertirea pozei - Nu s-a putut salva avatarul pe disc + Nu s-a putut salva poza de profil pe disc (Sau apasă îndelung pentru a reseta la implicit) - Serverul dumneavoastră nu permite publicarea de avatare + Serverul dumneavoastră nu permite publicarea de poze de profil șoptește către %s Trimite mesaj privat catre %s @@ -352,7 +352,7 @@ Activează notificările Nu s-a găsit serverul pentru discuția de grup Nu s-a putut crea discuția de grup - Avatar cont + Poză de profil cont Copiază amprenta OMEMO în memorie Generează din nou cheia OMEMO Curață lista dispozitivelor @@ -417,7 +417,7 @@ document PDF Aplicație Android Contact - Avatarul a fost publicat! + Poza de profil a fost publicată! Trimit %s Ofer %s Ascunde deconectat @@ -593,7 +593,7 @@ Șterge identitățile OEMO Regenerează cheile personale OMEMO. Toate contactele vor fi obligate să verifice cheile dumneavoastră din nou. Folosiți asta doar ca o ultimă opțiune. Șterge cheile selectate - Pentru a putea publica avatarul trebuie să existe o conexiune. + Pentru a putea publica poza de profil trebuie să existe o conexiune. Arată mesaj de eroare Mesaj de eroare Economizorul de date este activat @@ -741,9 +741,9 @@ Partajare fișiere prin HTTP pentru S3 Activează direct căutarea În ecranul \"Discuție nouă\" focalizează câmpul de căutare și arată tastatura - Avatar discuție de grup - Serverul gazdă nu suporta avatare pentru grupuri - Doar proprietarul grupului poate schimba avatarul + Poză de profil discuție de grup + Serverul gazdă nu suporta poze de profil pentru grupuri + Doar proprietarul grupului poate schimba poza de profil Nume contact Numele dumneavoastră Titlu discuție de grup @@ -833,7 +833,7 @@ carte electronică Original (necompresat) Deschide cu… - Poză profil + Poză de profil Alegeți contul Restaurează o copie de siguranță Restaurează @@ -956,9 +956,9 @@ Nu s-a putut corecta mesajul Toate discuțiile Această discuție - Avatarul dumneavoastră - Avatar pentru %s - Criptare OMEMO + Poza dumneavoastră de profil + Poză de profil pentru %s + Criptat cu OMEMO Criptare OpenPGP Fără criptare Ieșire @@ -989,7 +989,7 @@ Nu este posibilă înregistrarea unui cont Nu a fost găsită o adresă XMPP Eroare temporară de autentificare - Șterge avatar + Șterge poza de profil Apelurile sunt dezactivate atunci când utilizați Tor Comută la video Respinge solicitarea de comutare la video @@ -1090,7 +1090,7 @@ Atunci când dispozitivul este blocat permite aplicației să arate notificările apelurilor pe tot ecranul. Permite mesaje private Nu s-a putut dezactiva videoul. - Avatarul dumneavoastră. Atingeți pentru a selecta un nou avatar din galerie. + Poza dumneavoastră de profil. Atingeți pentru a selecta una nouă din galerie. Schimbă setările notificărilor Schimbă configurația Editare nume și subiect @@ -1114,18 +1114,18 @@ Mai multe reacții Nu s-a putut modifica apelul Clientul XMPP al contactului dvs. este posibil să nu accepte apeluri audio/video. - Culoare de fundal, mărime font, avatare - Afișați avatare pentru mesajele dvs. și în discuțiile 1:1, în plus față de cele de grup. + Culoare de fundal, mărime font, poze profil + Afișați poze de profil pentru mesajele dvs. și în discuțiile 1:1, în plus față de cele de grup. Bule de mesaj Bule de mesaj - Arată avatare + Arată poze de profil Activați setările de notificare personalizate (importanță, sunet, vibrații) pentru această conversație? Notificări personalizate Afișați toate mesajele, inclusiv cele trimise, în partea stângă pentru un aspect uniform al discuției. Mesaje aliniate la stânga Apelurile din această aplicație interacționează cu apelurile telefonice obișnuite, cum ar fi terminarea unui apel atunci când începe altul. Integrarea apelurilor - Ați dori să vă ștergeți avatarul? Unii clienți ar putea să continue să arate o copie a avatarului. + Ați dori să vă ștergeți poza de profil? Unii clienți ar putea să continue să arate o copie a ei. Arată doar contactelor Timp limită de conectare expirat Reîncearcă cu P2P From 8ac6d7935101e0ecef56e60252261eea050d4249 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Mon, 7 Apr 2025 17:29:06 +0200 Subject: [PATCH 168/169] version bump to 2.18.0 --- build.gradle | 4 ++-- .../android/de-DE/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/en-US/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/et/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/gl-ES/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/it-IT/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/pl-PL/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/ru-RU/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/sq/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/uk/changelogs/{4213804.txt => 4213904.txt} | 0 .../android/zh-CN/changelogs/{4213804.txt => 4213904.txt} | 0 11 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/de-DE/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/en-US/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/et/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/gl-ES/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/it-IT/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/pl-PL/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/ru-RU/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/sq/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{4213804.txt => 4213904.txt} (100%) rename fastlane/metadata/android/zh-CN/changelogs/{4213804.txt => 4213904.txt} (100%) diff --git a/build.gradle b/build.gradle index e78f3c4ed1989f025b200486b396114db44809e3..5433c94cf0e2ab35c10b32bf2996132d4f711bd9 100644 --- a/build.gradle +++ b/build.gradle @@ -117,8 +117,8 @@ android { defaultConfig { minSdkVersion 23 targetSdkVersion 34 - versionCode 42138 - versionName "2.18.0-beta.3" + versionCode 42139 + versionName "2.18.0" archivesBaseName += "-$versionName" applicationId "eu.siacs.conversations" resValue "string", "applicationId", applicationId diff --git a/fastlane/metadata/android/de-DE/changelogs/4213804.txt b/fastlane/metadata/android/de-DE/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/de-DE/changelogs/4213804.txt rename to fastlane/metadata/android/de-DE/changelogs/4213904.txt diff --git a/fastlane/metadata/android/en-US/changelogs/4213804.txt b/fastlane/metadata/android/en-US/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/4213804.txt rename to fastlane/metadata/android/en-US/changelogs/4213904.txt diff --git a/fastlane/metadata/android/et/changelogs/4213804.txt b/fastlane/metadata/android/et/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/et/changelogs/4213804.txt rename to fastlane/metadata/android/et/changelogs/4213904.txt diff --git a/fastlane/metadata/android/gl-ES/changelogs/4213804.txt b/fastlane/metadata/android/gl-ES/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/gl-ES/changelogs/4213804.txt rename to fastlane/metadata/android/gl-ES/changelogs/4213904.txt diff --git a/fastlane/metadata/android/it-IT/changelogs/4213804.txt b/fastlane/metadata/android/it-IT/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/it-IT/changelogs/4213804.txt rename to fastlane/metadata/android/it-IT/changelogs/4213904.txt diff --git a/fastlane/metadata/android/pl-PL/changelogs/4213804.txt b/fastlane/metadata/android/pl-PL/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/pl-PL/changelogs/4213804.txt rename to fastlane/metadata/android/pl-PL/changelogs/4213904.txt diff --git a/fastlane/metadata/android/ru-RU/changelogs/4213804.txt b/fastlane/metadata/android/ru-RU/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/ru-RU/changelogs/4213804.txt rename to fastlane/metadata/android/ru-RU/changelogs/4213904.txt diff --git a/fastlane/metadata/android/sq/changelogs/4213804.txt b/fastlane/metadata/android/sq/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/sq/changelogs/4213804.txt rename to fastlane/metadata/android/sq/changelogs/4213904.txt diff --git a/fastlane/metadata/android/uk/changelogs/4213804.txt b/fastlane/metadata/android/uk/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/4213804.txt rename to fastlane/metadata/android/uk/changelogs/4213904.txt diff --git a/fastlane/metadata/android/zh-CN/changelogs/4213804.txt b/fastlane/metadata/android/zh-CN/changelogs/4213904.txt similarity index 100% rename from fastlane/metadata/android/zh-CN/changelogs/4213804.txt rename to fastlane/metadata/android/zh-CN/changelogs/4213904.txt From 16cc323466d1844b6cd94b50ea77efba0f7381b7 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Tue, 8 Apr 2025 10:11:18 +0200 Subject: [PATCH 169/169] use common trust manager in quicksy --- .../services/QuickConversationsService.java | 402 ++++++++++-------- 1 file changed, 224 insertions(+), 178 deletions(-) diff --git a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java index d2af65f8346c4d0f70c5f4572d6330f08180a482..54cfc5ee60f7357956aeb584b9cff6cdd8df4272 100644 --- a/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java +++ b/src/quicksy/java/eu/siacs/conversations/services/QuickConversationsService.java @@ -1,21 +1,17 @@ package eu.siacs.conversations.services; - import static eu.siacs.conversations.utils.Random.SECURE_RANDOM; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.util.Log; - import com.google.common.collect.ImmutableMap; - +import de.gultsch.common.TrustManagers; import eu.siacs.conversations.Config; import eu.siacs.conversations.android.PhoneNumberContact; -import eu.siacs.conversations.crypto.TrustManagers; import eu.siacs.conversations.crypto.sasl.Plain; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -30,11 +26,8 @@ import eu.siacs.conversations.utils.TLSSocketFactory; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xmpp.Jid; - import im.conversations.android.xmpp.model.stanza.Iq; - import io.michaelrocks.libphonenumber.android.Phonenumber; - import java.io.BufferedWriter; import java.io.IOException; import java.io.OutputStream; @@ -63,7 +56,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; - import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; @@ -73,7 +65,6 @@ import javax.net.ssl.X509TrustManager; public class QuickConversationsService extends AbstractQuickConversationsService { - public static final int API_ERROR_OTHER = -1; public static final int API_ERROR_UNKNOWN_HOST = -2; public static final int API_ERROR_CONNECT = -3; @@ -87,8 +78,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService private static final String BASE_URL = "https://" + API_DOMAIN; - private final Set mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>()); - private final Set mOnVerification = Collections.newSetFromMap(new WeakHashMap<>()); + private final Set mOnVerificationRequested = + Collections.newSetFromMap(new WeakHashMap<>()); + private final Set mOnVerification = + Collections.newSetFromMap(new WeakHashMap<>()); private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false); private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false); @@ -97,7 +90,8 @@ public class QuickConversationsService extends AbstractQuickConversationsService private Attempt mLastSyncAttempt = Attempt.NULL; - private final SerialSingleThreadExecutor mSerialSingleThreadExecutor = new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName()); + private final SerialSingleThreadExecutor mSerialSingleThreadExecutor = + new SerialSingleThreadExecutor(QuickConversationsService.class.getSimpleName()); QuickConversationsService(XmppConnectionService xmppConnectionService) { super(xmppConnectionService); @@ -105,19 +99,22 @@ public class QuickConversationsService extends AbstractQuickConversationsService private static long retryAfter(HttpURLConnection connection) { try { - return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L); + return SystemClock.elapsedRealtime() + + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L); } catch (Exception e) { return 0; } } - public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) { + public void addOnVerificationRequestedListener( + OnVerificationRequested onVerificationRequested) { synchronized (mOnVerificationRequested) { mOnVerificationRequested.add(onVerificationRequested); } } - public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) { + public void removeOnVerificationRequestedListener( + OnVerificationRequested onVerificationRequested) { synchronized (mOnVerificationRequested) { mOnVerificationRequested.remove(onVerificationRequested); } @@ -139,62 +136,63 @@ public class QuickConversationsService extends AbstractQuickConversationsService final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber); if (mVerificationRequestInProgress.compareAndSet(false, true)) { SmsRetrieverWrapper.start(service); - new Thread(() -> { - try { - final URL url = new URL(BASE_URL + "/authentication/" + e164); - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - setBundledLetsEncrypt(service, connection); - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - setHeader(connection); - final int code = connection.getResponseCode(); - if (code == 200) { - createAccountAndWait(phoneNumber, 0L); - } else if (code == 429) { - createAccountAndWait(phoneNumber, retryAfter(connection)); - } else { - synchronized (mOnVerificationRequested) { - for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) { - onVerificationRequested.onVerificationRequestFailed(code); - } - } - } - } catch (IOException e) { - final int code = getApiErrorCode(e); - synchronized (mOnVerificationRequested) { - for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) { - onVerificationRequested.onVerificationRequestFailed(code); - } - } - } finally { - mVerificationRequestInProgress.set(false); - } - }).start(); + new Thread( + () -> { + try { + final URL url = new URL(BASE_URL + "/authentication/" + e164); + final HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + setBundledLetsEncrypt(service, connection); + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + setHeader(connection); + final int code = connection.getResponseCode(); + if (code == 200) { + createAccountAndWait(phoneNumber, 0L); + } else if (code == 429) { + createAccountAndWait(phoneNumber, retryAfter(connection)); + } else { + synchronized (mOnVerificationRequested) { + for (OnVerificationRequested onVerificationRequested : + mOnVerificationRequested) { + onVerificationRequested.onVerificationRequestFailed( + code); + } + } + } + } catch (IOException e) { + final int code = getApiErrorCode(e); + synchronized (mOnVerificationRequested) { + for (OnVerificationRequested onVerificationRequested : + mOnVerificationRequested) { + onVerificationRequested.onVerificationRequestFailed( + code); + } + } + } finally { + mVerificationRequestInProgress.set(false); + } + }) + .start(); } } private static void setBundledLetsEncrypt( final Context context, final HttpURLConnection connection) { if (connection instanceof HttpsURLConnection httpsURLConnection) { - final X509TrustManager trustManager; - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { - try { - trustManager = TrustManagers.defaultWithBundledLetsEncrypt(context); - } catch (final NoSuchAlgorithmException - | KeyStoreException - | CertificateException - | IOException e) { - Log.e(Config.LOGTAG, "could not configured bundled LetsEncrypt", e); - return; - } - } else { - return; - } final SSLSocketFactory socketFactory; try { socketFactory = - new TLSSocketFactory(new X509TrustManager[] {trustManager}, SECURE_RANDOM); - } catch (final KeyManagementException | NoSuchAlgorithmException e) { + new TLSSocketFactory( + new X509TrustManager[] { + TrustManagers.createForAndroidVersion(context) + }, + SECURE_RANDOM); + } catch (final KeyManagementException + | NoSuchAlgorithmException + | KeyStoreException + | CertificateException + | IOException e) { Log.e(Config.LOGTAG, "could not configured bundled LetsEncrypt", e); return; } @@ -211,7 +209,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) { String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber); - Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber)); + Log.d( + Config.LOGTAG, + "requesting verification for " + + PhoneNumberUtilWrapper.normalize(service, phoneNumber)); Jid jid = Jid.of(local, Config.QUICKSY_DOMAIN, null); Account account = AccountUtils.getFirst(service); if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) { @@ -237,64 +238,74 @@ public class QuickConversationsService extends AbstractQuickConversationsService public void verify(final Account account, String pin) { if (mVerificationInProgress.compareAndSet(false, true)) { - new Thread(() -> { - try { - final URL url = new URL(BASE_URL + "/password"); - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - setBundledLetsEncrypt(service, connection); - connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin)); - setHeader(connection); - final OutputStream os = connection.getOutputStream(); - final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); - writer.write(account.getPassword()); - writer.flush(); - writer.close(); - os.close(); - connection.connect(); - final int code = connection.getResponseCode(); - if (code == 200 || code == 201) { - account.setOption(Account.OPTION_UNVERIFIED, false); - account.setOption(Account.OPTION_DISABLED, false); - awaitingAccountStateChange = new CountDownLatch(1); - service.updateAccount(account); - try { - awaitingAccountStateChange.await(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": timer expired while waiting for account to connect"); - } - synchronized (mOnVerification) { - for (OnVerification onVerification : mOnVerification) { - onVerification.onVerificationSucceeded(); - } - } - } else if (code == 429) { - final long retryAfter = retryAfter(connection); - synchronized (mOnVerification) { - for (OnVerification onVerification : mOnVerification) { - onVerification.onVerificationRetryAt(retryAfter); - } - } - } else { - synchronized (mOnVerification) { - for (OnVerification onVerification : mOnVerification) { - onVerification.onVerificationFailed(code); - } - } - } - } catch (IOException e) { - final int code = getApiErrorCode(e); - synchronized (mOnVerification) { - for (OnVerification onVerification : mOnVerification) { - onVerification.onVerificationFailed(code); - } - } - } finally { - mVerificationInProgress.set(false); - } - }).start(); + new Thread( + () -> { + try { + final URL url = new URL(BASE_URL + "/password"); + final HttpURLConnection connection = + (HttpURLConnection) url.openConnection(); + setBundledLetsEncrypt(service, connection); + connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000); + connection.setRequestMethod("POST"); + connection.setRequestProperty( + "Authorization", + Plain.getMessage(account.getUsername(), pin)); + setHeader(connection); + final OutputStream os = connection.getOutputStream(); + final BufferedWriter writer = + new BufferedWriter(new OutputStreamWriter(os, "UTF-8")); + writer.write(account.getPassword()); + writer.flush(); + writer.close(); + os.close(); + connection.connect(); + final int code = connection.getResponseCode(); + if (code == 200 || code == 201) { + account.setOption(Account.OPTION_UNVERIFIED, false); + account.setOption(Account.OPTION_DISABLED, false); + awaitingAccountStateChange = new CountDownLatch(1); + service.updateAccount(account); + try { + awaitingAccountStateChange.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": timer expired while waiting for" + + " account to connect"); + } + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationSucceeded(); + } + } + } else if (code == 429) { + final long retryAfter = retryAfter(connection); + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationRetryAt(retryAfter); + } + } + } else { + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationFailed(code); + } + } + } + } catch (IOException e) { + final int code = getApiErrorCode(e); + synchronized (mOnVerification) { + for (OnVerification onVerification : mOnVerification) { + onVerification.onVerificationFailed(code); + } + } + } finally { + mVerificationInProgress.set(false); + } + }) + .start(); } } @@ -339,7 +350,6 @@ public class QuickConversationsService extends AbstractQuickConversationsService return mVerificationRequestInProgress.get(); } - @Override public boolean isSynchronizing() { return mRunningSyncJobs.get() > 0; @@ -353,12 +363,13 @@ public class QuickConversationsService extends AbstractQuickConversationsService @Override public void considerSyncBackground(final boolean forced) { mRunningSyncJobs.incrementAndGet(); - mSerialSingleThreadExecutor.execute(() -> { - considerSync(forced); - if (mRunningSyncJobs.decrementAndGet() == 0) { - service.updateRosterUi(); - } - }); + mSerialSingleThreadExecutor.execute( + () -> { + considerSync(forced); + if (mRunningSyncJobs.decrementAndGet() == 0) { + service.updateRosterUi(); + } + }); } @Override @@ -380,16 +391,19 @@ public class QuickConversationsService extends AbstractQuickConversationsService onVerification.startBackgroundVerification(pin); } } - } - private void considerSync(boolean forced) { - final ImmutableMap allContacts = PhoneNumberContact.load(service); + final ImmutableMap allContacts = + PhoneNumberContact.load(service); for (final Account account : service.getAccounts()) { - final Map contacts = filtered(allContacts, account.getJid().getLocal()); + final Map contacts = + filtered(allContacts, account.getJid().getLocal()); if (contacts.size() < allContacts.size()) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": found own phone number in address book. ignoring..."); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": found own phone number in address book. ignoring..."); } refresh(account, contacts.values()); if (!considerSync(account, contacts, forced)) { @@ -408,17 +422,24 @@ public class QuickConversationsService extends AbstractQuickConversationsService } private void refresh(Account account, Collection contacts) { - for (Contact contact : account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) { + for (Contact contact : + account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)) { final Uri uri = contact.getSystemAccount(); if (uri == null) { continue; } final String number = getNumber(contact); - final PhoneNumberContact phoneNumberContact = PhoneNumberContact.findByUriOrNumber(contacts, uri, number); + final PhoneNumberContact phoneNumberContact = + PhoneNumberContact.findByUriOrNumber(contacts, uri, number); final boolean needsCacheClean; if (phoneNumberContact != null) { if (!uri.equals(phoneNumberContact.getLookupUri())) { - Log.d(Config.LOGTAG, "lookupUri has changed from " + uri + " to " + phoneNumberContact.getLookupUri()); + Log.d( + Config.LOGTAG, + "lookupUri has changed from " + + uri + + " to " + + phoneNumberContact.getLookupUri()); } needsCacheClean = contact.setPhoneContact(phoneNumberContact); } else { @@ -439,7 +460,10 @@ public class QuickConversationsService extends AbstractQuickConversationsService return null; } - private boolean considerSync(final Account account, final Map contacts, final boolean forced) { + private boolean considerSync( + final Account account, + final Map contacts, + final boolean forced) { final int hash = contacts.keySet().hashCode(); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": consider sync of " + hash); if (!mLastSyncAttempt.retry(hash) && !forced) { @@ -448,59 +472,79 @@ public class QuickConversationsService extends AbstractQuickConversationsService } mRunningSyncJobs.incrementAndGet(); final Jid syncServer = Jid.of(API_DOMAIN); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending phone list to " + syncServer); + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + ": sending phone list to " + syncServer); final List entries = new ArrayList<>(); for (final PhoneNumberContact c : contacts.values()) { entries.add(new Element("entry").setAttribute("number", c.getPhoneNumber())); } final Iq query = new Iq(Iq.Type.GET); query.setTo(syncServer); - final Element book = new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries); - final String statusQuo = Entry.statusQuo(contacts.values(), account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)); + final Element book = + new Element("phone-book", Namespace.SYNCHRONIZATION).setChildren(entries); + final String statusQuo = + Entry.statusQuo( + contacts.values(), + account.getRoster().getWithSystemAccounts(PhoneNumberContact.class)); book.setAttribute("ver", statusQuo); query.addChild(book); mLastSyncAttempt = Attempt.create(hash); - service.sendIqPacket(account, query, (response) -> { - if (response.getType() == Iq.Type.RESULT) { - final Element phoneBook = response.findChild("phone-book", Namespace.SYNCHRONIZATION); - if (phoneBook != null) { - final List withSystemAccounts = account.getRoster().getWithSystemAccounts(PhoneNumberContact.class); - for (Entry entry : Entry.ofPhoneBook(phoneBook)) { - final PhoneNumberContact phoneContact = contacts.get(entry.getNumber()); - if (phoneContact == null) { - continue; - } - for (final Jid jid : entry.getJids()) { - final Contact contact = account.getRoster().getContact(jid); - final boolean needsCacheClean = contact.setPhoneContact(phoneContact); - if (needsCacheClean) { - service.getAvatarService().clear(contact); + service.sendIqPacket( + account, + query, + (response) -> { + if (response.getType() == Iq.Type.RESULT) { + final Element phoneBook = + response.findChild("phone-book", Namespace.SYNCHRONIZATION); + if (phoneBook != null) { + final List withSystemAccounts = + account.getRoster() + .getWithSystemAccounts(PhoneNumberContact.class); + for (Entry entry : Entry.ofPhoneBook(phoneBook)) { + final PhoneNumberContact phoneContact = + contacts.get(entry.getNumber()); + if (phoneContact == null) { + continue; + } + for (final Jid jid : entry.getJids()) { + final Contact contact = account.getRoster().getContact(jid); + final boolean needsCacheClean = + contact.setPhoneContact(phoneContact); + if (needsCacheClean) { + service.getAvatarService().clear(contact); + } + withSystemAccounts.remove(contact); + } } - withSystemAccounts.remove(contact); - } - } - for (final Contact contact : withSystemAccounts) { - final boolean needsCacheClean = contact.unsetPhoneContact(PhoneNumberContact.class); - if (needsCacheClean) { - service.getAvatarService().clear(contact); + for (final Contact contact : withSystemAccounts) { + final boolean needsCacheClean = + contact.unsetPhoneContact(PhoneNumberContact.class); + if (needsCacheClean) { + service.getAvatarService().clear(contact); + } + } + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": phone number contact list remains unchanged"); } + } else if (response.getType() == Iq.Type.TIMEOUT) { + mLastSyncAttempt = Attempt.NULL; + } else { + Log.d( + Config.LOGTAG, + account.getJid().asBareJid() + + ": failed to sync contact list with api server"); } - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": phone number contact list remains unchanged"); - } - } else if (response.getType() == Iq.Type.TIMEOUT) { - mLastSyncAttempt = Attempt.NULL; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": failed to sync contact list with api server"); - } - mRunningSyncJobs.decrementAndGet(); - service.syncRoster(account); - service.updateRosterUi(); - }); + mRunningSyncJobs.decrementAndGet(); + service.syncRoster(account); + service.updateRosterUi(); + }); return true; } - public interface OnVerificationRequested { void onVerificationRequestFailed(int code); @@ -535,7 +579,9 @@ public class QuickConversationsService extends AbstractQuickConversationsService } public boolean retry(int hash) { - return hash != this.hash || SystemClock.elapsedRealtime() - timestamp >= Config.CONTACT_SYNC_RETRY_INTERVAL; + return hash != this.hash + || SystemClock.elapsedRealtime() - timestamp + >= Config.CONTACT_SYNC_RETRY_INTERVAL; } } -} \ No newline at end of file +}