Merge branch 'master' of https://codeberg.org/iNPUTmice/Conversations

Stephen Paul Weber created

* 'master' of https://codeberg.org/iNPUTmice/Conversations: (108 commits)
  Translated using Weblate (Spanish)
  Translated using Weblate (Ukrainian)
  Translated using Weblate (German)
  Translated using Weblate (German)
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Spanish)
  Translated using Weblate (German)
  Translated using Weblate (Silesian)
  Translated using Weblate (Vietnamese)
  Translated using Weblate (Serbian)
  Translated using Weblate (Portuguese (Brazil))
  Translated using Weblate (Norwegian Bokmål)
  Translated using Weblate (Russian)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Polish)
  Translated using Weblate (Korean)
  Translated using Weblate (Japanese)
  Translated using Weblate (Hebrew)
  Translated using Weblate (Italian)
  Translated using Weblate (Indonesian)
  ...

Change summary

.gitignore                                                                           |   1 
CHANGELOG.md                                                                         |  14 
build.gradle                                                                         |   3 
fastlane/metadata/android/de-DE/changelogs/4208804.txt                               |   3 
fastlane/metadata/android/de-DE/changelogs/4209004.txt                               |   2 
fastlane/metadata/android/de-DE/changelogs/4209204.txt                               |   2 
fastlane/metadata/android/de-DE/changelogs/4209404.txt                               |   1 
fastlane/metadata/android/en-US/changelogs/4208804.txt                               |   0 
fastlane/metadata/android/en-US/changelogs/4209004.txt                               |   2 
fastlane/metadata/android/en-US/changelogs/4209204.txt                               |   2 
fastlane/metadata/android/en-US/changelogs/4209404.txt                               |   1 
fastlane/metadata/android/es-ES/changelogs/4208804.txt                               |   3 
fastlane/metadata/android/es-ES/changelogs/4209004.txt                               |   2 
fastlane/metadata/android/es-ES/changelogs/4209204.txt                               |   2 
fastlane/metadata/android/es-ES/changelogs/4209404.txt                               |   1 
fastlane/metadata/android/gl-ES/changelogs/4208104.txt                               |   4 
fastlane/metadata/android/gl-ES/changelogs/4208804.txt                               |   3 
fastlane/metadata/android/gl-ES/changelogs/4209004.txt                               |   2 
fastlane/metadata/android/gl-ES/changelogs/4209204.txt                               |   2 
fastlane/metadata/android/it-IT/changelogs/4208804.txt                               |   3 
fastlane/metadata/android/it-IT/changelogs/4209004.txt                               |   2 
fastlane/metadata/android/uk/changelogs/4208804.txt                                  |   3 
fastlane/metadata/android/uk/changelogs/4209004.txt                                  |   2 
fastlane/metadata/android/uk/changelogs/4209204.txt                                  |   2 
fastlane/metadata/android/uk/changelogs/4209404.txt                                  |   1 
fastlane/metadata/android/zh-CN/changelogs/4208804.txt                               |   3 
fastlane/metadata/android/zh-CN/changelogs/4209004.txt                               |   2 
fastlane/metadata/android/zh-CN/changelogs/4209204.txt                               |   2 
src/conversations/fastlane/metadata/android/zh-CN/full_description.txt               |   2 
src/conversations/res/values-bn-rIN/strings.xml                                      |  12 
src/free/AndroidManifest.xml                                                         |   3 
src/main/AndroidManifest.xml                                                         |   2 
src/main/java/eu/siacs/conversations/android/JabberIdContact.java                    |  63 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                       |   2 
src/main/java/eu/siacs/conversations/parser/PresenceParser.java                      | 746 
src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java |  43 
src/main/java/eu/siacs/conversations/services/NotificationService.java               | 277 
src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java                 |   7 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java             |  70 
src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java                  |  39 
src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java           |   2 
src/main/java/eu/siacs/conversations/ui/RecordingActivity.java                       |  26 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java                      |   9 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java               | 127 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                            |  18 
src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java                       |   4 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                            |   1 
src/main/java/eu/siacs/conversations/utils/PhoneHelper.java                          |  39 
src/main/java/eu/siacs/conversations/utils/Resolver.java                             |  10 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                        | 427 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java   |  13 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java                |  34 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java           |  38 
src/main/res/menu/fragment_conversations_overview.xml                                |   5 
src/main/res/menu/start_conversation.xml                                             |  22 
src/main/res/values-bg/strings.xml                                                   |   5 
src/main/res/values-cs/strings.xml                                                   |   5 
src/main/res/values-da-rDK/strings.xml                                               |   5 
src/main/res/values-de/strings.xml                                                   |  24 
src/main/res/values-el/strings.xml                                                   |  10 
src/main/res/values-es/strings.xml                                                   |  16 
src/main/res/values-fi/strings.xml                                                   |   5 
src/main/res/values-gl/strings.xml                                                   |  25 
src/main/res/values-hu/strings.xml                                                   |   5 
src/main/res/values-id/strings.xml                                                   |   3 
src/main/res/values-it/strings.xml                                                   |  17 
src/main/res/values-iw/strings.xml                                                   |   2 
src/main/res/values-ja/strings.xml                                                   |  87 
src/main/res/values-ko/strings.xml                                                   |   2 
src/main/res/values-nb-rNO/strings.xml                                               |   2 
src/main/res/values-pl/strings.xml                                                   |   5 
src/main/res/values-pt-rBR/strings.xml                                               |  12 
src/main/res/values-pt/strings.xml                                                   |   2 
src/main/res/values-ro-rRO/strings.xml                                               |  14 
src/main/res/values-ru/strings.xml                                                   |  48 
src/main/res/values-sr/strings.xml                                                   |   7 
src/main/res/values-szl/strings.xml                                                  |   7 
src/main/res/values-uk/strings.xml                                                   |  13 
src/main/res/values-vi/strings.xml                                                   |   5 
src/main/res/values-zh-rCN/strings.xml                                               |   6 
src/main/res/values/strings.xml                                                      |  13 
src/playstore/AndroidManifest.xml                                                    |   4 
src/quicksy/AndroidManifest.xml                                                      |   3 
src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java                    |  44 
src/quicksy/res/values-de/strings.xml                                                |   2 
src/quicksy/res/values-ja/strings.xml                                                |   4 
src/quicksy/res/values-ru/strings.xml                                                |   2 
87 files changed, 1,547 insertions(+), 968 deletions(-)

Detailed changes

.gitignore 🔗

@@ -11,6 +11,7 @@ src/quicksyPlaystore/res/values/push.xml
 build/
 captures/
 signing.properties
+signing.managed.properties
 # Ignore Gradle GUI config
 gradle-app.setting
 

CHANGELOG.md 🔗

@@ -1,5 +1,19 @@
 # Changelog
 
+### Version 2.13.4
+
+* Fix minor regressions introduced with 2.13.1
+
+### Version 2.13.3
+
+* Provide easier access to 'Privacy Policy' on Play Store version (Quicksy and Conversations)
+* Remove address book integration on Play Store version of Conversations
+
+### Version 2.13.2
+
+* minor bug fixes
+* slight modifications in Quicksy onboard flow
+
 ### Version 2.13.1
 
 * Support P2P file transfer via WebRTC data channels

build.gradle 🔗

@@ -175,10 +175,12 @@ android {
             def appName = "Quicksy"
             resValue "string", "app_name", appName
             buildConfigField "String", "APP_NAME", "\"$appName\""
+            buildConfigField "String", "PRIVACY_POLICY", "\"https://quicksy.im/privacy.htm\""
         }
 
         conversations {
             dimension "mode"
+            buildConfigField "String", "PRIVACY_POLICY", "\"https://conversations.im/privacy.html\""
         }
 
         cheogram {
@@ -189,6 +191,7 @@ android {
             def appName = "Cheogram"
             resValue "string", "app_name", appName
             buildConfigField "String", "APP_NAME", "\"$appName\"";
+            buildConfigField "String", "PRIVACY_POLICY", "\"https://cheogram.com/android-privacy.html\""
         }
 
         playstore {

fastlane/metadata/android/gl-ES/changelogs/4208104.txt 🔗

@@ -1,4 +1,4 @@
-* Acceso mais rápido á 'Mostrar código QR'
-* Soporte para a PEP Marcadores Nativos
+* Acceso mais rápido a 'Mostrar código QR'
+* Soporte para PEP Marcadores Nativos
 * Engadido soporte para SDP Offer / Answer Model (usado por pasarelas SIP)
 * Establecida a API de Android 14 como obxectivo

fastlane/metadata/android/gl-ES/changelogs/4208804.txt 🔗

@@ -0,0 +1,3 @@
+* Soporte para a transferencia de ficheiros P2P a través de canles de datos WebRTC
+* Arranxo dos problemas de interoperabilidade con Bind 2.0 en ejabberd
+* Paquete de certificados raiz Let's Encrypt para Android <=7

fastlane/metadata/android/uk/changelogs/4208804.txt 🔗

@@ -0,0 +1,3 @@
+* Підтримка передачі файлів P2P через канали даних WebRTC
+* Виправлено проблеми сумісності з Bind 2.0 на ejabberd
+* Пакет кореневих сертифікатів Let's Encrypt для версій Android до 7-ї включно

fastlane/metadata/android/uk/changelogs/4209204.txt 🔗

@@ -0,0 +1,2 @@
+* Простіший доступ до «Політики конфіденційності» у версії Play Store (Quicksy та Conversations)
+* Видалено інтеграцію адресної книги у версії Conversations для Play Store

src/conversations/fastlane/metadata/android/zh-CN/full_description.txt 🔗

@@ -28,7 +28,7 @@ Conversations 适用于所有 XMPP 服务器。然而,XMPP 是一种可扩展
 
 到目前为止,这些 XEP 是:
 
-* XEP-0065:SOCKS5 字节流(or mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。
+* XEP-0065:SOCKS5 字节流(或 mod_proxy65)。如果双方都在防火墙(NAT)后面,将用于传输文件。
 * XEP-0163:个人事件协议(头像)
 * XEP-0191:屏蔽命令可让您将垃圾消息发送者列入黑名单或屏蔽的联系人中,而不会将其从花名册中删除。
 * XEP-0198:流管理允许 XMPP 在小规模网络中断和底层 TCP 连接发生变化时继续运行。

src/conversations/res/values-bn-rIN/strings.xml 🔗

@@ -1,10 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <string name="pick_a_server">XMPP সার্ভার নির্বাচন করুন</string>
-    <string name="use_conversations.im">conversations.im-ই ব্যবহার করা যাক</string>
-    <string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করা যাক</string>
-    <string name="do_you_have_an_account">আপনার কি একটা XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। এই মুহুর্তে আরেকটা অ্যকাউন্ট তৈরী করা সম্ভব না।‌\nHint: মাঝে মাঝে ইমেল অ্যকাউন্ট খুললেও এরকম অ্যকাউন্ট নিজে থেকেই তৈরী হয়ে যায়।</string>
-    <string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।\nমনে রাখবেন, সুধুমাত্র আপনার সুবিধার্থেই conversations.im -এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
+    <string name="pick_a_server">আপনার XMPP প্রোভাইডার নির্বাচন করুন</string>
+    <string name="use_conversations.im">conversations.im ব্যবহার করুন</string>
+    <string name="create_new_account">নতুন অ্যকাউন্ট তৈরী করুন</string>
+    <string name="do_you_have_an_account">আপনার কি কোনও XMPP অ্যকাউন্ট ইতিমধ্যে করা আছে? সেরকমটা হতেই পারে যদি এর আগে আপনি কোনো অন্য XMPP প্রোগ্রাম বা অ্যাপ ব্যবহার করে থাকেন। যদি না করে থাকেন, তাহলে আপনি এখন একটি XMPP অ্যাকাউন্ট বানাতে পারেন।
+\nসূত্র: মাঝে মধ্যে কিছু ইমেল প্রোভাইডাররা XMPP অ্যাকাউন্ট দেয়।</string>
+    <string name="server_select_text">XMPP কোনো একটি নির্দিষ্ট সংস্থার উপরে নির্ভরশীল নয়। এই অ্যপটি আপনি যেকোনো সংস্থার XMPP সার্ভারের সাথে ব্যবহার করতে পারেন।
+\nআপনার সুবিধার্থে conversations.im-এ আপনার জন্যে একটি অ্যকাউন্ট তৈরী করে দেওয়া সুবিধাজনক করা হয়েছে। Conversations অ্যপটি এই সার্ভারের সাথে সবথেকে বেশী কার্যকারী।</string>
     <string name="magic_create_text_on_x">আপনাকে %1$s-এ আমন্ত্রিত করা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\n%1$s ব্যবহার করলেও, অন্য সেবা-প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনি কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
     <string name="magic_create_text_fixed">আপনাকে %1$s-এ নিমন্ত্রণ করা হয়েছে। একটি username-ও আপনার জন্যে নির্দিষ্ট করে রাখা হয়েছে। অ্যকাউন্ট তৈরী করার সময় আপনাকে সাহায্য করা হবে।\nঅন্য XMPP সেবা প্রদানকারী সংস্থার ব্যবহারকারীদের সাথে আপনিও কথা বলতে পারবেন, আপনার সম্পূর্ণ XMPP অ্যড্রেস তাদেরকে বলে দিয়ে।</string>
     <string name="your_server_invitation">আপনার নিমন্ত্রণপত্র, সার্ভার থেকে</string>

src/free/AndroidManifest.xml 🔗

@@ -3,5 +3,6 @@
     xmlns:tools="http://schemas.android.com/tools">
 
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
-
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
 </manifest>

src/main/AndroidManifest.xml 🔗

@@ -5,8 +5,6 @@
     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_CONTACTS" />
-    <uses-permission android:name="android.permission.READ_PROFILE" />
     <uses-permission
         android:name="android.permission.READ_PHONE_STATE"
         android:maxSdkVersion="22" />

src/main/java/eu/siacs/conversations/android/JabberIdContact.java 🔗

@@ -8,28 +8,39 @@ import android.os.Build;
 import android.provider.ContactsContract;
 import android.util.Log;
 
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.xmpp.Jid;
+
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.xmpp.Jid;
-
 public class JabberIdContact extends AbstractPhoneContact {
 
-    private static final String[] PROJECTION = new String[]{ContactsContract.Data._ID,
-            ContactsContract.Data.DISPLAY_NAME,
-            ContactsContract.Data.PHOTO_URI,
-            ContactsContract.Data.LOOKUP_KEY,
-            ContactsContract.CommonDataKinds.Im.DATA
-    };
-    private static final String SELECTION = ContactsContract.Data.MIMETYPE + "=? AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? or (" + ContactsContract.CommonDataKinds.Im.PROTOCOL + "=? and lower(" + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL + ")=?))";
+    private static final String[] PROJECTION =
+            new String[] {
+                ContactsContract.Data._ID,
+                ContactsContract.Data.DISPLAY_NAME,
+                ContactsContract.Data.PHOTO_URI,
+                ContactsContract.Data.LOOKUP_KEY,
+                ContactsContract.CommonDataKinds.Im.DATA
+            };
+    private static final String SELECTION =
+            ContactsContract.Data.MIMETYPE
+                    + "=? AND ("
+                    + ContactsContract.CommonDataKinds.Im.PROTOCOL
+                    + "=? or ("
+                    + ContactsContract.CommonDataKinds.Im.PROTOCOL
+                    + "=? and lower("
+                    + ContactsContract.CommonDataKinds.Im.CUSTOM_PROTOCOL
+                    + ")=?))";
 
     private static final String[] SELECTION_ARGS = {
-            ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE,
-            String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER),
-            String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM),
-            "xmpp"
+        ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE,
+        String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER),
+        String.valueOf(ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM),
+        "xmpp"
     };
 
     private final Jid jid;
@@ -37,8 +48,12 @@ public class JabberIdContact extends AbstractPhoneContact {
     private JabberIdContact(Cursor cursor) throws IllegalArgumentException {
         super(cursor);
         try {
-            this.jid = Jid.of(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
-        } catch (IllegalArgumentException | NullPointerException e) {
+            this.jid =
+                    Jid.of(
+                            cursor.getString(
+                                    cursor.getColumnIndexOrThrow(
+                                            ContactsContract.CommonDataKinds.Im.DATA)));
+        } catch (final IllegalArgumentException | NullPointerException e) {
             throw new IllegalArgumentException(e);
         }
     }
@@ -47,11 +62,21 @@ public class JabberIdContact extends AbstractPhoneContact {
         return jid;
     }
 
-    public static Map<Jid, JabberIdContact> load(Context context) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && context.checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+    public static Map<Jid, JabberIdContact> load(final Context context) {
+        if (!QuickConversationsService.isContactListIntegration(context)
+                || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                        && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                                != PackageManager.PERMISSION_GRANTED)) {
             return Collections.emptyMap();
         }
-        try (final Cursor cursor = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, SELECTION_ARGS, null)) {
+        try (final Cursor cursor =
+                context.getContentResolver()
+                        .query(
+                                ContactsContract.Data.CONTENT_URI,
+                                PROJECTION,
+                                SELECTION,
+                                SELECTION_ARGS,
+                                null)) {
             if (cursor == null) {
                 return Collections.emptyMap();
             }

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -963,7 +963,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
             }
 
             if (isTypeGroupChat) {
-                if (packet.hasChild("subject") && !packet.hasChild("thread")) { // already know it has no body from above
+                if (packet.hasChild("subject") && !packet.hasChild("thread")) { // We already know it has no body per above
                     if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
                         conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
                         final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject");

src/main/java/eu/siacs/conversations/parser/PresenceParser.java 🔗

@@ -2,11 +2,6 @@ package eu.siacs.conversations.parser;
 
 import android.util.Log;
 
-import org.openintents.openpgp.util.OpenPgpUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -28,130 +23,162 @@ import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 import eu.siacs.conversations.xmpp.pep.Avatar;
 import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 
-public class PresenceParser extends AbstractParser implements
-		OnPresencePacketReceived {
+import org.openintents.openpgp.util.OpenPgpUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PresenceParser extends AbstractParser implements OnPresencePacketReceived {
+
+    public PresenceParser(XmppConnectionService service) {
+        super(service);
+    }
 
-	public PresenceParser(XmppConnectionService service) {
-		super(service);
-	}
+    public void parseConferencePresence(PresencePacket packet, Account account) {
+        final Conversation conversation =
+                packet.getFrom() == null
+                        ? null
+                        : mXmppConnectionService.find(account, packet.getFrom().asBareJid());
+        if (conversation != null) {
+            final MucOptions mucOptions = conversation.getMucOptions();
+            boolean before = mucOptions.online();
+            int count = mucOptions.getUserCount();
+            final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
+            processConferencePresence(packet, conversation);
+            final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
+            if (!tileUserAfter.equals(tileUserBefore)) {
+                mXmppConnectionService.getAvatarService().clear(mucOptions);
+            }
+            if (before != mucOptions.online()
+                    || (mucOptions.online() && count != mucOptions.getUserCount())) {
+                mXmppConnectionService.updateConversationUi();
+            } else if (mucOptions.online()) {
+                mXmppConnectionService.updateMucRosterUi();
+            }
+        }
+    }
 
-	public void parseConferencePresence(PresencePacket packet, Account account) {
-		final Conversation conversation = packet.getFrom() == null ? null : mXmppConnectionService.find(account, packet.getFrom().asBareJid());
-		if (conversation != null) {
-			final MucOptions mucOptions = conversation.getMucOptions();
-			boolean before = mucOptions.online();
-			int count = mucOptions.getUserCount();
-			final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
-			processConferencePresence(packet, conversation);
-			final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
-			if (!tileUserAfter.equals(tileUserBefore)) {
-				mXmppConnectionService.getAvatarService().clear(mucOptions);
-			}
-			if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) {
-				mXmppConnectionService.updateConversationUi();
-			} else if (mucOptions.online()) {
-				mXmppConnectionService.updateMucRosterUi();
-			}
-		}
-	}
+    private void processConferencePresence(PresencePacket packet, Conversation conversation) {
 
-	private void processConferencePresence(PresencePacket packet, Conversation conversation) {
-		final Account account = conversation.getAccount();
-		final MucOptions mucOptions = conversation.getMucOptions();
-		final Jid jid = conversation.getAccount().getJid();
-		final Jid from = packet.getFrom();
-		if (!from.isBareJid()) {
-			final String type = packet.getAttribute("type");
-			final Element x = packet.findChild("x", Namespace.MUC_USER);
-			final Element nick = packet.findChild("nick", Namespace.NICK);
-			Element hats = packet.findChild("hats", "urn:xmpp:hats:0");
-			if (hats == null) {
-				hats = packet.findChild("hats", "xmpp:prosody.im/protocol/hats:1");
-			}
-			if (hats == null) hats = new Element("hats", "urn:xmpp:hats:0");
-			final Element occupantId = packet.findChild("occupant-id", "urn:xmpp:occupant-id:0");
-			Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
-			final List<String> codes = getStatusCodes(x);
-			if (type == null) {
-				if (x != null) {
-					Element item = x.findChild("item");
-					if (item != null && !from.isBareJid()) {
-						mucOptions.setError(MucOptions.Error.NONE);
-						MucOptions.User user = parseItem(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats);
-						if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) {
-							if (mucOptions.setOnline()) {
-								mXmppConnectionService.getAvatarService().clear(mucOptions);
-							}
-							if (mucOptions.setSelf(user)) {
-								Log.d(Config.LOGTAG,"role or affiliation changed");
-								mXmppConnectionService.databaseBackend.updateConversation(conversation);
-							}
 
-							mXmppConnectionService.persistSelfNick(user);
-							invokeRenameListener(mucOptions, true);
-						}
-						boolean isNew = mucOptions.updateUser(user);
-						final AxolotlService axolotlService = conversation.getAccount().getAxolotlService();
-						Contact contact = user.getContact();
-						if (isNew
-								&& user.getRealJid() != null
-								&& mucOptions.isPrivateAndNonAnonymous()
-								&& (contact == null || !contact.mutualPresenceSubscription())
-								&& axolotlService.hasEmptyDeviceList(user.getRealJid())) {
-							axolotlService.fetchDeviceIds(user.getRealJid());
-						}
-						if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) {
-							Log.d(Config.LOGTAG,account.getJid().asBareJid()
-									+": room '"
-									+mucOptions.getConversation().getJid().asBareJid()
-									+"' created. pushing default configuration");
-							mXmppConnectionService.pushConferenceConfiguration(mucOptions.getConversation(),
-									IqGenerator.defaultChannelConfiguration(),
-									null);
-						}
-						if (mXmppConnectionService.getPgpEngine() != null) {
-							Element signed = packet.findChild("x", "jabber:x:signed");
-							if (signed != null) {
-								Element status = packet.findChild("status");
-								String msg = status == null ? "" : status.getContent();
-								long keyId = mXmppConnectionService.getPgpEngine().fetchKeyId(mucOptions.getAccount(), msg, signed.getContent());
-								if (keyId != 0) {
-									user.setPgpKeyId(keyId);
-								}
-							}
-						}
-						if (avatar != null) {
-							avatar.owner = from;
-							if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
-								if (user.setAvatar(avatar)) {
-									mXmppConnectionService.getAvatarService().clear(user);
-								}
-								if (user.getRealJid() != null) {
-									final Contact c = conversation.getAccount().getRoster().getContact(user.getRealJid());
-									c.setAvatar(avatar);
-									mXmppConnectionService.syncRoster(conversation.getAccount());
-									mXmppConnectionService.getAvatarService().clear(c);
-									mXmppConnectionService.updateRosterUi();
-								}
-							} else if (mXmppConnectionService.isDataSaverDisabled()) {
-								mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
-							}
-						}
-					}
-				}
-			} else if (type.equals("unavailable")) {
-				final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid());
-				if (x.hasChild("destroy") && fullJidMatches) {
-					Element destroy = x.findChild("destroy");
-					final Jid alternate = destroy == null ? null : InvalidJid.getNullForInvalid(destroy.getAttributeAsJid("jid"));
-					mucOptions.setError(MucOptions.Error.DESTROYED);
-					if (alternate != null) {
-						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc destroyed. alternate location " + alternate);
-					}
-				} else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) {
-					mucOptions.setError(MucOptions.Error.SHUTDOWN);
-				} else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) {
-					if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) {
+        final Account account = conversation.getAccount();
+        final MucOptions mucOptions = conversation.getMucOptions();
+        final Jid jid = conversation.getAccount().getJid();
+        final Jid from = packet.getFrom();
+        if (!from.isBareJid()) {
+            final String type = packet.getAttribute("type");
+            final Element x = packet.findChild("x", Namespace.MUC_USER);
+            final Element nick = packet.findChild("nick", Namespace.NICK);
+            Element hats = packet.findChild("hats", "urn:xmpp:hats:0");
+            if (hats == null) {
+                hats = packet.findChild("hats", "xmpp:prosody.im/protocol/hats:1");
+            }
+            if (hats == null) hats = new Element("hats", "urn:xmpp:hats:0");
+            final Element occupantId = packet.findChild("occupant-id", "urn:xmpp:occupant-id:0");
+            Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
+            final List<String> codes = getStatusCodes(x);
+            if (type == null) {
+                if (x != null) {
+                    Element item = x.findChild("item");
+                    if (item != null && !from.isBareJid()) {
+                        mucOptions.setError(MucOptions.Error.NONE);
+                        MucOptions.User user = parseItem(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats);
+                        if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && jid.equals(InvalidJid.getNullForInvalid(item.getAttributeAsJid("jid"))))) {
+                            if (mucOptions.setOnline()) {
+                                mXmppConnectionService.getAvatarService().clear(mucOptions);
+                            }
+                            if (mucOptions.setSelf(user)) {
+                                Log.d(Config.LOGTAG,"role or affiliation changed");
+                                mXmppConnectionService.databaseBackend.updateConversation(conversation);
+                            }
+                            mXmppConnectionService.persistSelfNick(user);
+                            invokeRenameListener(mucOptions, true);
+                        }
+                        boolean isNew = mucOptions.updateUser(user);
+                        final AxolotlService axolotlService =
+                                conversation.getAccount().getAxolotlService();
+                        Contact contact = user.getContact();
+                        if (isNew
+                                && user.getRealJid() != null
+                                && mucOptions.isPrivateAndNonAnonymous()
+                                && (contact == null || !contact.mutualPresenceSubscription())
+                                && axolotlService.hasEmptyDeviceList(user.getRealJid())) {
+                            axolotlService.fetchDeviceIds(user.getRealJid());
+                        }
+                        if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED)
+                                && mucOptions.autoPushConfiguration()) {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    account.getJid().asBareJid()
+                                            + ": room '"
+                                            + mucOptions.getConversation().getJid().asBareJid()
+                                            + "' created. pushing default configuration");
+                            mXmppConnectionService.pushConferenceConfiguration(
+                                    mucOptions.getConversation(),
+                                    IqGenerator.defaultChannelConfiguration(),
+                                    null);
+                        }
+                        if (mXmppConnectionService.getPgpEngine() != null) {
+                            Element signed = packet.findChild("x", "jabber:x:signed");
+                            if (signed != null) {
+                                Element status = packet.findChild("status");
+                                String msg = status == null ? "" : status.getContent();
+                                long keyId =
+                                        mXmppConnectionService
+                                                .getPgpEngine()
+                                                .fetchKeyId(
+                                                        mucOptions.getAccount(),
+                                                        msg,
+                                                        signed.getContent());
+                                if (keyId != 0) {
+                                    user.setPgpKeyId(keyId);
+                                }
+                            }
+                        }
+                        if (avatar != null) {
+                            avatar.owner = from;
+                            if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
+                                if (user.setAvatar(avatar)) {
+                                    mXmppConnectionService.getAvatarService().clear(user);
+                                }
+                                if (user.getRealJid() != null) {
+                                    final Contact c =
+                                            conversation
+                                                    .getAccount()
+                                                    .getRoster()
+                                                    .getContact(user.getRealJid());
+                                    c.setAvatar(avatar);
+                                    mXmppConnectionService.syncRoster(conversation.getAccount());
+                                    mXmppConnectionService.getAvatarService().clear(c);
+                                    mXmppConnectionService.updateRosterUi();
+                                }
+                            } else if (mXmppConnectionService.isDataSaverDisabled()) {
+                                mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
+                            }
+                        }
+                    }
+                }
+            } else if (type.equals("unavailable")) {
+                final boolean fullJidMatches = from.equals(mucOptions.getSelf().getFullJid());
+                if (x.hasChild("destroy") && fullJidMatches) {
+                    Element destroy = x.findChild("destroy");
+                    final Jid alternate =
+                            destroy == null
+                                    ? null
+                                    : InvalidJid.getNullForInvalid(
+                                            destroy.getAttributeAsJid("jid"));
+                    mucOptions.setError(MucOptions.Error.DESTROYED);
+                    if (alternate != null) {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid()
+                                        + ": muc destroyed. alternate location "
+                                        + alternate);
+                    }
+                } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN) && fullJidMatches) {
+                    mucOptions.setError(MucOptions.Error.SHUTDOWN);
+                } else if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE)) {
+                    if (codes.contains(MucOptions.STATUS_CODE_TECHNICAL_REASONS)) {
                         final boolean wasOnline = mucOptions.online();
                         mucOptions.setError(MucOptions.Error.TECHNICAL_PROBLEMS);
                         Log.d(
@@ -164,238 +191,259 @@ public class PresenceParser extends AbstractParser implements
                         if (wasOnline) {
                             mXmppConnectionService.mucSelfPingAndRejoin(conversation);
                         }
-					} else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
-						mucOptions.setError(MucOptions.Error.KICKED);
-					} else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
-						mucOptions.setError(MucOptions.Error.BANNED);
-					} else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
-						mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
-					} else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
-						mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
-					} else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
-						mucOptions.setError(MucOptions.Error.SHUTDOWN);
-					} else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
-						mucOptions.setError(MucOptions.Error.UNKNOWN);
-						Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
-					}
-				} else if (!from.isBareJid()){
-					Element item = x.findChild("item");
-					if (item != null) {
+                    } else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
+                        mucOptions.setError(MucOptions.Error.KICKED);
+                    } else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
+                        mucOptions.setError(MucOptions.Error.BANNED);
+                    } else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
+                        mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
+                    } else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
+                        mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
+                    } else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
+                        mucOptions.setError(MucOptions.Error.SHUTDOWN);
+                    } else if (!codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
+                        mucOptions.setError(MucOptions.Error.UNKNOWN);
+                        Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
+                    }
+                } else if (!from.isBareJid()) {
+                    Element item = x.findChild("item");
+                    if (item != null) {
 						mucOptions.updateUser(parseItem(conversation, item, from, occupantId, nick == null ? null : nick.getContent(), hats));
-					}
-					MucOptions.User user = mucOptions.deleteUser(from);
-					if (user != null) {
-						mXmppConnectionService.getAvatarService().clear(user);
-					}
-				}
-			} else if (type.equals("error")) {
-				final Element error = packet.findChild("error");
-				if (error == null) {
-					return;
-				}
-				if (error.hasChild("conflict")) {
-					if (mucOptions.online()) {
-						invokeRenameListener(mucOptions, false);
-					} else {
-						mucOptions.setError(MucOptions.Error.NICK_IN_USE);
-					}
-				} else if (error.hasChild("not-authorized")) {
-					mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
-				} else if (error.hasChild("forbidden")) {
-					mucOptions.setError(MucOptions.Error.BANNED);
-				} else if (error.hasChild("registration-required")) {
-					mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
-				} else if (error.hasChild("resource-constraint")) {
-					mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT);
-				} else if (error.hasChild("remote-server-timeout")) {
-					mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT);
-				} else if (error.hasChild("gone")) {
-					final String gone = error.findChildContent("gone");
-					final Jid alternate;
-					if (gone != null) {
-						final XmppUri xmppUri = new XmppUri(gone);
-						if (xmppUri.isValidJid()) {
-							alternate = xmppUri.getJid();
-						} else {
-							alternate = null;
-						}
-					} else {
-						alternate = null;
-					}
-					mucOptions.setError(MucOptions.Error.DESTROYED);
-					if (alternate != null) {
-						Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": muc destroyed. alternate location " + alternate);
-					}
-				} else {
-					final String text = error.findChildContent("text");
-					if (text != null && text.contains("attribute 'to'")) {
-						if (mucOptions.online()) {
-							invokeRenameListener(mucOptions, false);
-						} else {
-							mucOptions.setError(MucOptions.Error.INVALID_NICK);
-						}
-					} else {
-						mucOptions.setError(MucOptions.Error.UNKNOWN);
-						Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
-					}
-				}
-			}
-		}
-	}
+                    }
+                    MucOptions.User user = mucOptions.deleteUser(from);
+                    if (user != null) {
+                        mXmppConnectionService.getAvatarService().clear(user);
+                    }
+                }
+            } else if (type.equals("error")) {
+                final Element error = packet.findChild("error");
+                if (error == null) {
+                    return;
+                }
+                if (error.hasChild("conflict")) {
+                    if (mucOptions.online()) {
+                        invokeRenameListener(mucOptions, false);
+                    } else {
+                        mucOptions.setError(MucOptions.Error.NICK_IN_USE);
+                    }
+                } else if (error.hasChild("not-authorized")) {
+                    mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
+                } else if (error.hasChild("forbidden")) {
+                    mucOptions.setError(MucOptions.Error.BANNED);
+                } else if (error.hasChild("registration-required")) {
+                    mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
+                } else if (error.hasChild("resource-constraint")) {
+                    mucOptions.setError(MucOptions.Error.RESOURCE_CONSTRAINT);
+                } else if (error.hasChild("remote-server-timeout")) {
+                    mucOptions.setError(MucOptions.Error.REMOTE_SERVER_TIMEOUT);
+                } else if (error.hasChild("gone")) {
+                    final String gone = error.findChildContent("gone");
+                    final Jid alternate;
+                    if (gone != null) {
+                        final XmppUri xmppUri = new XmppUri(gone);
+                        if (xmppUri.isValidJid()) {
+                            alternate = xmppUri.getJid();
+                        } else {
+                            alternate = null;
+                        }
+                    } else {
+                        alternate = null;
+                    }
+                    mucOptions.setError(MucOptions.Error.DESTROYED);
+                    if (alternate != null) {
+                        Log.d(
+                                Config.LOGTAG,
+                                conversation.getAccount().getJid().asBareJid()
+                                        + ": muc destroyed. alternate location "
+                                        + alternate);
+                    }
+                } else {
+                    final String text = error.findChildContent("text");
+                    if (text != null && text.contains("attribute 'to'")) {
+                        if (mucOptions.online()) {
+                            invokeRenameListener(mucOptions, false);
+                        } else {
+                            mucOptions.setError(MucOptions.Error.INVALID_NICK);
+                        }
+                    } else {
+                        mucOptions.setError(MucOptions.Error.UNKNOWN);
+                        Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
+                    }
+                }
+            }
+        }
+    }
 
-	private static void invokeRenameListener(final MucOptions options, boolean success) {
-		if (options.onRenameListener != null) {
-			if (success) {
-				options.onRenameListener.onSuccess();
-			} else {
-				options.onRenameListener.onFailure();
-			}
-			options.onRenameListener = null;
-		}
-	}
+    private static void invokeRenameListener(final MucOptions options, boolean success) {
+        if (options.onRenameListener != null) {
+            if (success) {
+                options.onRenameListener.onSuccess();
+            } else {
+                options.onRenameListener.onFailure();
+            }
+            options.onRenameListener = null;
+        }
+    }
 
-	private static List<String> getStatusCodes(Element x) {
-		List<String> codes = new ArrayList<>();
-		if (x != null) {
-			for (Element child : x.getChildren()) {
-				if (child.getName().equals("status")) {
-					String code = child.getAttribute("code");
-					if (code != null) {
-						codes.add(code);
-					}
-				}
-			}
-		}
-		return codes;
-	}
+    private static List<String> getStatusCodes(Element x) {
+        List<String> codes = new ArrayList<>();
+        if (x != null) {
+            for (Element child : x.getChildren()) {
+                if (child.getName().equals("status")) {
+                    String code = child.getAttribute("code");
+                    if (code != null) {
+                        codes.add(code);
+                    }
+                }
+            }
+        }
+        return codes;
+    }
 
-	private void parseContactPresence(final PresencePacket packet, final Account account) {
-		final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
-		final Jid from = packet.getFrom();
-		if (from == null || from.equals(account.getJid())) {
-			return;
-		}
-		final String type = packet.getAttribute("type");
-		final Contact contact = account.getRoster().getContact(from);
-		if (type == null) {
-			final String resource = from.isBareJid() ? "" : from.getResource();
-			Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
-			if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
-				avatar.owner = from.asBareJid();
-				if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
-					if (avatar.owner.equals(account.getJid().asBareJid())) {
-						account.setAvatar(avatar.getFilename());
-						mXmppConnectionService.databaseBackend.updateAccount(account);
-						mXmppConnectionService.getAvatarService().clear(account);
-						mXmppConnectionService.updateConversationUi();
-						mXmppConnectionService.updateAccountUi();
-					} else {
-						contact.setAvatar(avatar);
-						mXmppConnectionService.syncRoster(account);
-						mXmppConnectionService.getAvatarService().clear(contact);
-						mXmppConnectionService.updateConversationUi();
-						mXmppConnectionService.updateRosterUi();
-					}
-				} else if (mXmppConnectionService.isDataSaverDisabled()){
-					mXmppConnectionService.fetchAvatar(account, avatar);
-				}
-			}
+    private void parseContactPresence(final PresencePacket packet, final Account account) {
+        final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
+        final Jid from = packet.getFrom();
+        if (from == null || from.equals(account.getJid())) {
+            return;
+        }
+        final String type = packet.getAttribute("type");
+        final Contact contact = account.getRoster().getContact(from);
+        if (type == null) {
+            final String resource = from.isBareJid() ? "" : from.getResource();
+            final Avatar avatar =
+                    Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
+            if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
+                avatar.owner = from.asBareJid();
+                if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
+                    if (avatar.owner.equals(account.getJid().asBareJid())) {
+                        account.setAvatar(avatar.getFilename());
+                        mXmppConnectionService.databaseBackend.updateAccount(account);
+                        mXmppConnectionService.getAvatarService().clear(account);
+                        mXmppConnectionService.updateConversationUi();
+                        mXmppConnectionService.updateAccountUi();
+                    } else {
+                        contact.setAvatar(avatar);
+                        mXmppConnectionService.syncRoster(account);
+                        mXmppConnectionService.getAvatarService().clear(contact);
+                        mXmppConnectionService.updateConversationUi();
+                        mXmppConnectionService.updateRosterUi();
+                    }
+                } else if (mXmppConnectionService.isDataSaverDisabled()) {
+                    mXmppConnectionService.fetchAvatar(account, avatar);
+                }
+            }
 
-			if (mXmppConnectionService.isMuc(account, from)) {
-				return;
-			}
+            if (mXmppConnectionService.isMuc(account, from)) {
+                return;
+            }
 
-			int sizeBefore = contact.getPresences().size();
+            final int sizeBefore = contact.getPresences().size();
 
-			final String show = packet.findChildContent("show");
-			final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
-			final String message = packet.findChildContent("status");
-			final Presence presence = Presence.parse(show, caps, message);
-			contact.updatePresence(resource, presence);
-			if (presence.hasCaps()) {
-				mXmppConnectionService.fetchCaps(account, from, presence);
-			}
+            final String show = packet.findChildContent("show");
+            final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
+            final String message = packet.findChildContent("status");
+            final Presence presence = Presence.parse(show, caps, message);
+            contact.updatePresence(resource, presence);
+            if (presence.hasCaps()) {
+                mXmppConnectionService.fetchCaps(account, from, presence);
+            }
 
-			final Element idle = packet.findChild("idle", Namespace.IDLE);
-			if (idle != null) {
-				try {
-					final String since = idle.getAttribute("since");
-					contact.setLastseen(AbstractParser.parseTimestamp(since));
-					contact.flagInactive();
-				} catch (Throwable throwable) {
-					if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) {
-						contact.flagActive();
-					}
-				}
-			} else {
-				if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) {
-					contact.flagActive();
-				}
-			}
+            final Element idle = packet.findChild("idle", Namespace.IDLE);
+            if (idle != null) {
+                try {
+                    final String since = idle.getAttribute("since");
+                    contact.setLastseen(AbstractParser.parseTimestamp(since));
+                    contact.flagInactive();
+                } catch (Throwable throwable) {
+                    if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) {
+                        contact.flagActive();
+                    }
+                }
+            } else {
+                if (contact.setLastseen(AbstractParser.parseTimestamp(packet))) {
+                    contact.flagActive();
+                }
+            }
 
-			PgpEngine pgp = mXmppConnectionService.getPgpEngine();
-			Element x = packet.findChild("x", "jabber:x:signed");
-			if (pgp != null && x != null) {
-				final String status = packet.findChildContent("status");
-				final long keyId = pgp.fetchKeyId(account, status, x.getContent());
-				if (keyId != 0 && contact.setPgpKeyId(keyId)) {
-					Log.d(Config.LOGTAG,account.getJid().asBareJid()+": found OpenPGP key id for "+contact.getJid()+" "+OpenPgpUtils.convertKeyIdToHex(keyId));
-					mXmppConnectionService.syncRoster(account);
-				}
-			}
-			boolean online = sizeBefore < contact.getPresences().size();
-			mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online);
-		} else if (type.equals("unavailable")) {
-			if (contact.setLastseen(AbstractParser.parseTimestamp(packet,0L,true))) {
-				contact.flagInactive();
-			}
-			if (from.isBareJid()) {
-				contact.clearPresences();
-			} else {
-				contact.removePresence(from.getResource());
-			}
-			if (contact.getShownStatus() == Presence.Status.OFFLINE) {
-				contact.flagInactive();
-			}
-			mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false);
-		} else if (type.equals("subscribe")) {
-			if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) {
-				mXmppConnectionService.syncRoster(account);
-				mXmppConnectionService.getAvatarService().clear(contact);
-			}
-			if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
-				mXmppConnectionService.sendPresencePacket(account,
-						mPresenceGenerator.sendPresenceUpdatesTo(contact));
-			} else {
-				contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
-				final Conversation conversation = mXmppConnectionService.findOrCreateConversation(
-						account, contact.getJid().asBareJid(), false, false);
-				final String statusMessage = packet.findChildContent("status");
-				if (statusMessage != null
-						&& !statusMessage.isEmpty()
-						&& conversation.countMessages() == 0) {
-					conversation.add(new Message(
-							conversation,
-							statusMessage,
-							Message.ENCRYPTION_NONE,
-							Message.STATUS_RECEIVED
-					));
-				}
-			}
-		}
-		mXmppConnectionService.updateRosterUi();
-	}
+            final PgpEngine pgp = mXmppConnectionService.getPgpEngine();
+            final Element x = packet.findChild("x", "jabber:x:signed");
+            if (pgp != null && x != null) {
+                final String status = packet.findChildContent("status");
+                final long keyId = pgp.fetchKeyId(account, status, x.getContent());
+                if (keyId != 0 && contact.setPgpKeyId(keyId)) {
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": found OpenPGP key id for "
+                                    + contact.getJid()
+                                    + " "
+                                    + OpenPgpUtils.convertKeyIdToHex(keyId));
+                    mXmppConnectionService.syncRoster(account);
+                }
+            }
+            boolean online = sizeBefore < contact.getPresences().size();
+            mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online);
+        } else if (type.equals("unavailable")) {
+            if (contact.setLastseen(AbstractParser.parseTimestamp(packet, 0L, true))) {
+                contact.flagInactive();
+            }
+            if (from.isBareJid()) {
+                contact.clearPresences();
+            } else {
+                contact.removePresence(from.getResource());
+            }
+            if (contact.getShownStatus() == Presence.Status.OFFLINE) {
+                contact.flagInactive();
+            }
+            mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false);
+        } else if (type.equals("subscribe")) {
+            if (contact.isBlocked()) {
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": ignoring 'subscribe' presence from blocked "
+                                + from);
+                return;
+            }
+            if (contact.setPresenceName(packet.findChildContent("nick", Namespace.NICK))) {
+                mXmppConnectionService.syncRoster(account);
+                mXmppConnectionService.getAvatarService().clear(contact);
+            }
+            if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
+                mXmppConnectionService.sendPresencePacket(
+                        account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
+            } else {
+                contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
+                final Conversation conversation =
+                        mXmppConnectionService.findOrCreateConversation(
+                                account, contact.getJid().asBareJid(), false, false);
+                final String statusMessage = packet.findChildContent("status");
+                if (statusMessage != null
+                        && !statusMessage.isEmpty()
+                        && conversation.countMessages() == 0) {
+                    conversation.add(
+                            new Message(
+                                    conversation,
+                                    statusMessage,
+                                    Message.ENCRYPTION_NONE,
+                                    Message.STATUS_RECEIVED));
+                }
+            }
+        }
+        mXmppConnectionService.updateRosterUi();
+    }
 
-	@Override
-	public void onPresencePacketReceived(Account account, PresencePacket packet) {
-		if (packet.hasChild("x", Namespace.MUC_USER)) {
-			this.parseConferencePresence(packet, account);
-		} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
-			this.parseConferencePresence(packet, account);
-		} else if ("error".equals(packet.getAttribute("type")) && mXmppConnectionService.isMuc(account, packet.getFrom())) {
-			this.parseConferencePresence(packet, account);
-		} else {
-			this.parseContactPresence(packet, account);
-		}
-	}
+    @Override
+    public void onPresencePacketReceived(Account account, PresencePacket packet) {
+        if (packet.hasChild("x", Namespace.MUC_USER)) {
+            this.parseConferencePresence(packet, account);
+        } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
+            this.parseConferencePresence(packet, account);
+        } else if ("error".equals(packet.getAttribute("type"))
+                && mXmppConnectionService.isMuc(account, packet.getFrom())) {
+            this.parseConferencePresence(packet, account);
+        } else {
+            this.parseContactPresence(packet, account);
+        }
+    }
 }

src/main/java/eu/siacs/conversations/services/AbstractQuickConversationsService.java 🔗

@@ -1,14 +1,22 @@
 package eu.siacs.conversations.services;
 
+import android.Manifest;
+import android.content.Context;
 import android.content.Intent;
-import android.os.Build;
+import android.content.pm.PackageManager;
+
+import com.google.common.collect.Iterables;
 
 import eu.siacs.conversations.BuildConfig;
 
+import java.util.Arrays;
+
 public abstract class AbstractQuickConversationsService {
 
+    public static final String SMS_RETRIEVED_ACTION =
+            "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
 
-    public static final String SMS_RETRIEVED_ACTION = "com.google.android.gms.auth.api.phone.SMS_RETRIEVED";
+    private static Boolean declaredReadContacts = null;
 
     protected final XmppConnectionService service;
 
@@ -30,6 +38,37 @@ public abstract class AbstractQuickConversationsService {
         return "playstore".equals(BuildConfig.FLAVOR_distribution);
     }
 
+    public static boolean isContactListIntegration(final Context context) {
+        if ("quicksy".equals(BuildConfig.FLAVOR_mode)) {
+            return true;
+        }
+        final var readContacts = AbstractQuickConversationsService.declaredReadContacts;
+        if (readContacts != null) {
+            return Boolean.TRUE.equals(readContacts);
+        }
+        AbstractQuickConversationsService.declaredReadContacts = hasDeclaredReadContacts(context);
+        return AbstractQuickConversationsService.declaredReadContacts;
+    }
+
+    private static boolean hasDeclaredReadContacts(final Context context) {
+        final String[] permissions;
+        try {
+            permissions =
+                    context.getPackageManager()
+                            .getPackageInfo(
+                                    context.getPackageName(), PackageManager.GET_PERMISSIONS)
+                            .requestedPermissions;
+        } catch (final PackageManager.NameNotFoundException e) {
+            return false;
+        }
+        return Iterables.any(
+                Arrays.asList(permissions), p -> p.equals(Manifest.permission.READ_CONTACTS));
+    }
+
+    public static boolean isQuicksyPlayStore() {
+        return isQuicksy() && isPlayStoreFlavor();
+    }
+
     public abstract void signalAccountStateChange();
 
     public abstract boolean isSynchronizing();

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -125,6 +125,7 @@ public class NotificationService {
     private long mLastNotification;
 
     private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
+    private static final String MESSAGES_NOTIFICATION_CHANNEL = "messages";
     private Ringtone currentlyPlayingRingtone = null;
     private ScheduledFuture<?> vibrationFuture;
 
@@ -254,7 +255,7 @@ public class NotificationService {
 
         final NotificationChannel messagesChannel =
                 new NotificationChannel(
-                        "messages",
+                        MESSAGES_NOTIFICATION_CHANNEL,
                         c.getString(R.string.messages_channel_name),
                         NotificationManager.IMPORTANCE_HIGH);
         messagesChannel.setShowBadge(true);
@@ -1208,7 +1209,7 @@ public class NotificationService {
         final Builder mBuilder =
                 new NotificationCompat.Builder(
                         mXmppConnectionService,
-                        quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
+                        quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages"));
         final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
         style.setBigContentTitle(
                 mXmppConnectionService
@@ -1274,157 +1275,157 @@ public class NotificationService {
 
     private Builder buildSingleConversations(
             final ArrayList<Message> messages, final boolean notify, final boolean quietHours) {
-        final Builder mBuilder =
-                new NotificationCompat.Builder(
-                        mXmppConnectionService,
-                        quietHours ? "quiet_hours" : (notify ? "messages" : "silent_messages"));
-        if (messages.size() >= 1) {
-            final Conversation conversation = (Conversation) messages.get(0).getConversation();
-            mBuilder.setLargeIcon(FileBackend.drawDrawable(
+        final var channel = quietHours ? "quiet_hours" : (notify ? MESSAGES_NOTIFICATION_CHANNEL : "silent_messages");
+        final Builder notificationBuilder =
+                new NotificationCompat.Builder(mXmppConnectionService, channel);
+        if (messages.isEmpty()) {
+            return notificationBuilder;
+        }
+        final Conversation conversation = (Conversation) messages.get(0).getConversation();
+        notificationBuilder.setLargeIcon(FileBackend.drawDrawable(
+                mXmppConnectionService
+                        .getAvatarService()
+                        .get(
+                                conversation,
+                                AvatarService.getSystemUiAvatarSize(mXmppConnectionService))));
+        notificationBuilder.setContentTitle(conversation.getName());
+        if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
+            int count = messages.size();
+            notificationBuilder.setContentText(
                     mXmppConnectionService
-                            .getAvatarService()
-                            .get(
-                                    conversation,
-                                    AvatarService.getSystemUiAvatarSize(mXmppConnectionService))));
-            mBuilder.setContentTitle(conversation.getName());
-            if (Config.HIDE_MESSAGE_TEXT_IN_NOTIFICATION) {
-                int count = messages.size();
-                mBuilder.setContentText(
-                        mXmppConnectionService
-                                .getResources()
-                                .getQuantityString(R.plurals.x_messages, count, count));
+                            .getResources()
+                            .getQuantityString(R.plurals.x_messages, count, count));
+        } else {
+            final Message message;
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
+                    && (message = getImage(messages)) != null) {
+                modifyForImage(notificationBuilder, message, messages);
             } else {
-                Message message;
-                // TODO starting with Android 9 we might want to put images in MessageStyle
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
-                        && (message = getImage(messages)) != null) {
-                    modifyForImage(mBuilder, message, messages);
-                } else {
-                    modifyForTextOnly(mBuilder, messages);
-                }
-                RemoteInput remoteInput =
-                        new RemoteInput.Builder("text_reply")
-                                .setLabel(
-                                        UIHelper.getMessageHint(
-                                                mXmppConnectionService, conversation))
-                                .build();
-                PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
-                NotificationCompat.Action markReadAction =
-                        new NotificationCompat.Action.Builder(
-                                        R.drawable.ic_drafts_white_24dp,
-                                        mXmppConnectionService.getString(R.string.mark_as_read),
-                                        markAsReadPendingIntent)
-                                .setSemanticAction(
-                                        NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
-                                .setShowsUserInterface(false)
-                                .build();
-                final String replyLabel = mXmppConnectionService.getString(R.string.reply);
-                final String lastMessageUuid = Iterables.getLast(messages).getUuid();
-                final NotificationCompat.Action replyAction =
-                        new NotificationCompat.Action.Builder(
-                                        R.drawable.ic_send_text_offline,
-                                        replyLabel,
-                                        createReplyIntent(conversation, lastMessageUuid, false))
-                                .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
-                                .setShowsUserInterface(false)
-                                .addRemoteInput(remoteInput)
-                                .build();
-                final NotificationCompat.Action wearReplyAction =
+                modifyForTextOnly(notificationBuilder, messages);
+            }
+            RemoteInput remoteInput =
+                    new RemoteInput.Builder("text_reply")
+                            .setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation))
+                            .build();
+            PendingIntent markAsReadPendingIntent = createReadPendingIntent(conversation);
+            NotificationCompat.Action markReadAction =
+                    new NotificationCompat.Action.Builder(
+                                    R.drawable.ic_drafts_white_24dp,
+                                    mXmppConnectionService.getString(R.string.mark_as_read),
+                                    markAsReadPendingIntent)
+                            .setSemanticAction(
+                                    NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ)
+                            .setShowsUserInterface(false)
+                            .build();
+            final String replyLabel = mXmppConnectionService.getString(R.string.reply);
+            final String lastMessageUuid = Iterables.getLast(messages).getUuid();
+            final NotificationCompat.Action replyAction =
+                    new NotificationCompat.Action.Builder(
+                                    R.drawable.ic_send_text_offline,
+                                    replyLabel,
+                                    createReplyIntent(conversation, lastMessageUuid, false))
+                            .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
+                            .setShowsUserInterface(false)
+                            .addRemoteInput(remoteInput)
+                            .build();
+            final NotificationCompat.Action wearReplyAction =
+                    new NotificationCompat.Action.Builder(
+                                    R.drawable.ic_wear_reply,
+                                    replyLabel,
+                                    createReplyIntent(conversation, lastMessageUuid, true))
+                            .addRemoteInput(remoteInput)
+                            .build();
+            notificationBuilder.extend(
+                    new NotificationCompat.WearableExtender().addAction(wearReplyAction));
+            int addedActionsCount = 1;
+            notificationBuilder.addAction(markReadAction);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                notificationBuilder.addAction(replyAction);
+                ++addedActionsCount;
+            }
+
+            if (displaySnoozeAction(messages)) {
+                String label = mXmppConnectionService.getString(R.string.snooze);
+                PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
+                NotificationCompat.Action snoozeAction =
                         new NotificationCompat.Action.Builder(
-                                        R.drawable.ic_wear_reply,
-                                        replyLabel,
-                                        createReplyIntent(conversation, lastMessageUuid, true))
-                                .addRemoteInput(remoteInput)
+                                        R.drawable.ic_notifications_paused_white_24dp,
+                                        label,
+                                        pendingSnoozeIntent)
                                 .build();
-                mBuilder.extend(
-                        new NotificationCompat.WearableExtender().addAction(wearReplyAction));
-                int addedActionsCount = 1;
-                mBuilder.addAction(markReadAction);
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                    mBuilder.addAction(replyAction);
-                    ++addedActionsCount;
-                }
-
-                if (displaySnoozeAction(messages)) {
-                    String label = mXmppConnectionService.getString(R.string.snooze);
-                    PendingIntent pendingSnoozeIntent = createSnoozeIntent(conversation);
-                    NotificationCompat.Action snoozeAction =
-                            new NotificationCompat.Action.Builder(
-                                            R.drawable.ic_notifications_paused_white_24dp,
-                                            label,
-                                            pendingSnoozeIntent)
-                                    .build();
-                    mBuilder.addAction(snoozeAction);
-                    ++addedActionsCount;
-                }
-                if (addedActionsCount < 3) {
-                    final Message firstLocationMessage = getFirstLocationMessage(messages);
-                    if (firstLocationMessage != null) {
-                        final PendingIntent pendingShowLocationIntent =
-                                createShowLocationIntent(firstLocationMessage);
-                        if (pendingShowLocationIntent != null) {
-                            final String label =
-                                    mXmppConnectionService
-                                            .getResources()
-                                            .getString(R.string.show_location);
-                            NotificationCompat.Action locationAction =
-                                    new NotificationCompat.Action.Builder(
-                                                    R.drawable.ic_room_white_24dp,
-                                                    label,
-                                                    pendingShowLocationIntent)
-                                            .build();
-                            mBuilder.addAction(locationAction);
-                            ++addedActionsCount;
-                        }
-                    }
-                }
-                if (addedActionsCount < 3) {
-                    Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
-                    if (firstDownloadableMessage != null) {
-                        String label =
+                notificationBuilder.addAction(snoozeAction);
+                ++addedActionsCount;
+            }
+            if (addedActionsCount < 3) {
+                final Message firstLocationMessage = getFirstLocationMessage(messages);
+                if (firstLocationMessage != null) {
+                    final PendingIntent pendingShowLocationIntent =
+                            createShowLocationIntent(firstLocationMessage);
+                    if (pendingShowLocationIntent != null) {
+                        final String label =
                                 mXmppConnectionService
                                         .getResources()
-                                        .getString(
-                                                R.string.download_x_file,
-                                                UIHelper.getFileDescriptionString(
-                                                        mXmppConnectionService,
-                                                        firstDownloadableMessage));
-                        PendingIntent pendingDownloadIntent =
-                                createDownloadIntent(firstDownloadableMessage);
-                        NotificationCompat.Action downloadAction =
+                                        .getString(R.string.show_location);
+                        NotificationCompat.Action locationAction =
                                 new NotificationCompat.Action.Builder(
-                                                R.drawable.ic_file_download_white_24dp,
+                                                R.drawable.ic_room_white_24dp,
                                                 label,
-                                                pendingDownloadIntent)
+                                                pendingShowLocationIntent)
                                         .build();
-                        mBuilder.addAction(downloadAction);
+                        notificationBuilder.addAction(locationAction);
                         ++addedActionsCount;
                     }
                 }
             }
-            final ShortcutInfoCompat info;
-            if (conversation.getMode() == Conversation.MODE_SINGLE) {
-                final Contact contact = conversation.getContact();
-                final Uri systemAccount = contact.getSystemAccount();
-                if (systemAccount != null) {
-                    mBuilder.addPerson(systemAccount.toString());
+            if (addedActionsCount < 3) {
+                Message firstDownloadableMessage = getFirstDownloadableMessage(messages);
+                if (firstDownloadableMessage != null) {
+                    String label =
+                            mXmppConnectionService
+                                    .getResources()
+                                    .getString(
+                                            R.string.download_x_file,
+                                            UIHelper.getFileDescriptionString(
+                                                    mXmppConnectionService,
+                                                    firstDownloadableMessage));
+                    PendingIntent pendingDownloadIntent =
+                            createDownloadIntent(firstDownloadableMessage);
+                    NotificationCompat.Action downloadAction =
+                            new NotificationCompat.Action.Builder(
+                                            R.drawable.ic_file_download_white_24dp,
+                                            label,
+                                            pendingDownloadIntent)
+                                    .build();
+                    notificationBuilder.addAction(downloadAction);
+                    ++addedActionsCount;
                 }
-                info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
-            } else {
-                info =
-                        mXmppConnectionService
-                                .getShortcutService()
-                                .getShortcutInfoCompat(conversation.getMucOptions());
             }
-            mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
-            mBuilder.setSmallIcon(R.drawable.ic_notification);
-            mBuilder.setDeleteIntent(createDeleteIntent(conversation));
-            mBuilder.setContentIntent(createContentIntent(conversation));
-            if (mXmppConnectionService.getAccounts().size() > 1) {
-                mBuilder.setSubText(conversation.getAccount().getJid().asBareJid().toString());
+        }
+        final ShortcutInfoCompat info;
+        if (conversation.getMode() == Conversation.MODE_SINGLE) {
+            final Contact contact = conversation.getContact();
+            final Uri systemAccount = contact.getSystemAccount();
+            if (systemAccount != null) {
+                notificationBuilder.addPerson(systemAccount.toString());
             }
-
-            mBuilder.setShortcutInfo(info);
+            info = mXmppConnectionService.getShortcutService().getShortcutInfoCompat(contact);
+        } else {
+            info =
+                    mXmppConnectionService
+                            .getShortcutService()
+                            .getShortcutInfoCompat(conversation.getMucOptions());
+        }
+        notificationBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
+        notificationBuilder.setSmallIcon(R.drawable.ic_notification);
+        notificationBuilder.setDeleteIntent(createDeleteIntent(conversation));
+        notificationBuilder.setContentIntent(createContentIntent(conversation));
+        if (mXmppConnectionService.getAccounts().size() > 1) {
+            notificationBuilder.setSubText(conversation.getAccount().getJid().asBareJid().toString());
+        }
+        if (channel.equals(MESSAGES_NOTIFICATION_CHANNEL)) {
+            // when do not want 'customized' notifications for silent notifications in their
+            // respective channels
+            notificationBuilder.setShortcutInfo(info);
             if (Build.VERSION.SDK_INT >= 30) {
                 mXmppConnectionService
                         .getSystemService(ShortcutManager.class)
@@ -1432,7 +1433,7 @@ public class NotificationService {
                 // mBuilder.setBubbleMetadata(new NotificationCompat.BubbleMetadata.Builder(info.getId()).build());
             }
         }
-        return mBuilder;
+        return notificationBuilder;
     }
 
     private void modifyForImage(

src/main/java/eu/siacs/conversations/services/UnifiedPushBroker.java 🔗

@@ -12,6 +12,7 @@ import android.preference.PreferenceManager;
 import android.util.Log;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import com.google.common.base.Optional;
 import com.google.common.base.Strings;
@@ -85,11 +86,11 @@ public class UnifiedPushBroker {
         service.sendPresencePacket(account, presence);
     }
 
-    public Optional<Transport> renewUnifiedPushEndpoints() {
-        return renewUnifiedPushEndpoints(null);
+    public void renewUnifiedPushEndpoints() {
+        renewUnifiedPushEndpoints(null);
     }
 
-    public Optional<Transport> renewUnifiedPushEndpoints(final PushTargetMessenger pushTargetMessenger) {
+    public Optional<Transport> renewUnifiedPushEndpoints(@Nullable final PushTargetMessenger pushTargetMessenger) {
         final Optional<Transport> transportOptional = getTransport();
         if (transportOptional.isPresent()) {
             final Transport transport = transportOptional.get();

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1090,28 +1090,45 @@ public class XmppConnectionService extends Service {
         manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
     }
 
-    private synchronized void manageAccountConnectionStates(final String action, final Bundle extras) {
-        Log.d(Config.LOGTAG, "manageAccountConnectionStates: " + action);
+    private synchronized void manageAccountConnectionStates(
+            final String action, final Bundle extras) {
         final String pushedAccountHash = extras == null ? null : extras.getString("account");
-        final boolean interactive = Arrays.asList(ACTION_TRY_AGAIN).contains(action);
+        final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action);
         WakeLockHelper.acquire(wakeLock);
-        boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
+        boolean pingNow =
+                ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
+                        || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0
+                                && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
         final HashSet<Account> pingCandidates = new HashSet<>();
-        final String androidId = PhoneHelper.getAndroidId(this);
+        final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this);
         for (final Account account : accounts) {
-            final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash);
-            pingNow |= processAccountState(account,
-                    interactive,
-                    "ui".equals(action),
-                    pushWasMeantForThisAccount,
-                    pingCandidates);
+            final boolean pushWasMeantForThisAccount =
+                    androidId != null
+                            && CryptoHelper.getAccountFingerprint(account, androidId)
+                                    .equals(pushedAccountHash);
+            pingNow |=
+                    processAccountState(
+                            account,
+                            interactive,
+                            "ui".equals(action),
+                            pushWasMeantForThisAccount,
+                            pingCandidates);
         }
         if (pingNow) {
-            for (Account account : pingCandidates) {
+            for (final Account account : pingCandidates) {
                 final boolean lowTimeout = isInLowPingTimeoutMode(account);
                 account.getXmppConnection().sendPing();
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")");
-                scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + " send ping (action="
+                                + action
+                                + ",lowTimeout="
+                                + lowTimeout
+                                + ")");
+                scheduleWakeUpCall(
+                        lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
+                        account.getUuid().hashCode());
             }
             long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
             if (msToMucPing <= 0) {
@@ -1484,7 +1501,11 @@ public class XmppConnectionService extends Service {
 
         restoreFromDatabase();
 
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
+        if (QuickConversationsService.isContactListIntegration(this)
+                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+                        || ContextCompat.checkSelfPermission(
+                                        this, Manifest.permission.READ_CONTACTS)
+                                == PackageManager.PERMISSION_GRANTED)) {
             startContactObserver();
         }
         FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
@@ -1692,11 +1713,17 @@ public class XmppConnectionService extends Service {
         Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off"));
     }
 
-    private void startForegroundOrCatch(final int id, final Notification notification, boolean needMic) {
+    private void startForegroundOrCatch(
+            final int id, final Notification notification, final boolean requireMicrophone) {
         try {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
-                int foregroundServiceType;
-                if (getSystemService(PowerManager.class)
+                final int foregroundServiceType;
+                if (requireMicrophone
+                        && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+                                == PackageManager.PERMISSION_GRANTED) {
+                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
+                    Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
+                } else if (getSystemService(PowerManager.class)
                         .isIgnoringBatteryOptimizations(getPackageName())) {
                     foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
                 } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
@@ -1707,14 +1734,9 @@ public class XmppConnectionService extends Service {
                     foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
                 } else {
                     foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
-                    Log.w(Config.LOGTAG,"falling back to special use foreground service type");
+                    Log.w(Config.LOGTAG, "falling back to special use foreground service type");
                 }
 
-                if (needMic && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
-                        == PackageManager.PERMISSION_GRANTED) {
-                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
-                 }
-
                 startForeground(id, notification, foregroundServiceType);
             } else {
                 startForeground(id, notification);

src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java 🔗

@@ -62,6 +62,7 @@ import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
 import eu.siacs.conversations.services.AbstractQuickConversationsService;
+import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
 import eu.siacs.conversations.ui.adapter.MediaAdapter;
@@ -140,13 +141,13 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
     private void checkContactPermissionAndShowAddDialog() {
         if (hasContactsPermission()) {
             showAddToPhoneBookDialog();
-        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        } else if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
         }
     }
 
     private boolean hasContactsPermission() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (QuickConversationsService.isContactListIntegration(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             return checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED;
         } else {
             return true;
@@ -612,18 +613,30 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp
         }
     }
 
-    private void onBadgeClick(View view) {
-        final Uri systemAccount = contact.getSystemAccount();
-        if (systemAccount == null) {
-            checkContactPermissionAndShowAddDialog();
-        } else {
-            final Intent intent = new Intent(Intent.ACTION_VIEW);
-            intent.setData(systemAccount);
-            try {
-                startActivity(intent);
-            } catch (final ActivityNotFoundException e) {
-                Toast.makeText(this, R.string.no_application_found_to_view_contact, Toast.LENGTH_SHORT).show();
+    private void onBadgeClick(final View view) {
+        if (QuickConversationsService.isContactListIntegration(this)) {
+            final Uri systemAccount = contact.getSystemAccount();
+            if (systemAccount == null) {
+                checkContactPermissionAndShowAddDialog();
+            } else {
+                final Intent intent = new Intent(Intent.ACTION_VIEW);
+                intent.setData(systemAccount);
+                try {
+                    startActivity(intent);
+                } catch (final ActivityNotFoundException e) {
+                    Toast.makeText(
+                                    this,
+                                    R.string.no_application_found_to_view_contact,
+                                    Toast.LENGTH_SHORT)
+                            .show();
+                }
             }
+        } else {
+            Toast.makeText(
+                            this,
+                            R.string.contact_list_integration_not_available,
+                            Toast.LENGTH_SHORT)
+                    .show();
         }
     }
 

src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java 🔗

@@ -60,6 +60,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
+import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding;
@@ -67,6 +68,7 @@ import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.ui.adapter.ConversationAdapter;
 import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
 import eu.siacs.conversations.ui.interfaces.OnConversationSelected;

src/main/java/eu/siacs/conversations/ui/RecordingActivity.java 🔗

@@ -19,6 +19,8 @@ import android.widget.Toast;
 
 import androidx.databinding.DataBindingUtil;
 
+import com.google.common.collect.ImmutableSet;
+
 import java.io.File;
 import java.lang.ref.WeakReference;
 import java.text.SimpleDateFormat;
@@ -27,6 +29,7 @@ import java.util.Locale;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
+import java.util.Set;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -101,6 +104,16 @@ public class RecordingActivity extends Activity implements View.OnClickListener
         return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
     }
 
+    private static final Set<String> AAC_SENSITIVE_DEVICES =
+            new ImmutableSet.Builder<String>()
+                    .add("FP4")             // Fairphone 4 https://codeberg.org/monocles/monocles_chat/issues/133
+                    .add("ONEPLUS A6000")   // OnePlus 6 https://github.com/iNPUTmice/Conversations/issues/4329
+                    .add("ONEPLUS A6003")   // OnePlus 6 https://github.com/iNPUTmice/Conversations/issues/4329
+                    .add("ONEPLUS A6010")   // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133
+                    .add("ONEPLUS A6013")   // OnePlus 6T https://codeberg.org/monocles/monocles_chat/issues/133
+                    .add("Pixel 4a")        // Pixel 4a https://github.com/iNPUTmice/Conversations/issues/4223
+                    .build();
+
     private boolean startRecording() {
         mRecorder = new MediaRecorder();
         mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
@@ -114,9 +127,16 @@ public class RecordingActivity extends Activity implements View.OnClickListener
         } else if ("mpeg4".equals(userChosenCodec) || !Config.USE_OPUS_VOICE_MESSAGES) {
             outputFormat = MediaRecorder.OutputFormat.MPEG_4;
             mRecorder.setOutputFormat(outputFormat);
-            mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
-            mRecorder.setAudioEncodingBitRate(96000);
-            mRecorder.setAudioSamplingRate(22050);
+            if (AAC_SENSITIVE_DEVICES.contains(Build.MODEL)) {
+                // Changing these three settings for AAC sensitive devices might lead to sporadically truncated (cut-off) voice messages.
+                mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);
+                mRecorder.setAudioSamplingRate(24_000);
+                mRecorder.setAudioEncodingBitRate(28_000);
+            } else {
+                mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
+                mRecorder.setAudioSamplingRate(22_050);
+                mRecorder.setAudioEncodingBitRate(64_000);
+            }
         } else {
             outputFormat = MediaRecorder.OutputFormat.THREE_GPP;
             mRecorder.setOutputFormat(outputFormat);

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java 🔗

@@ -13,6 +13,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.opengl.GLException;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -881,8 +882,12 @@ public class RtpSessionActivity extends XmppActivity
         surfaceViewRenderer.setVisibility(View.VISIBLE);
         try {
             surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
-        } catch (final IllegalStateException e) {
-            // Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
+        } catch (final IllegalStateException ignored) {
+            // SurfaceViewRenderer was already initialized
+        } catch (final RuntimeException e) {
+            if (Throwables.getRootCause(e) instanceof GLException glException) {
+                Log.w(Config.LOGTAG, "could not set up hardware renderer", glException);
+            }
         }
         surfaceViewRenderer.setEnableHardwareScaler(true);
     }

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -6,6 +6,7 @@ import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
@@ -75,6 +76,7 @@ import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
+import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityStartConversationBinding;
@@ -108,6 +110,8 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
 
+    private static final String PREF_KEY_CONTACT_INTEGRATION_CONSENT = "contact_list_integration_consent";
+
     public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
 
     private final int REQUEST_SYNC_CONTACTS = 0x28cf;
@@ -718,11 +722,15 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
+    public boolean onCreateOptionsMenu(final Menu menu) {
         getMenuInflater().inflate(R.menu.start_conversation, menu);
         AccountUtils.showHideMenuItems(menu);
-        MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
-        MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
+        final MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
+        final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
+        final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy);
+        privacyPolicyMenuItem.setVisible(
+                BuildConfig.PRIVACY_POLICY != null
+                        && QuickConversationsService.isPlayStoreFlavor());
         qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
         if (QuickConversationsService.isQuicksy()) {
             menuHideOffline.setVisible(false);
@@ -851,39 +859,96 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     }
 
     private void askForContactsPermissions() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+        if (QuickConversationsService.isContactListIntegration(this)
+                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                    != PackageManager.PERMISSION_GRANTED) {
                 if (mRequestedContactsPermission.compareAndSet(false, true)) {
-                    if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
+                    final String consent =
+                            PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
+                                    .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
+                    final boolean requiresConsent =
+                            (QuickConversationsService.isQuicksy()
+                                            || QuickConversationsService.isPlayStoreFlavor())
+                                    && !"agreed".equals(consent);
+                    if (requiresConsent && "declined".equals(consent)) {
+                        Log.d(Config.LOGTAG,"not asking for contacts permission because consent has been declined");
+                        return;
+                    }
+                    if (requiresConsent
+                            || shouldShowRequestPermissionRationale(
+                                    Manifest.permission.READ_CONTACTS)) {
                         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
                         final AtomicBoolean requestPermission = new AtomicBoolean(false);
-                        builder.setTitle(R.string.sync_with_contacts);
-                        builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
-                        builder.setPositiveButton(R.string.next, (dialog, which) -> {
-                            if (requestPermission.compareAndSet(false, true)) {
-                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
-                            }
-                        });
-                        builder.setOnDismissListener(dialog -> {
-                            if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) {
-                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
-                            }
-                        });
                         if (QuickConversationsService.isQuicksy()) {
-                            builder.setNegativeButton(R.string.decline, null);
+                            builder.setTitle(R.string.quicksy_wants_your_consent);
+                            builder.setMessage(
+                                    Html.fromHtml(
+                                            getString(R.string.sync_with_contacts_quicksy_static)));
+                        } else {
+                            builder.setTitle(R.string.sync_with_contacts);
+                            builder.setMessage(
+                                    getString(
+                                            R.string.sync_with_contacts_long,
+                                            getString(R.string.app_name)));
+                        }
+                        @StringRes int confirmButtonText;
+                        if (requiresConsent) {
+                            confirmButtonText = R.string.agree_and_continue;
+                        } else {
+                            confirmButtonText = R.string.next;
                         }
-                        builder.setCancelable(QuickConversationsService.isQuicksy());
+                        builder.setPositiveButton(
+                                confirmButtonText,
+                                (dialog, which) -> {
+                                    if (requiresConsent) {
+                                        PreferenceManager.getDefaultSharedPreferences(
+                                                        getApplicationContext())
+                                                .edit()
+                                                .putString(
+                                                        PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
+                                                .apply();
+                                    }
+                                    if (requestPermission.compareAndSet(false, true)) {
+                                        requestPermissions(
+                                                new String[] {Manifest.permission.READ_CONTACTS},
+                                                REQUEST_SYNC_CONTACTS);
+                                    }
+                                });
+                        if (requiresConsent) {
+                            builder.setNegativeButton(R.string.decline, (dialog, which) -> PreferenceManager.getDefaultSharedPreferences(
+                                            getApplicationContext())
+                                    .edit()
+                                    .putString(
+                                            PREF_KEY_CONTACT_INTEGRATION_CONSENT, "declined")
+                                    .apply());
+                        } else {
+                            builder.setOnDismissListener(
+                                    dialog -> {
+                                        if (requestPermission.compareAndSet(false, true)) {
+                                            requestPermissions(
+                                                    new String[] {
+                                                        Manifest.permission.READ_CONTACTS
+                                                    },
+                                                    REQUEST_SYNC_CONTACTS);
+                                        }
+                                    });
+                        }
+                        builder.setCancelable(requiresConsent);
                         final AlertDialog dialog = builder.create();
-                        dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
-                        dialog.setOnShowListener(dialogInterface -> {
-                            final TextView tv = dialog.findViewById(android.R.id.message);
-                            if (tv != null) {
-                                tv.setMovementMethod(LinkMovementMethod.getInstance());
-                            }
-                        });
+                        dialog.setCanceledOnTouchOutside(requiresConsent);
+                        dialog.setOnShowListener(
+                                dialogInterface -> {
+                                    final TextView tv = dialog.findViewById(android.R.id.message);
+                                    if (tv != null) {
+                                        tv.setMovementMethod(LinkMovementMethod.getInstance());
+                                    }
+                                });
                         dialog.show();
                     } else {
-                        requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
+                        requestPermissions(
+                                new String[] {Manifest.permission.READ_CONTACTS},
+                                REQUEST_SYNC_CONTACTS);
                     }
                 }
             }
@@ -919,8 +984,10 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
     @Override
     protected void onBackendConnected() {
-
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
+        if (QuickConversationsService.isContactListIntegration(this)
+                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+                        || checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                                == PackageManager.PERMISSION_GRANTED)) {
             xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
         }
         if (mPostponedActivityResult != null) {

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -69,6 +69,7 @@ import java.util.List;
 import java.util.PriorityQueue;
 import java.util.concurrent.RejectedExecutionException;
 
+import eu.siacs.conversations.BuildConfig;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpEngine;
@@ -438,6 +439,9 @@ public abstract class XmppActivity extends ActionBarActivity {
             case R.id.action_settings:
                 startActivity(new Intent(this, SettingsActivity.class));
                 break;
+            case R.id.action_privacy_policy:
+                openPrivacyPolicy();
+                break;
             case R.id.action_accounts:
                 AccountUtils.launchManageAccounts(this);
                 break;
@@ -454,6 +458,20 @@ public abstract class XmppActivity extends ActionBarActivity {
         return super.onOptionsItemSelected(item);
     }
 
+    private void openPrivacyPolicy() {
+        if (BuildConfig.PRIVACY_POLICY == null) {
+            return;
+        }
+        final var viewPolicyIntent = new Intent(Intent.ACTION_VIEW);
+        viewPolicyIntent.setData(Uri.parse(BuildConfig.PRIVACY_POLICY));
+        try {
+            startActivity(viewPolicyIntent);
+        } catch (final ActivityNotFoundException e) {
+            Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT)
+                    .show();
+        }
+    }
+
     public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
         final Contact contact = conversation.getContact();
         if (contact.showInRoster() || contact.isSelf()) {

src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java 🔗

@@ -90,9 +90,7 @@ public class FixedURLSpan extends URLSpan {
 		}
 
 		final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-			intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-		}
+		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
 		//intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
 		try {
 			context.startActivity(intent);

src/main/java/eu/siacs/conversations/utils/MimeUtils.java 🔗

@@ -68,6 +68,7 @@ public final class MimeUtils {
         // by guessExtensionFromMimeType.
         add("application/andrew-inset", "ez");
         add("application/dsptype", "tsp");
+        add("application/json", "json");
         add("application/epub+zip", "epub");
         add("application/gpx+xml", "gpx");
         add("application/hta", "hta");

src/main/java/eu/siacs/conversations/utils/PhoneHelper.java 🔗

@@ -10,34 +10,37 @@ import android.os.Build;
 import android.provider.ContactsContract.Profile;
 import android.provider.Settings;
 
+import com.google.common.base.Strings;
+
+import eu.siacs.conversations.services.QuickConversationsService;
+
 public class PhoneHelper {
 
     @SuppressLint("HardwareIds")
-    public static String getAndroidId(Context context) {
+    public static String getAndroidId(final Context context) {
         return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
     }
 
-    public static Uri getProfilePictureUri(Context context) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
-                && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
-                        != PackageManager.PERMISSION_GRANTED) {
+    public static Uri getProfilePictureUri(final Context context) {
+        if (!QuickConversationsService.isContactListIntegration(context)
+                || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+                        && context.checkSelfPermission(Manifest.permission.READ_CONTACTS)
+                                != PackageManager.PERMISSION_GRANTED)) {
             return null;
         }
         final String[] projection = new String[] {Profile._ID, Profile.PHOTO_URI};
-        final Cursor cursor;
-        try {
-            cursor =
-                    context.getContentResolver()
-                            .query(Profile.CONTENT_URI, projection, null, null, null);
-        } catch (Throwable e) {
-            return null;
-        }
-        if (cursor == null) {
-            return null;
+        try (final Cursor cursor =
+                context.getContentResolver()
+                        .query(Profile.CONTENT_URI, projection, null, null, null)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                final var photoUri = cursor.getString(1);
+                if (Strings.isNullOrEmpty(photoUri)) {
+                    return null;
+                }
+                return Uri.parse(photoUri);
+            }
         }
-        final String uri = cursor.moveToFirst() ? cursor.getString(1) : null;
-        cursor.close();
-        return uri == null ? null : Uri.parse(uri);
+        return null;
     }
 
     public static boolean isEmulator() {

src/main/java/eu/siacs/conversations/utils/Resolver.java 🔗

@@ -277,15 +277,15 @@ public class Resolver {
                 threads[2].interrupt();
                 synchronized (results) {
                     Collections.sort(results);
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results.toString());
-                    return new ArrayList<>(results);
+                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + results);
+                    return results;
                 }
             } else {
                 threads[2].join();
                 synchronized (fallbackResults) {
                     Collections.sort(fallbackResults);
-                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults.toString());
-                    return new ArrayList<>(fallbackResults);
+                    Log.d(Config.LOGTAG, Resolver.class.getSimpleName() + ": " + fallbackResults);
+                    return fallbackResults;
                 }
             }
         } catch (InterruptedException e) {
@@ -371,7 +371,7 @@ public class Resolver {
     }
 
     private static List<Result> resolveNoSrvRecords(DnsName dnsName, boolean withCnames) {
-        List<Result> results = new ArrayList<>();
+        final List<Result> results = new ArrayList<>();
         try {
             ResolverResult<A> aResult = resolveWithFallback(dnsName, A.class);
             for (A a : aResult.getAnswersOrEmptySet()) {

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -116,8 +116,50 @@ import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
 import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
 import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
 import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
+
 import okhttp3.HttpUrl;
 
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ConnectException;
+import java.net.IDN;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Principal;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Matcher;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
+
 public class XmppConnection implements Runnable {
 
     private static final int PACKET_IQ = 0;
@@ -300,7 +342,8 @@ public class XmppConnection implements Runnable {
             mXmppConnectionService.resetSendingToWaiting(account);
         }
         Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
-        features.encryptionEnabled = false;
+        this.loginInfo = null;
+        this.features.encryptionEnabled = false;
         this.inSmacksSession = false;
         this.quickStartInProgress = false;
         this.isBound = false;
@@ -353,12 +396,13 @@ public class XmppConnection implements Runnable {
                 }
             } else {
                 final String domain = account.getServer();
-                final List<Resolver.Result> results;
+                final List<Resolver.Result> results = new ArrayList<>();
                 final boolean hardcoded = extended && !account.getHostname().isEmpty();
                 if (hardcoded) {
-                    results = Resolver.fromHardCoded(account.getHostname(), account.getPort());
+                    results.addAll(
+                            Resolver.fromHardCoded(account.getHostname(), account.getPort()));
                 } else {
-                    results = Resolver.resolve(domain);
+                    results.addAll(Resolver.resolve(domain));
                 }
                 if (Thread.currentThread().isInterrupted()) {
                     Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": Thread was interrupted");
@@ -388,12 +432,18 @@ public class XmppConnection implements Runnable {
                 final StreamId streamId = this.streamId;
                 final Resolver.Result resumeLocation = streamId == null ? null : streamId.location;
                 if (resumeLocation != null) {
-                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected resume location on position 0");
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": injected resume location on position 0");
                     results.add(0, resumeLocation);
                 }
                 final Resolver.Result seeOtherHost = this.seeOtherHostResolverResult;
                 if (seeOtherHost != null) {
-                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": injected see-other-host on position 0");
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": injected see-other-host on position 0");
                     results.add(0, seeOtherHost);
                 }
                 for (final Iterator<Resolver.Result> iterator = results.iterator();
@@ -535,8 +585,7 @@ public class XmppConnection implements Runnable {
         tagReader.setInputStream(socket.getInputStream());
         tagWriter.beginDocument();
         final boolean quickStart;
-        if (socket instanceof SSLSocket) {
-            final SSLSocket sslSocket = (SSLSocket) socket;
+        if (socket instanceof SSLSocket sslSocket) {
             SSLSockets.log(account, sslSocket);
             quickStart = establishStream(SSLSockets.version(sslSocket));
         } else {
@@ -546,7 +595,16 @@ public class XmppConnection implements Runnable {
         if (Thread.currentThread().isInterrupted()) {
             throw new InterruptedException();
         }
-        final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS);
+        if (tag == null) {
+            return false;
+        }
+        final boolean success = tag.isStart("stream", Namespace.STREAMS);
+        if (success) {
+            final var from = tag.getAttribute("from");
+            if (from == null || !from.equals(account.getServer())) {
+                throw new StateChangingException(Account.State.HOST_UNKNOWN);
+            }
+        }
         if (success && quickStart) {
             this.quickStartInProgress = true;
         }
@@ -603,13 +661,18 @@ public class XmppConnection implements Runnable {
                 processStreamFeatures(nextTag);
             } else if (nextTag.isStart("proceed", Namespace.TLS)) {
                 switchOverToTls();
+            } else if (nextTag.isStart("failure", Namespace.TLS)) {
+                throw new StateChangingException(Account.State.TLS_ERROR);
+            } else if (account.isOptionSet(Account.OPTION_REGISTER)
+                    && nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
+                processIq(nextTag);
+            } else if (!isSecure() || this.loginInfo == null) {
+                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
             } else if (nextTag.isStart("success")) {
                 final Element success = tagReader.readElement(nextTag);
                 if (processSuccess(success)) {
                     break;
                 }
-            } else if (nextTag.isStart("failure", Namespace.TLS)) {
-                throw new StateChangingException(Account.State.TLS_ERROR);
             } else if (nextTag.isStart("failure")) {
                 final Element failure = tagReader.readElement(nextTag);
                 processFailure(failure);
@@ -617,22 +680,33 @@ public class XmppConnection implements Runnable {
                 // two step sasl2 - we don’t support this yet
                 throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
             } else if (nextTag.isStart("challenge")) {
-                if (isSecure() && this.loginInfo != null) {
-                    final Element challenge = tagReader.readElement(nextTag);
-                    processChallenge(challenge);
-                } else {
-                    Log.d(
-                            Config.LOGTAG,
-                            account.getJid().asBareJid()
-                                    + ": received 'challenge on an unsecure connection");
-                    throw new StateChangingException(Account.State.INCOMPATIBLE_CLIENT);
-                }
+                final Element challenge = tagReader.readElement(nextTag);
+                processChallenge(challenge);
+            } else if (!LoginInfo.isSuccess(this.loginInfo)) {
+                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+            } else if (this.streamId != null
+                    && nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
+                final Element resumed = tagReader.readElement(nextTag);
+                processResumed(resumed);
+            } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
+                final Element failed = tagReader.readElement(nextTag);
+                processFailed(failed, true);
+            } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
+                processIq(nextTag);
+            } else if (!isBound) {
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": server sent unexpected"
+                                + nextTag.identifier());
+                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+            } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
+                processMessage(nextTag);
+            } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
+                processPresence(nextTag);
             } else if (nextTag.isStart("enabled", Namespace.STREAM_MANAGEMENT)) {
                 final Element enabled = tagReader.readElement(nextTag);
                 processEnabled(enabled);
-            } else if (nextTag.isStart("resumed", Namespace.STREAM_MANAGEMENT)) {
-                final Element resumed = tagReader.readElement(nextTag);
-                processResumed(resumed);
             } else if (nextTag.isStart("r", Namespace.STREAM_MANAGEMENT)) {
                 tagReader.readElement(nextTag);
                 if (Config.EXTENDED_SM_LOGGING) {
@@ -687,15 +761,6 @@ public class XmppConnection implements Runnable {
                 if (acknowledgedMessages) {
                     mXmppConnectionService.updateConversationUi();
                 }
-            } else if (nextTag.isStart("failed", Namespace.STREAM_MANAGEMENT)) {
-                final Element failed = tagReader.readElement(nextTag);
-                processFailed(failed, true);
-            } else if (nextTag.isStart("iq", Namespace.JABBER_CLIENT)) {
-                processIq(nextTag);
-            } else if (nextTag.isStart("message", Namespace.JABBER_CLIENT)) {
-                processMessage(nextTag);
-            } else if (nextTag.isStart("presence", Namespace.JABBER_CLIENT)) {
-                processPresence(nextTag);
             } else {
                 Log.e(
                         Config.LOGTAG,
@@ -726,8 +791,14 @@ public class XmppConnection implements Runnable {
         } else {
             throw new AssertionError("Missing implementation for " + version);
         }
+        final LoginInfo currentLoginInfo = this.loginInfo;
+        if (currentLoginInfo == null || LoginInfo.isSuccess(currentLoginInfo)) {
+            throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
+        }
         try {
-            response.setContent(this.loginInfo.saslMechanism.getResponse(challenge.getContent(), sslSocketOrNull(socket)));
+            response.setContent(
+                    currentLoginInfo.saslMechanism.getResponse(
+                            challenge.getContent(), sslSocketOrNull(socket)));
         } catch (final SaslMechanism.AuthenticationException e) {
             // TODO: Send auth abort tag.
             Log.e(Config.LOGTAG, e.toString());
@@ -758,9 +829,9 @@ public class XmppConnection implements Runnable {
             throw new AssertionError("Missing implementation for " + version);
         }
         try {
-            currentSaslMechanism.getResponse(challenge, sslSocketOrNull(socket));
+            currentLoginInfo.success(challenge, sslSocketOrNull(socket));
         } catch (final SaslMechanism.AuthenticationException e) {
-            Log.e(Config.LOGTAG, String.valueOf(e));
+            Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": authentication failure ", e);
             throw new StateChangingException(Account.State.UNAUTHORIZED);
         }
         Log.d(
@@ -822,7 +893,10 @@ public class XmppConnection implements Runnable {
             if (resumed != null && streamId != null) {
                 if (this.boundStreamFeatures != null) {
                     this.streamFeatures = this.boundStreamFeatures;
-                    Log.d(Config.LOGTAG, "putting previous stream features back in place: " + XmlHelper.printElementNames(this.boundStreamFeatures));
+                    Log.d(
+                            Config.LOGTAG,
+                            "putting previous stream features back in place: "
+                                    + XmlHelper.printElementNames(this.boundStreamFeatures));
                 }
                 processResumed(resumed);
             } else if (failed != null) {
@@ -842,7 +916,7 @@ public class XmppConnection implements Runnable {
                     processEnabled(streamManagementEnabled);
                     waitForDisco = true;
                 } else {
-                    //if we did not enable stream management in bind do it now
+                    // if we did not enable stream management in bind do it now
                     waitForDisco = enableStreamManagement();
                 }
                 final boolean negotiatedCarbons;
@@ -874,13 +948,22 @@ public class XmppConnection implements Runnable {
                 tokenMechanism = null;
             }
             if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
-                if (ChannelBinding.priority(tokenMechanism.channelBinding) >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
+                if (ChannelBinding.priority(tokenMechanism.channelBinding)
+                        >= ChannelBindingMechanism.getPriority(currentSaslMechanism)) {
                     this.account.setFastToken(tokenMechanism, token);
                     Log.d(
                             Config.LOGTAG,
-                            account.getJid().asBareJid() + ": storing hashed token " + tokenMechanism);
+                            account.getJid().asBareJid()
+                                    + ": storing hashed token "
+                                    + tokenMechanism);
                 } else {
-                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": not accepting hashed token "+ tokenMechanism.name()+" for log in mechanism "+currentSaslMechanism.getMechanism());
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": not accepting hashed token "
+                                    + tokenMechanism.name()
+                                    + " for log in mechanism "
+                                    + currentSaslMechanism.getMechanism());
                     this.account.resetFastToken();
                 }
             } else if (this.hashTokenRequest != null) {
@@ -1033,7 +1116,9 @@ public class XmppConnection implements Runnable {
         } else {
             Log.d(
                     Config.LOGTAG,
-                    account.getJid().asBareJid() + ": stream management enabled. resume at: " + streamId.location);
+                    account.getJid().asBareJid()
+                            + ": stream management enabled. resume at: "
+                            + streamId.location);
         }
         this.streamId = streamId;
         this.stanzasReceived = 0;
@@ -1079,8 +1164,7 @@ public class XmppConnection implements Runnable {
                 Config.LOGTAG,
                 account.getJid().asBareJid() + ": resending " + failedStanzas.size() + " stanzas");
         for (final AbstractAcknowledgeableStanza packet : failedStanzas) {
-            if (packet instanceof MessagePacket) {
-                MessagePacket message = (MessagePacket) packet;
+            if (packet instanceof MessagePacket message) {
                 mXmppConnectionService.markMessage(
                         account,
                         message.getTo().asBareJid(),
@@ -1143,8 +1227,7 @@ public class XmppConnection implements Runnable {
                                     + mStanzaQueue.keyAt(i));
                 }
                 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
-                if (stanza instanceof MessagePacket && acknowledgedListener != null) {
-                    final MessagePacket packet = (MessagePacket) stanza;
+                if (stanza instanceof MessagePacket packet && acknowledgedListener != null) {
                     final String id = packet.getId();
                     final Jid to = packet.getTo();
                     if (id != null && to != null) {
@@ -1161,20 +1244,13 @@ public class XmppConnection implements Runnable {
 
     private @NonNull Element processPacket(final Tag currentTag, final int packetType)
             throws IOException {
-        final Element element;
-        switch (packetType) {
-            case PACKET_IQ:
-                element = new IqPacket();
-                break;
-            case PACKET_MESSAGE:
-                element = new MessagePacket();
-                break;
-            case PACKET_PRESENCE:
-                element = new PresencePacket();
-                break;
-            default:
-                throw new AssertionError("Should never encounter invalid type");
-        }
+        final Element element =
+                switch (packetType) {
+                    case PACKET_IQ -> new IqPacket();
+                    case PACKET_MESSAGE -> new MessagePacket();
+                    case PACKET_PRESENCE -> new PresencePacket();
+                    default -> throw new AssertionError("Should never encounter invalid type");
+                };
         element.setAttributes(currentTag.getAttributes());
         Tag nextTag = tagReader.readTag();
         if (nextTag == null) {
@@ -1228,57 +1304,77 @@ public class XmppConnection implements Runnable {
                             + "'");
             return;
         }
-        if (packet instanceof JinglePacket) {
+        if (Thread.currentThread().isInterrupted()) {
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid() + "Not processing iq. Thread was interrupted");
+            return;
+        }
+        if (packet instanceof JinglePacket jinglePacket && isBound) {
             if (this.jingleListener != null) {
-                this.jingleListener.onJinglePacketReceived(account, (JinglePacket) packet);
+                this.jingleListener.onJinglePacketReceived(account, jinglePacket);
+            }
+        } else {
+            final var callback = getIqPacketReceivedCallback(packet);
+            if (callback == null) {
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid().toString()
+                                + ": no callback registered for IQ from "
+                                + packet.getFrom());
+                return;
+            }
+            final ScheduledFuture timeoutFuture = callback.second;
+            try {
+                if (timeoutFuture == null || timeoutFuture.cancel(false)) {
+                    callback.first.onIqPacketReceived(account, packet);
+                }
+            } catch (final StateChangingError error) {
+                throw new StateChangingException(error.state);
+            }
+        }
+    }
+
+    private Pair<OnIqPacketReceived, ScheduledFuture> getIqPacketReceivedCallback(final IqPacket stanza)
+            throws StateChangingException {
+        final boolean isRequest =
+                stanza.getType() == IqPacket.TYPE.GET || stanza.getType() == IqPacket.TYPE.SET;
+        if (isRequest) {
+            if (isBound) {
+                return new Pair<>(this.unregisteredIqListener, null);
+            } else {
+                throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
             }
         } else {
-            OnIqPacketReceived callback = null;
             synchronized (this.packetCallbacks) {
-                final Pair<IqPacket, Pair<OnIqPacketReceived, ScheduledFuture>> packetCallbackDuple =
-                        packetCallbacks.get(packet.getId());
-                if (packetCallbackDuple != null) {
-                    ScheduledFuture timeoutFuture = packetCallbackDuple.second.second;
-                    // Packets to the server should have responses from the server
-                    if (packetCallbackDuple.first.toServer(account)) {
-                        if (packet.fromServer(account)) {
-                            if (timeoutFuture == null || timeoutFuture.cancel(false)) {
-                                callback = packetCallbackDuple.second.first;
-                            }
-                            packetCallbacks.remove(packet.getId());
-                        } else {
-                            Log.e(
-                                    Config.LOGTAG,
-                                    account.getJid().asBareJid().toString()
-                                            + ": ignoring spoofed iq packet");
-                        }
+                final var pair = packetCallbacks.get(stanza.getId());
+                if (pair == null) {
+                    return null;
+                }
+                if (pair.first.toServer(account)) {
+                    if (stanza.fromServer(account)) {
+                        packetCallbacks.remove(stanza.getId());
+                        return pair.second;
                     } else {
-                        if (packet.getFrom() != null
-                                && packet.getFrom().equals(packetCallbackDuple.first.getTo())) {
-                            if (timeoutFuture == null || timeoutFuture.cancel(false)) {
-                                callback = packetCallbackDuple.second.first;
-                            }
-                            packetCallbacks.remove(packet.getId());
-                        } else {
-                            Log.e(
-                                    Config.LOGTAG,
-                                    account.getJid().asBareJid().toString()
-                                            + ": ignoring spoofed iq packet");
-                        }
+                        Log.e(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid().toString()
+                                        + ": ignoring spoofed iq packet");
+                    }
+                } else {
+                    if (stanza.getFrom() != null && stanza.getFrom().equals(pair.first.getTo())) {
+                        packetCallbacks.remove(stanza.getId());
+                        return pair.second;
+                    } else {
+                        Log.e(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid().toString()
+                                        + ": ignoring spoofed iq packet");
                     }
-                } else if (packet.getType() == IqPacket.TYPE.GET
-                        || packet.getType() == IqPacket.TYPE.SET) {
-                    callback = this.unregisteredIqListener;
-                }
-            }
-            if (callback != null) {
-                try {
-                    callback.onIqPacketReceived(account, packet);
-                } catch (StateChangingError error) {
-                    throw new StateChangingException(error.state);
                 }
             }
         }
+        return null;
     }
 
     private void processMessage(final Tag currentTag) throws IOException {
@@ -1293,11 +1389,18 @@ public class XmppConnection implements Runnable {
                             + "'");
             return;
         }
+        if (Thread.currentThread().isInterrupted()) {
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + "Not processing message. Thread was interrupted");
+            return;
+        }
         this.messageListener.onMessagePacketReceived(account, packet);
     }
 
     private void processPresence(final Tag currentTag) throws IOException {
-        PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
+        final PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
         if (!packet.valid()) {
             Log.e(
                     Config.LOGTAG,
@@ -1308,6 +1411,13 @@ public class XmppConnection implements Runnable {
                             + "'");
             return;
         }
+        if (Thread.currentThread().isInterrupted()) {
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + "Not processing presence. Thread was interrupted");
+            return;
+        }
         this.presenceListener.onPresencePacketReceived(account, packet);
     }
 
@@ -1449,6 +1559,8 @@ public class XmppConnection implements Runnable {
                 && isSecure) {
             authenticate(SaslMechanism.Version.SASL);
         } else if (this.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT)
+                && isSecure
+                && LoginInfo.isSuccess(loginInfo)
                 && streamId != null
                 && !inSmacksSession) {
             if (Config.EXTENDED_SM_LOGGING) {
@@ -1463,7 +1575,9 @@ public class XmppConnection implements Runnable {
             this.mWaitingForSmCatchup.set(true);
             this.tagWriter.writeStanzaAsync(resume);
         } else if (needsBinding) {
-            if (this.streamFeatures.hasChild("bind", Namespace.BIND) && isSecure) {
+            if (this.streamFeatures.hasChild("bind", Namespace.BIND)
+                    && isSecure
+                    && LoginInfo.isSuccess(loginInfo)) {
                 sendBindRequest();
             } else {
                 Log.d(
@@ -1474,7 +1588,6 @@ public class XmppConnection implements Runnable {
                 throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
             }
         } else {
-
             Log.d(
                     Config.LOGTAG,
                     account.getJid().asBareJid()
@@ -1510,10 +1623,12 @@ public class XmppConnection implements Runnable {
                 this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
         final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
         final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
-        final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
+        final SaslMechanism saslMechanism =
+                factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
         this.validate(saslMechanism, mechanisms);
         final boolean quickStartAvailable;
-        final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
+        final String firstMessage =
+                saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
         final boolean usingFast = SaslMechanism.hashedToken(saslMechanism);
         final Element authenticate;
         if (version == SaslMechanism.Version.SASL) {
@@ -1522,7 +1637,7 @@ public class XmppConnection implements Runnable {
                 authenticate.setContent(firstMessage);
             }
             quickStartAvailable = false;
-            this.loginInfo = new LoginInfo(saslMechanism,version,Collections.emptyList());
+            this.loginInfo = new LoginInfo(saslMechanism, version, Collections.emptyList());
         } else if (version == SaslMechanism.Version.SASL_2) {
             final Element inline = authElement.findChild("inline", Namespace.SASL_2);
             final boolean sm = inline != null && inline.hasChild("sm", Namespace.STREAM_MANAGEMENT);
@@ -1530,7 +1645,8 @@ public class XmppConnection implements Runnable {
             if (usingFast) {
                 hashTokenRequest = null;
             } else {
-                final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
+                final Element fast =
+                        inline == null ? null : inline.findChild("fast", Namespace.FAST);
                 final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
                 hashTokenRequest =
                         HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
@@ -1551,9 +1667,11 @@ public class XmppConnection implements Runnable {
                     return;
                 }
             }
-            this.loginInfo = new LoginInfo(saslMechanism,version,bindFeatures);
+            this.loginInfo = new LoginInfo(saslMechanism, version, bindFeatures);
             this.hashTokenRequest = hashTokenRequest;
-            authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
+            authenticate =
+                    generateAuthenticationRequest(
+                            firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
         } else {
             throw new AssertionError("Missing implementation for " + version);
         }
@@ -1581,7 +1699,9 @@ public class XmppConnection implements Runnable {
         return inline != null && inline.hasChild("fast", Namespace.FAST);
     }
 
-    private void validate(final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms) throws StateChangingException {
+    private void validate(
+            final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms)
+            throws StateChangingException {
         if (saslMechanism == null) {
             Log.d(
                     Config.LOGTAG,
@@ -1608,8 +1728,10 @@ public class XmppConnection implements Runnable {
         }
     }
 
-    private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
-        return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
+    private Element generateAuthenticationRequest(
+            final String firstMessage, final boolean usingFast) {
+        return generateAuthenticationRequest(
+                firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
     }
 
     private Element generateAuthenticationRequest(
@@ -1802,8 +1924,10 @@ public class XmppConnection implements Runnable {
         resetAttemptCount(true);
         resetStreamId();
         clearIqCallbacks();
-        this.stanzasSent = 0;
-        mStanzaQueue.clear();
+        synchronized (this.mStanzaQueue) {
+            this.stanzasSent = 0;
+            this.mStanzaQueue.clear();
+        }
         this.redirectionUrl = null;
         synchronized (this.disco) {
             disco.clear();
@@ -2261,10 +2385,18 @@ public class XmppConnection implements Runnable {
             final String seeOtherHost = streamError.findChildContent("see-other-host");
             final Resolver.Result currentResolverResult = this.currentResolverResult;
             if (Strings.isNullOrEmpty(seeOtherHost) || currentResolverResult == null) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": stream error " + streamError);
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid() + ": stream error " + streamError);
                 throw new StateChangingException(Account.State.STREAM_ERROR);
             }
-            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": see other host: "+seeOtherHost+" "+currentResolverResult);
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid()
+                            + ": see other host: "
+                            + seeOtherHost
+                            + " "
+                            + currentResolverResult);
             final Resolver.Result seeOtherResult = currentResolverResult.seeOtherHost(seeOtherHost);
             if (seeOtherResult != null) {
                 this.seeOtherHostResolverResult = seeOtherResult;
@@ -2282,8 +2414,7 @@ public class XmppConnection implements Runnable {
         synchronized (this.mStanzaQueue) {
             for (int i = 0; i < mStanzaQueue.size(); ++i) {
                 final AbstractAcknowledgeableStanza stanza = mStanzaQueue.valueAt(i);
-                if (stanza instanceof MessagePacket) {
-                    final MessagePacket packet = (MessagePacket) stanza;
+                if (stanza instanceof MessagePacket packet) {
                     final String id = packet.getId();
                     final Jid to = packet.getTo();
                     mXmppConnectionService.markMessage(
@@ -2298,7 +2429,8 @@ public class XmppConnection implements Runnable {
         final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
         final SaslMechanism quickStartMechanism;
         if (secureConnection) {
-            quickStartMechanism = SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
+            quickStartMechanism =
+                    SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
         } else {
             quickStartMechanism = null;
         }
@@ -2307,10 +2439,16 @@ public class XmppConnection implements Runnable {
                 && quickStartMechanism != null
                 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
             mXmppConnectionService.restoredFromDatabaseLatch.await();
-            this.loginInfo = new LoginInfo(quickStartMechanism, SaslMechanism.Version.SASL_2, Bind2.QUICKSTART_FEATURES);
+            this.loginInfo =
+                    new LoginInfo(
+                            quickStartMechanism,
+                            SaslMechanism.Version.SASL_2,
+                            Bind2.QUICKSTART_FEATURES);
             final boolean usingFast = quickStartMechanism instanceof HashedToken;
             final Element authenticate =
-                    generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
+                    generateAuthenticationRequest(
+                            quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)),
+                            usingFast);
             authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
             sendStartStream(true, false);
             synchronized (this.mStanzaQueue) {
@@ -2419,9 +2557,7 @@ public class XmppConnection implements Runnable {
                                 + " do not write stanza to unbound stream "
                                 + packet.toString());
             }
-            if (packet instanceof AbstractAcknowledgeableStanza) {
-                AbstractAcknowledgeableStanza stanza = (AbstractAcknowledgeableStanza) packet;
-
+            if (packet instanceof AbstractAcknowledgeableStanza stanza) {
                 if (this.mStanzaQueue.size() != 0) {
                     int currentHighestKey = this.mStanzaQueue.keyAt(this.mStanzaQueue.size() - 1);
                     if (currentHighestKey != stanzasSent) {
@@ -2431,7 +2567,13 @@ public class XmppConnection implements Runnable {
 
                 ++stanzasSent;
                 if (Config.EXTENDED_SM_LOGGING) {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid()+": counting outbound "+packet.getName()+" as #" + stanzasSent);
+                    Log.d(
+                            Config.LOGTAG,
+                            account.getJid().asBareJid()
+                                    + ": counting outbound "
+                                    + packet.getName()
+                                    + " as #"
+                                    + stanzasSent);
                 }
                 this.mStanzaQueue.append(stanzasSent, stanza);
                 if (stanza instanceof MessagePacket && stanza.getId() != null && inSmacksSession) {
@@ -2614,7 +2756,7 @@ public class XmppConnection implements Runnable {
     public int getTimeToNextAttempt(final boolean aggressive) {
         final int interval;
         if (aggressive) {
-            interval = Math.min((int) (3 * Math.pow(1.3,attempt)), 60);
+            interval = Math.min((int) (3 * Math.pow(1.3, attempt)), 60);
         } else {
             final int additionalTime =
                     account.getLastErrorStatus() == Account.State.POLICY_VIOLATION ? 3 : 0;
@@ -2724,6 +2866,7 @@ public class XmppConnection implements Runnable {
         public final SaslMechanism saslMechanism;
         public final SaslMechanism.Version saslVersion;
         public final List<String> inlineBindFeatures;
+        public final AtomicBoolean success = new AtomicBoolean(false);
 
         private LoginInfo(
                 final SaslMechanism saslMechanism,
@@ -2742,6 +2885,23 @@ public class XmppConnection implements Runnable {
         public static SaslMechanism mechanism(final LoginInfo loginInfo) {
             return loginInfo == null ? null : loginInfo.saslMechanism;
         }
+
+        public void success(final String challenge, final SSLSocket sslSocket)
+                throws SaslMechanism.AuthenticationException {
+            final var response = this.saslMechanism.getResponse(challenge, sslSocket);
+            if (!Strings.isNullOrEmpty(response)) {
+                throw new SaslMechanism.AuthenticationException(
+                        "processing success yielded another response");
+            }
+            if (this.success.compareAndSet(false, true)) {
+                return;
+            }
+            throw new SaslMechanism.AuthenticationException("Process 'success' twice");
+        }
+
+        public static boolean isSuccess(final LoginInfo loginInfo) {
+            return loginInfo != null && loginInfo.success.get();
+        }
     }
 
     private static class StreamId {
@@ -2815,11 +2975,6 @@ public class XmppConnection implements Runnable {
                     && pepPublishOptions();
         }
 
-        public boolean avatarConversion() {
-            return hasDiscoFeature(account.getJid().asBareJid(), Namespace.AVATAR_CONVERSION)
-                    && pepPublishOptions();
-        }
-
         public boolean blocking() {
             return hasDiscoFeature(account.getDomain(), Namespace.BLOCKING);
         }
@@ -2845,7 +3000,8 @@ public class XmppConnection implements Runnable {
         public boolean sm() {
             return streamId != null
                     || (connection.streamFeatures != null
-                            && connection.streamFeatures.hasChild("sm", Namespace.STREAM_MANAGEMENT));
+                            && connection.streamFeatures.hasChild(
+                                    "sm", Namespace.STREAM_MANAGEMENT));
         }
 
         public boolean csi() {
@@ -2966,7 +3122,8 @@ public class XmppConnection implements Runnable {
         }
 
         public boolean bookmarks2() {
-            return pepPublishOptions() && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
+            return pepPublishOptions()
+                    && hasDiscoFeature(account.getJid().asBareJid(), Namespace.BOOKMARKS2_COMPAT);
         }
 
         public boolean externalServiceDiscovery() {

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleFileTransferConnection.java 🔗

@@ -209,8 +209,12 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
                 this.transportSecurity =
                         new TransportSecurity(
                                 xmppAxolotlMessage.getInnerKey(), xmppAxolotlMessage.getIV());
-                jinglePacket.setSecurity(
-                        Iterables.getOnlyElement(contentMap.contents.keySet()), xmppAxolotlMessage);
+                final var contents = jinglePacket.getJingleContents();
+                final var rawContent =
+                        contents.get(Iterables.getOnlyElement(contentMap.contents.keySet()));
+                if (rawContent != null) {
+                    rawContent.setSecurity(xmppAxolotlMessage);
+                }
             }
             jinglePacket.setTo(id.with);
             xmppConnectionService.sendIqPacket(
@@ -327,8 +331,10 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
             return;
         }
         final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
+        final var contents = jinglePacket.getJingleContents();
+        final var rawContent = contents.get(Iterables.getOnlyElement(contentMap.contents.keySet()));
         final var security =
-                jinglePacket.getSecurity(Iterables.getOnlyElement(contentMap.contents.keySet()));
+                rawContent == null ? null : rawContent.getSecurity(jinglePacket.getFrom());
         if (security != null) {
             Log.d(Config.LOGTAG, "found security element!");
             keyTransportMessage =
@@ -349,7 +355,6 @@ public class JingleFileTransferConnection extends AbstractJingleConnection
 
         if (transition(State.SESSION_INITIALIZED, () -> setRemoteContentMap(contentMap))) {
             respondOk(jinglePacket);
-            Log.d(Config.LOGTAG, jinglePacket.toString());
             Log.d(
                     Config.LOGTAG,
                     "got file offer " + file + " jet=" + Objects.nonNull(keyTransportMessage));

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java 🔗

@@ -9,8 +9,11 @@ import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableSet;
 
 import eu.siacs.conversations.Config;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 
 import java.util.Locale;
@@ -102,6 +105,37 @@ public class Content extends Element {
         }
     }
 
+    public void setSecurity(final XmppAxolotlMessage xmppAxolotlMessage) {
+        final String contentName = this.getContentName();
+        final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
+        security.setAttribute("name", contentName);
+        security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding");
+        security.setAttribute("type", AxolotlService.PEP_PREFIX);
+        security.addChild(xmppAxolotlMessage.toElement());
+        this.addChild(security);
+    }
+
+    public XmppAxolotlMessage getSecurity(final Jid from) {
+        final String contentName = this.getContentName();
+        for (final Element child : getChildren()) {
+            if ("security".equals(child.getName())
+                    && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) {
+                final String name = child.getAttribute("name");
+                final String type = child.getAttribute("type");
+                final String cipher = child.getAttribute("cipher");
+                if (contentName.equals(name)
+                        && AxolotlService.PEP_PREFIX.equals(type)
+                        && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) {
+                    final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX);
+                    if (encrypted != null) {
+                        return XmppAxolotlMessage.fromElement(encrypted, from.asBareJid());
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
     public void setTransport(GenericTransportInfo transportInfo) {
         this.addChild(transportInfo);
     }

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java 🔗

@@ -1,7 +1,5 @@
 package eu.siacs.conversations.xmpp.jingle.stanzas;
 
-import android.util.Log;
-
 import androidx.annotation.NonNull;
 
 import com.google.common.base.CaseFormat;
@@ -9,9 +7,6 @@ import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.crypto.axolotl.AxolotlService;
-import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
@@ -121,39 +116,6 @@ public class JinglePacket extends IqPacket {
         jingle.addChild(child);
     }
 
-    public void setSecurity(final String name, final XmppAxolotlMessage xmppAxolotlMessage) {
-        final Element security = new Element("security", Namespace.JINGLE_ENCRYPTED_TRANSPORT);
-        security.setAttribute("name", name);
-        security.setAttribute("cipher", "urn:xmpp:ciphers:aes-128-gcm-nopadding");
-        security.setAttribute("type", AxolotlService.PEP_PREFIX);
-        security.addChild(xmppAxolotlMessage.toElement());
-        addJingleChild(security);
-    }
-
-    public XmppAxolotlMessage getSecurity(final String nameNeedle) {
-        final Element jingle = findChild("jingle", Namespace.JINGLE);
-        if (jingle == null) {
-            return null;
-        }
-        for (final Element child : jingle.getChildren()) {
-            if ("security".equals(child.getName())
-                    && Namespace.JINGLE_ENCRYPTED_TRANSPORT.equals(child.getNamespace())) {
-                final String name = child.getAttribute("name");
-                final String type = child.getAttribute("type");
-                final String cipher = child.getAttribute("cipher");
-                if (nameNeedle.equals(name)
-                        && AxolotlService.PEP_PREFIX.equals(type)
-                        && "urn:xmpp:ciphers:aes-128-gcm-nopadding".equals(cipher)) {
-                    final var encrypted = child.findChild("encrypted", AxolotlService.PEP_PREFIX);
-                    if (encrypted != null) {
-                        return XmppAxolotlMessage.fromElement(encrypted, getFrom().asBareJid());
-                    }
-                }
-            }
-        }
-        return null;
-    }
-
     public String getSessionId() {
         return findChild("jingle", Namespace.JINGLE).getAttribute("sid");
     }

src/main/res/menu/fragment_conversations_overview.xml 🔗

@@ -51,6 +51,11 @@
         android:orderInCategory="90"
         android:title="@string/action_account"
         app:showAsAction="never" />
+    <item android:id="@+id/action_privacy_policy"
+        android:visible="false"
+        android:orderInCategory="98"
+        android:title="@string/privacy_policy"
+        app:showAsAction="never"/>
     <item
         android:id="@+id/action_settings"
         android:orderInCategory="100"

src/main/res/menu/start_conversation.xml 🔗

@@ -1,18 +1,18 @@
 <?xml version="1.0" encoding="utf-8"?>
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-      xmlns:app="http://schemas.android.com/apk/res-auto">
+    xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <item
         android:id="@+id/action_search"
         android:icon="?attr/icon_search"
         android:title="@string/search"
         app:actionLayout="@layout/actionview_search"
-        app:showAsAction="collapseActionView|always"/>
+        app:showAsAction="collapseActionView|always" />
     <item
         android:id="@+id/action_scan_qr_code"
-        android:title="@string/scan_qr_code"
         android:icon="?attr/icon_scan_qr_code"
-        app:showAsAction="always"/>
+        android:title="@string/scan_qr_code"
+        app:showAsAction="always" />
 
     <item
         android:id="@+id/action_hide_offline"
@@ -20,21 +20,27 @@
         android:checked="false"
         android:orderInCategory="85"
         android:title="@string/hide_offline"
-        app:showAsAction="never"/>
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_accounts"
         android:orderInCategory="90"
         android:title="@string/action_accounts"
-        app:showAsAction="never"/>
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_account"
         android:orderInCategory="90"
         android:title="@string/action_account"
-        app:showAsAction="never"/>
+        app:showAsAction="never" />
+    <item
+        android:id="@+id/action_privacy_policy"
+        android:orderInCategory="98"
+        android:title="@string/privacy_policy"
+        android:visible="false"
+        app:showAsAction="never" />
     <item
         android:id="@+id/action_settings"
         android:orderInCategory="100"
         android:title="@string/action_settings"
-        app:showAsAction="never"/>
+        app:showAsAction="never" />
 
 </menu>

src/main/res/values-bg/strings.xml 🔗

@@ -505,7 +505,10 @@
     <string name="no_storage_permission">Дайте на %1$s разрешение за достъп до външната памет</string>
     <string name="no_camera_permission">Дайте на %1$s разрешение за достъп до камерата</string>
     <string name="sync_with_contacts">Синхронизиране с контактите</string>
-    <string name="sync_with_contacts_long">%1$s иска разрешение за достъп до адресната Ви книга, за да потърси съвпадения със списъка от контакти в XMPP.\nТова ще покаже пълните имена и аватари на контактите Ви.\n\n%1$s само ще прочете адресната книга и ще потърси съвпадения на това устройство – нищо няма да се качва на сървъра Ви.</string>
+    <string name="sync_with_contacts_long">%1$s иска разрешение за достъп до адресната Ви книга, за да потърси съвпадения със списъка от контакти в XMPP.
+\nТова ще покаже пълните имена и аватари на контактите Ви.
+\n
+\n%1$s само ще прочете адресната книга и ще потърси съвпадения на това устройство – нищо няма да се качва на сървъра Ви.</string>
     <string name="notify_on_all_messages">Известяване за всички съобщения</string>
     <string name="notify_only_when_highlighted">Известяване само при споменаване</string>
     <string name="notify_never">Известията са изключени</string>

src/main/res/values-cs/strings.xml 🔗

@@ -511,7 +511,10 @@
     <string name="no_storage_permission">Povolit %1$s přístup k externímu úložišti</string>
     <string name="no_camera_permission">Povolit %1$s přístup ke kameře</string>
     <string name="sync_with_contacts">Synchronizovat s kontakty</string>
-    <string name="sync_with_contacts_long">%1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty.\nU kontaktů se pak zobrazí celé jméno a avatar.\n\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server.</string>
+    <string name="sync_with_contacts_long">%1$s požaduje přístup k Vašim kontaktům za účelem spárování s Vašimi XMPP kontakty.
+\nU kontaktů se pak zobrazí celé jméno a avatar.
+\n
+\n%1$s bude kontakty pouze číst a párovat místně v zařízení, aniž by došlo k nahrání těchto dat na server.</string>
     <string name="notify_on_all_messages">Upozorňovat na všechny zprávy</string>
     <string name="notify_only_when_highlighted">Upozornit pouze, když mě někdo zmíní</string>
     <string name="notify_never">Upozornění vypnuta</string>

src/main/res/values-da-rDK/strings.xml 🔗

@@ -514,7 +514,10 @@
     <string name="no_storage_permission">Giv %1$s adgang til ekstern lagerplads</string>
     <string name="no_camera_permission">Giv %1$s adgang til kameraet</string>
     <string name="sync_with_contacts">Synkroniser med kontakter</string>
-    <string name="sync_with_contacts_long">%1$s ønsker tilladelse til at få adgang til din adressebog for at matche den med din XMPP kontaktliste.\nDette vil vise dine kontakters fulde navne og avatarer.\n\n%1$s læser kun din adressebog og matcher den lokalt uden at uploade noget til din server.</string>
+    <string name="sync_with_contacts_long">%1$s ønsker tilladelse til at få adgang til din adressebog for at matche den med din XMPP kontaktliste.
+\nDette vil vise dine kontakters fulde navne og avatarer.
+\n
+\n%1$s læser kun din adressebog og matcher den lokalt uden at uploade noget til din server.</string>
     <string name="notify_on_all_messages">Underret ved alle beskeder</string>
     <string name="notify_only_when_highlighted">Underret kun når nævnt</string>
     <string name="notify_never">Notifikationer deaktiveret</string>

src/main/res/values-de/strings.xml 🔗

@@ -320,8 +320,8 @@
     <string name="jabber_id_copied_to_clipboard">XMPP-Adresse in Zwischenablage kopiert</string>
     <string name="error_message_copied_to_clipboard">Fehlermeldung in Zwischenablage kopiert</string>
     <string name="web_address">Internetadresse</string>
-    <string name="scan_qr_code">Barcode scannen</string>
-    <string name="show_qr_code">Barcode anzeigen</string>
+    <string name="scan_qr_code">QR-Code scannen</string>
+    <string name="show_qr_code">QR-Code anzeigen</string>
     <string name="show_block_list">Sperrliste anzeigen</string>
     <string name="account_details">Kontodetails</string>
     <string name="confirm">Bestätigen</string>
@@ -515,8 +515,10 @@
     <string name="shared_text_with_x">Text mit %s geteilt</string>
     <string name="no_storage_permission"> %1$s den Zugriff auf den externen Speicher gewähren</string>
     <string name="no_camera_permission">%1$s den Zugriff auf die Kamera gewähren</string>
-    <string name="sync_with_contacts">Mit Kontakten synchronisieren</string>
-    <string name="sync_with_contacts_long">%1$s möchte die Erlaubnis, auf deine Kontakte zuzugreifen, um sie mit deiner XMPP-Kontaktliste abzugleichen.\nDadurch werden die vollständigen Namen und Profilbilder deiner Kontakte angezeigt.\n\n%1$s liest nur dein Adressbuch und gleicht es lokal ab, ohne dass etwas auf deinen Server hochgeladen wird.</string>
+    <string name="sync_with_contacts">Kontaktlistenintegration</string>
+    <string name="sync_with_contacts_long">%1$s verarbeitet deine Kontaktliste lokal auf deinem Gerät, um dir die Namen und Profilbilder von passenden Kontakten auf XMPP zu zeigen.
+\n
+\nKeine Daten der Kontaktliste verlassen jemals dein Gerät!</string>
     <string name="notify_on_all_messages">Bei allen Nachrichten benachrichtigen</string>
     <string name="notify_only_when_highlighted">Nur benachrichtigen, wenn ich erwähnt werde</string>
     <string name="notify_never">Benachrichtigungen deaktiviert</string>
@@ -594,7 +596,7 @@
     <string name="pref_delete_omemo_identities_summary">Erzeuge neue OMEMO-Schlüssel. Alle deine Kontakte müssen sie erneut verifizieren. Verwende dies nur als letztes Mittel.</string>
     <string name="delete_selected_keys">Ausgewählte Schlüssel löschen</string>
     <string name="error_publish_avatar_offline">Du musst verbunden sein, um deinen Profilbild zu veröffentlichen.</string>
-    <string name="show_error_message">Zeige Fehlermeldung</string>
+    <string name="show_error_message">Fehlermeldung anzeigen</string>
     <string name="error_message">Fehlermeldung</string>
     <string name="data_saver_enabled">Datensparmodus aktiv</string>
     <string name="data_saver_enabled_explained">Dein Betriebssystem verhindert, dass %1$s im Hintergrund auf das Internet zugreift. Um Benachrichtigungen erhalten zu können, solltest du %1$s den Zugang erlauben, wenn der Datensparmodus aktiv ist.\n%1$s wird dennoch versuchen, so viele Daten wie möglich einzusparen.</string>
@@ -614,7 +616,7 @@
     <string name="pref_blind_trust_before_verification_summary">Neuen Geräten von nicht verifizierten Kontakten vertrauen, aber bei verifizierten Kontakten eine manuelle Bestätigung der neuen Geräte verlangen.</string>
     <string name="blindly_trusted_omemo_keys">Blind vertraute OMEMO-Schlüssel bedeutet, dass es sich um eine andere Person handeln könnte oder dass jemand sie abgehört haben könnte.</string>
     <string name="not_trusted">Nicht vertraut</string>
-    <string name="invalid_barcode">Ungültiger Barcode</string>
+    <string name="invalid_barcode">Ungültiger QR-Code</string>
     <string name="pref_clean_cache_summary">Cache-Ordner löschen (von der Kamera-App genutzt)</string>
     <string name="pref_clean_cache">Lösche Cache</string>
     <string name="pref_clean_private_storage">Lösche privaten Speicher</string>
@@ -1014,10 +1016,14 @@
     <string name="this_account_is_logged_out">Du hast dich von diesem Konto abgemeldet</string>
     <string name="log_in">Anmelden</string>
     <string name="hide_notification">Benachrichtigung ausblenden</string>
-    <string name="contact_uses_unverified_keys">Dein Kontakt verwendet nicht verifizierte Geräte. Scanne deren 2D-Barcode, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern.</string>
+    <string name="contact_uses_unverified_keys">Dein Kontakt verwendet nicht verifizierte Geräte. Scanne deren QR-Code, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern.</string>
     <string name="log_out">Abmelden</string>
     <string name="account_state_logged_out">Abgemeldet</string>
-    <string name="unverified_devices">Du verwendest nicht verifizierte Geräte. Scanne den 2D-Barcode auf deinen anderen Geräten, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern.</string>
+    <string name="unverified_devices">Du verwendest nicht verifizierte Geräte. Scanne den QR-Code auf deinen anderen Geräten, um eine Verifizierung durchzuführen und aktive MITM-Angriffe zu verhindern.</string>
     <string name="report_spam">Spam melden</string>
-    <string name="report_spam_and_block">Spam melden und Spammer blockieren</string>
+    <string name="report_spam_and_block">Spam melden und Spammer sperren</string>
+    <string name="welcome_header_quicksy">Willkommen bei Quicksy!</string>
+    <string name="privacy_policy">Datenschutzbestimmungen</string>
+    <string name="quicksy_wants_your_consent">Quicksy bittet dich um deine Zustimmung zur Verwendung deiner Daten</string>
+    <string name="contact_list_integration_not_available">Kontaktlistenintegration ist nicht verfügbar</string>
 </resources>

src/main/res/values-el/strings.xml 🔗

@@ -31,10 +31,7 @@
     <string name="minutes_ago">πριν από %d λεπτά</string>
     <plurals name="x_unread_conversations">
         <item quantity="one">%d μη αναγνωσμένη συζήτηση</item>
-
-    
         <item quantity="other">%d μη αναγνωσμένες συζητήσεις</item>
-
     </plurals>
     <string name="sending">αποστολή...</string>
     <string name="message_decrypting">Αποκρυπτογράφηση μηνύματος. Παρακαλώ περιμένετε...</string>
@@ -509,7 +506,10 @@
     <string name="no_storage_permission">Απόδοση δικαιώματος στο %1$s για πρόσβαση στον εξωτερικό αποθηκευτικό χώρο</string>
     <string name="no_camera_permission">Απόδοση δικαιώματος στο %1$s για πρόσβαση στην φωτογραφική μηχανή</string>
     <string name="sync_with_contacts">Συγχρονισμός με επαφές</string>
-    <string name="sync_with_contacts_long">Το %1$s ζητάει το δικαίωμα να έχει πρόσβαση στο βιβλίο διευθύνσεων για να το ταιριάξει με την λίστα επαφών XMPP σας.\nΑυτή η ενέργεια θα εμφανίσει τα πλήρη ονόματα και τις εικόνες προφίλ των επαφών σας.\n\nΤο %1$s θα διαβάσει μόνο το βιβλίο διευθύνσεών σας και θα το ταιριάξει τοπικά χωρίς να μεταφορτώσει κανένα στοιχείο στον διακομιστή σας. </string>
+    <string name="sync_with_contacts_long">Το %1$s ζητάει το δικαίωμα να έχει πρόσβαση στο βιβλίο διευθύνσεων για να το ταιριάξει με την λίστα επαφών XMPP σας.
+\nΑυτή η ενέργεια θα εμφανίσει τα πλήρη ονόματα και τις εικόνες προφίλ των επαφών σας.
+\n
+\nΤο %1$s θα διαβάσει μόνο το βιβλίο διευθύνσεών σας και θα το ταιριάξει τοπικά χωρίς να μεταφορτώσει κανένα στοιχείο στον διακομιστή σας.</string>
     <string name="notify_on_all_messages">Ειδοποίηση για όλα τα μηνύματα</string>
     <string name="notify_only_when_highlighted">Ειδοποίηση μόνο όταν αναφέρεται το όνομά μου</string>
     <string name="notify_never">Οι ειδοποιήσεις απενεργοποιήθηκαν</string>
@@ -963,4 +963,4 @@
     <string name="plain_text_document">Έγγραφο απλού κειμένου</string>
     <string name="account_registrations_are_not_supported">Δεν υποστηρίζονται εγγραφές λογαριασμών</string>
     <string name="no_xmpp_adddress_found">Δεν βρέθηκε διεύθυνση XMPP</string>
-    </resources>
+</resources>

src/main/res/values-es/strings.xml 🔗

@@ -321,7 +321,7 @@
     <string name="jabber_id_copied_to_clipboard">Dirección XMPP copiada al portapapeles</string>
     <string name="error_message_copied_to_clipboard">Mensaje de error copiado al portapapeles </string>
     <string name="web_address">dirección web</string>
-    <string name="scan_qr_code">Escanear código QR</string>
+    <string name="scan_qr_code">Escanear el código QR</string>
     <string name="show_qr_code">Mostrar código QR</string>
     <string name="show_block_list">Mostrar contactos bloqueados</string>
     <string name="account_details">Detalles de la cuenta</string>
@@ -518,8 +518,10 @@
     <string name="shared_text_with_x">Texto compartido con %s</string>
     <string name="no_storage_permission">Permitir a %1$s acceder al almacenamiento externo</string>
     <string name="no_camera_permission">Permitir a %1$s acceder a la cámara</string>
-    <string name="sync_with_contacts">Sincronizar contactos</string>
-    <string name="sync_with_contacts_long">%1$s quiere permiso para acceder a tu agenda de contactos y cruzarla con tu lista de contactos de XMPP.\nEsto permitirá mostrar el nombre completo y los avatares de tus contactos.\n\n%1$s solo leerá tu agenda de contactos y la cruzará localmente sin subir nada a tu servidor.</string>
+    <string name="sync_with_contacts">Integración de la lista de contactos</string>
+    <string name="sync_with_contacts_long">%1$s procesa tu lista de contactos localmente, en tu dispositivo, para mostrarte los nombres y fotos de perfil de los contactos coincidentes en XMPP.
+\n
+\n¡Ningún dato de la lista de contactos sale de tu dispositivo!</string>
     <string name="notify_on_all_messages">Notificar para todos los mensajes</string>
     <string name="notify_only_when_highlighted">Notificar solo cuando eres mencionado</string>
     <string name="notify_never">Notificaciones deshabilitadas</string>
@@ -1028,10 +1030,14 @@
     <string name="this_account_is_logged_out">Has salido de esta cuenta</string>
     <string name="log_in">Iniciar sesión</string>
     <string name="hide_notification">Ocultar la notificación</string>
-    <string name="contact_uses_unverified_keys">Su contacto utiliza dispositivos no verificados. Escanea su código de barras en 2D para realizar la verificación e impedir ataques MITM activos.</string>
+    <string name="contact_uses_unverified_keys">Tu contacto utiliza dispositivos no verificados. Escanea su código QR para realizar la verificación e impedir ataques MITM activos.</string>
     <string name="log_out">Desconectarse</string>
     <string name="account_state_logged_out">Desconectado</string>
-    <string name="unverified_devices">Está utilizando dispositivos no verificados. Escanea el código de barras 2D en tus otros dispositivos para realizar la verificación e impedir los ataques MITM activos.</string>
+    <string name="unverified_devices">Está utilizando dispositivos no verificados. Escanea el código QR en tus otros dispositivos para realizar la verificación e impedir ataques MITM activos.</string>
     <string name="report_spam_and_block">Informar de spam y bloquear al spammer</string>
     <string name="report_spam">Informar sobre spam</string>
+    <string name="welcome_header_quicksy">¡Bienvenido a Quicksy!</string>
+    <string name="quicksy_wants_your_consent">Quicksy pide tu consentimiento para utilizar tus datos</string>
+    <string name="privacy_policy">Política de privacidad</string>
+    <string name="contact_list_integration_not_available">La lista de contactos no está disponible</string>
 </resources>

src/main/res/values-fi/strings.xml 🔗

@@ -493,7 +493,10 @@
     <string name="no_storage_permission">Salli %1$s:n käyttää ulkoista tallennustilaa</string>
     <string name="no_camera_permission">Salli %1$s:n käyttää kameraa </string>
     <string name="sync_with_contacts">Synkronoi yhteystietojen kanssa</string>
-    <string name="sync_with_contacts_long">%1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa.\nTämä näyttää yhteystietojesi koko nimen ja kuvan.\n\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle.</string>
+    <string name="sync_with_contacts_long">%1$s haluaa pääsyn osoitekirjaasi yhdistääkseen sen XMPP-yhteystietojesi kanssa.
+\nTämä näyttää yhteystietojesi koko nimen ja kuvan.
+\n
+\n%1$s pelkästään lukee osoitekirjaasi ja vertailee niitä paikallisesti, lähettämättä mitään palvelimelle.</string>
     <string name="notify_on_all_messages">Ilmoita kaikista uusista viesteistä</string>
     <string name="notify_only_when_highlighted">Ilmoita vain kun minut mainitaan</string>
     <string name="notify_never">Ilmoitukset pois käytöstä</string>

src/main/res/values-gl/strings.xml 🔗

@@ -39,7 +39,7 @@
     <string name="nick_in_use">O alcume xa está en uso</string>
     <string name="invalid_muc_nick">Alcume non válido</string>
     <string name="admin">Admin</string>
-    <string name="owner">Dono</string>
+    <string name="owner">Dona</string>
     <string name="moderator">Moderador</string>
     <string name="participant">Participante</string>
     <string name="visitor">Visitante</string>
@@ -300,7 +300,7 @@
     <string name="pref_autojoin_summary">Poñer marca de \"autojoin\" ao entrar ou deixar unha MUC e reaccionar ás modificacións feitas desde outros clientes.</string>
     <string name="toast_message_omemo_fingerprint">Copiouse a impresión dixital OMEMO ao portapapeis</string>
     <string name="conference_banned">Non podes acceder a esta conversa en grupo</string>
-    <string name="conference_members_only">Esta conversa en grupo é so para membros</string>
+    <string name="conference_members_only">Este chat en grupo é so para membros</string>
     <string name="conference_resource_constraint">Restrición do recurso</string>
     <string name="conference_kicked">Xa foi expulsado de esta conversa en grupo</string>
     <string name="conference_shutdown">A conversa en grupo foi apagada</string>
@@ -322,8 +322,8 @@
     <string name="jabber_id_copied_to_clipboard">Copiouse o enderezo XMPP ao portapapeis</string>
     <string name="error_message_copied_to_clipboard">Mensaxe do fallo copiado ao portapapeis</string>
     <string name="web_address">Dirección Web</string>
-    <string name="scan_qr_code">Escanear código de barras 2D</string>
-    <string name="show_qr_code">Mostar código de barras 2D</string>
+    <string name="scan_qr_code">Escanear código QR</string>
+    <string name="show_qr_code">Mostar código QR</string>
     <string name="show_block_list">Mostrar lista de bloqueo</string>
     <string name="account_details">Detalles da conta</string>
     <string name="confirm">Confirmar</string>
@@ -518,7 +518,10 @@
     <string name="no_storage_permission">Permitir que %1$s acceda ao almacenaxe externo</string>
     <string name="no_camera_permission">Permitir que %1$s acceda á cámara</string>
     <string name="sync_with_contacts">Sincronice con todos os contactos</string>
-    <string name="sync_with_contacts_long">%1$s quere ter permiso para acceder á túa libreta de enderezos para comparala coa lista de contactos XMPP.\nDeste xeito poderá mostrar o nome completo e avatares dos teus contactos.\n\n%1$s só utilizará de xeito local a túa lista de contactos, sen subila a ningún servidor.</string>
+    <string name="sync_with_contacts_long">%1$s quere ter permiso para acceder á túa libreta de enderezos para comparala coa lista de contactos XMPP.
+\nDeste xeito poderá mostrar o nome completo e avatares dos teus contactos.
+\n
+\n%1$s só utilizará de xeito local a túa lista de contactos, sen subila a ningún servidor.</string>
     <string name="notify_on_all_messages">Notificar todas as mensaxes</string>
     <string name="notify_only_when_highlighted">Notificar só cando é mencionada</string>
     <string name="notify_never">Notificacións desactivadas</string>
@@ -616,7 +619,7 @@
     <string name="pref_blind_trust_before_verification_summary">Confiar en dispositivos novos de contactos non verificados, pero solicitar confirmación manual de novos dispositivos para contactos verificados.</string>
     <string name="blindly_trusted_omemo_keys">Chaves OMEMO de confianza cega, significa que podería ser calquera outra persoa ou algunha impostora.</string>
     <string name="not_trusted">Non confiables</string>
-    <string name="invalid_barcode">Código de barras 2D non válido</string>
+    <string name="invalid_barcode">Código QR non válido</string>
     <string name="pref_clean_cache_summary">Baleirar o cartafol da caché (utilizado pola cámara)</string>
     <string name="pref_clean_cache">Baleirar caché</string>
     <string name="pref_clean_private_storage">Baleirar almacenaxe privada</string>
@@ -748,7 +751,7 @@
     <string name="pref_start_search_summary">Na pantalla \'Iniciar Conversa\' abrir teclado e pór o cursor no campo de busca</string>
     <string name="group_chat_avatar">Avatar da conversa de grupo</string>
     <string name="host_does_not_support_group_chat_avatars">O servidor non soporta o avatar na conversa de grupo</string>
-    <string name="only_the_owner_can_change_group_chat_avatar">Só o dono pode cambiar o avatar da conversa de grupo</string>
+    <string name="only_the_owner_can_change_group_chat_avatar">Só a propietaria pode cambiar o avatar da conversa</string>
     <string name="contact_name">Nome do contacto</string>
     <string name="nickname">Alcume</string>
     <string name="group_chat_name">Nome</string>
@@ -1017,10 +1020,14 @@
     <string name="this_account_is_logged_out">Pechaches a sesión desta conta</string>
     <string name="log_in">Acceder</string>
     <string name="hide_notification">Agochar notificación</string>
-    <string name="contact_uses_unverified_keys">O teu contacto usa dispositivos non verificados. Escanea o seu código de barras 2D para verficalo e impedir ataques MITM.</string>
+    <string name="contact_uses_unverified_keys">O teu contacto usa dispositivos non verificados. Escanea o seu código QR para verficalo e impedir ataques MITM.</string>
     <string name="log_out">Saír da sesión</string>
     <string name="account_state_logged_out">Sesión pechada</string>
-    <string name="unverified_devices">Estás a usar dispositivos non verificados. Escanea os códigos de barras 2D nos teus outros dispositivos para verificalos e impedir ataques MITM.</string>
+    <string name="unverified_devices">Estás a usar dispositivos non verificados. Escanea os códigos QR nos teus outros dispositivos para verificalos e impedir ataques MITM.</string>
     <string name="report_spam_and_block">Informar de spam e bloquear conta</string>
     <string name="report_spam">Informar de spam</string>
+    <string name="welcome_header_quicksy">Benvida a Quicksy!</string>
+    <string name="quicksy_wants_your_consent">Quicksy solicita permiso para usar os teus datos</string>
+    <string name="privacy_policy">Política de privacidade</string>
+    <string name="contact_list_integration_not_available">Non está dispoñible a integración coa libreta de enderezos</string>
 </resources>

src/main/res/values-hu/strings.xml 🔗

@@ -31,10 +31,7 @@
     <string name="minutes_ago">%d perce</string>
     <plurals name="x_unread_conversations">
         <item quantity="one">%d olvasatlan beszélgetés</item>
-
-    
         <item quantity="other">%d olvasatlan beszélgetés</item>
-
     </plurals>
     <string name="sending">küldés…</string>
     <string name="message_decrypting">Üzenet visszafejtése. Kérem várjon…</string>
@@ -888,4 +885,4 @@
         <item quantity="one">%1$d résztvevő megtekintése</item>
         <item quantity="other">%1$d résztvevő megtekintése</item>
     </plurals>
-    </resources>
+</resources>

src/main/res/values-id/strings.xml 🔗

@@ -31,7 +31,6 @@
     <string name="minutes_ago">%d min lalu</string>
     <plurals name="x_unread_conversations">
         <item quantity="other">%d percakapan belum dibaca</item>
-
     </plurals>
     <string name="sending">mengirim...</string>
     <string name="message_decrypting">Mendekripsi pesan. Mohon tunggu…</string>
@@ -486,4 +485,4 @@
     <string name="xmpp_address">alamat XMPP</string>
     <string name="creating_channel">Buat channel publik...</string>
     <string name="rtp_state_declined_or_busy">Sibuk</string>
-    </resources>
+</resources>

src/main/res/values-it/strings.xml 🔗

@@ -321,8 +321,8 @@
     <string name="jabber_id_copied_to_clipboard">Indirizzo XMPP copiato negli appunti</string>
     <string name="error_message_copied_to_clipboard">Messaggio di errore copiato negli appunti</string>
     <string name="web_address">indirizzo web</string>
-    <string name="scan_qr_code">Scansiona codice a barre 2D</string>
-    <string name="show_qr_code">Mostra codice a barre 2D</string>
+    <string name="scan_qr_code">Scansiona codice QR</string>
+    <string name="show_qr_code">Mostra codice QR</string>
     <string name="show_block_list">Mostra la lista nera</string>
     <string name="account_details">Dettagli del profilo</string>
     <string name="confirm">Conferma</string>
@@ -519,7 +519,10 @@
     <string name="no_storage_permission">Dai a %1$s l\'accesso all\'archiviazione esterna</string>
     <string name="no_camera_permission">Dai a %1$s l\'accesso alla fotocamera</string>
     <string name="sync_with_contacts">Sincronizza con i contatti</string>
-    <string name="sync_with_contacts_long">%1$s vuole l\'autorizzazione ad accedere alla tua rubrica per confrontarla con la lista di contatti in XMPP.\nCiò mostrerà i nomi ed avatar dei contatti.\n\n%1$s leggerà solamente la rubrica e la confronterà localmente senza inviare nulla al tuo server.</string>
+    <string name="sync_with_contacts_long">%1$s vuole l\'autorizzazione ad accedere alla tua rubrica per confrontarla con la lista di contatti in XMPP.
+\nCiò mostrerà i nomi ed avatar dei contatti.
+\n
+\n%1$s leggerà solamente la rubrica e la confronterà localmente senza inviare nulla al tuo server.</string>
     <string name="notify_on_all_messages">Notifica per tutti i messaggi</string>
     <string name="notify_only_when_highlighted">Notifica solo quando menzionato</string>
     <string name="notify_never">Notifiche disattivate</string>
@@ -617,7 +620,7 @@
     <string name="pref_blind_trust_before_verification_summary">Fidati di nuovi dispositivi da contatti non verificati, ma chiedi una conferma manuale per nuovi dispositivi da contatti verificati.</string>
     <string name="blindly_trusted_omemo_keys">Chiavi OMEMO accettate ciecamente, perciò potrebbero essere di qualcun altro o qualcuno potrebbe essersi intromesso.</string>
     <string name="not_trusted">Non fidato</string>
-    <string name="invalid_barcode">Codice a barre 2D non valido</string>
+    <string name="invalid_barcode">Codice QR non valido</string>
     <string name="pref_clean_cache_summary">Svuota la cartella della cache (usata dall\'app fotocamera)</string>
     <string name="pref_clean_cache">Svuota cache</string>
     <string name="pref_clean_private_storage">Svuota archivio privato</string>
@@ -1028,10 +1031,12 @@
     <string name="this_account_is_logged_out">Ti sei disconnesso da questo profilo</string>
     <string name="log_in">Accedi</string>
     <string name="hide_notification">Nascondi notifica</string>
-    <string name="contact_uses_unverified_keys">Il tuo contatto usa dispositivi non verificati. Scansiona il suo codice a barre 2D per effettuare la verifica e impedire attacchi MITM attivi.</string>
+    <string name="contact_uses_unverified_keys">Il tuo contatto usa dispositivi non verificati. Scansiona il suo codice QR per effettuare la verifica e impedire attacchi MITM attivi.</string>
     <string name="log_out">Disconnetti</string>
     <string name="account_state_logged_out">Disconnesso</string>
-    <string name="unverified_devices">Stai usando dispositivi non verificati. Scansiona il codice a barre 2D nei tuoi altri dispositivi per effettuare la verifica e impedire attacchi MITM attivi.</string>
+    <string name="unverified_devices">Stai usando dispositivi non verificati. Scansiona il codice QR nei tuoi altri dispositivi per effettuare la verifica e impedire attacchi MITM attivi.</string>
     <string name="report_spam_and_block">Segnala spam e blocca l\'utente</string>
     <string name="report_spam">Segnala spam</string>
+    <string name="welcome_header_quicksy">Benvenuti su Quicksy!</string>
+    <string name="quicksy_wants_your_consent">Quicksy ti chiede il consenso per usare i tuoi dati</string>
 </resources>

src/main/res/values-iw/strings.xml 🔗

@@ -281,4 +281,4 @@
     <string name="presence_online">מקוון</string>
     <string name="message_copied_to_clipboard">ההודעה הועתקה</string>
     <string name="title_activity_show_location">הראה מיקום</string>
-    </resources>
+</resources>

src/main/res/values-ja/strings.xml 🔗

@@ -7,7 +7,7 @@
     <string name="action_end_conversation">会話を閉じる</string>
     <string name="action_contact_details">連絡先の詳細</string>
     <string name="action_muc_details">グループチャットの詳細</string>
-    <string name="channel_details">談話室の詳細</string>
+    <string name="channel_details">チャンネルの詳細</string>
     <string name="action_add_account">アカウントを追加</string>
     <string name="action_edit_contact">名前を編集</string>
     <string name="action_add_phone_book">アドレス帳に追加</string>
@@ -34,7 +34,7 @@
     </plurals>
     <string name="sending">送信中…</string>
     <string name="message_decrypting">メッセージを復号しています。しばらくお待ちください…</string>
-    <string name="pgp_message">OpenPGP 暗号化メッセージ</string>
+    <string name="pgp_message">OpenPGPで暗号化されたメッセージ</string>
     <string name="nick_in_use">ニックネームは既に使用されています</string>
     <string name="invalid_muc_nick">このニックネームは使えません</string>
     <string name="admin">管理者</string>
@@ -42,11 +42,11 @@
     <string name="moderator">調停者</string>
     <string name="participant">参加者</string>
     <string name="visitor">訪問者</string>
-    <string name="remove_contact_text">連絡先名簿から %s を削除しますか? この連絡先との会話は削除されません。</string>
-    <string name="block_contact_text">%s からあなたに送信されるメッセージをブロックしますか?</string>
+    <string name="remove_contact_text">連絡先リストから%sを削除しますか? この連絡先との会話は削除されません。</string>
+    <string name="block_contact_text">%sからあなたに送信されるメッセージをブロックしますか?</string>
     <string name="unblock_contact_text">%s のブロックを解除し、あなたにメッセージを送信できるようにしますか?</string>
-    <string name="block_domain_text">%s からの連絡をすべてブロックしますか?</string>
-    <string name="unblock_domain_text">%s からすべての連絡先のブロックを解除しますか?</string>
+    <string name="block_domain_text">%sの連絡先をすべてブロックしますか?</string>
+    <string name="unblock_domain_text">%sのすべての連絡先のブロックを解除しますか?</string>
     <string name="contact_blocked">連絡先をブロックしました</string>
     <string name="blocked">ブロックしました</string>
     <string name="remove_bookmark_text">%s のブックマークを削除しますか? このブックマークとの会話は削除されません。</string>
@@ -68,14 +68,14 @@
     <string name="save">保存</string>
     <string name="ok">OK</string>
     <string name="crash_report_title">%1$s がクラッシュしました</string>
-    <string name="crash_report_message">あなたの XMPP アカウントを使用してスタックトレースの送信をすることで、 %1$s の継続的な開発を支援します。</string>
+    <string name="crash_report_message">あなたのXMPPアカウントを使用してスタックトレースを送信すると、 %1$sの実施中の開発の支援となります。</string>
     <string name="send_now">今すぐ送信</string>
     <string name="send_never">今後は表示しない</string>
     <string name="problem_connecting_to_account">アカウントに接続できません</string>
     <string name="problem_connecting_to_accounts">複数のアカウントに接続できません</string>
     <string name="touch_to_fix">タップしてアカウントを管理</string>
     <string name="attach_file">ファイルを添付</string>
-    <string name="not_in_roster">連絡先が連絡先名簿にありません。名簿に追加しますか?</string>
+    <string name="not_in_roster">連絡先が連絡先リストにありません。リストに追加しますか?</string>
     <string name="add_contact">連絡先を追加</string>
     <string name="send_failed">配信に失敗しました</string>
     <string name="preparing_image">送信用画像の準備中</string>
@@ -105,8 +105,10 @@
     <string name="offering">依頼中…</string>
     <string name="waiting">待機中…</string>
     <string name="no_pgp_key">OpenPGP 鍵が見つかりません</string>
-    <string name="contact_has_no_pgp_key">連絡先が公開鍵を通知しないため、あなたのメッセージを暗号化することができません。\n\n<small>連絡先に OpenPGP をセットアップするように依頼してください。</small></string>
-    <string name="no_pgp_keys">OpenPGP 鍵が見つかりません</string>
+    <string name="contact_has_no_pgp_key">連絡先が公開鍵を通知していないため、あなたのメッセージを暗号化することができません。
+\n
+\n<small>連絡先にOpenPGPを設定するように依頼してください。</small></string>
+    <string name="no_pgp_keys">OpenPGPの鍵が見つかりません</string>
     <string name="contacts_have_no_pgp_keys">連絡先が公開鍵を通知しないため、あなたのメッセージを暗号化することができません。\n\n<small>連絡先に OpenPGP をセットアップするように依頼してください。</small></string>
     <string name="pref_general">全般</string>
     <string name="pref_accept_files">ファイルを受取</string>
@@ -122,10 +124,10 @@
     <string name="pref_notification_sound_summary">新着メッセージの通知音</string>
     <string name="pref_call_ringtone_summary">着信通話の呼出音</string>
     <string name="pref_notification_grace_period">猶予期間</string>
-    <string name="pref_notification_grace_period_summary">別のデバイスでの操作を検知した際に、通知を止める時間の長さ</string>
+    <string name="pref_notification_grace_period_summary">別のデバイスでの操作を検知した際に、通知を止める時間の長さ。</string>
     <string name="pref_advanced_options">詳細</string>
     <string name="pref_never_send_crash">クラッシュレポートを送信しない</string>
-    <string name="pref_never_send_crash_summary">スタックトレースを送信すると、 Conversations の開発を支援します</string>
+    <string name="pref_never_send_crash_summary">スタックトレースを送信すると、 開発の助けとなります</string>
     <string name="pref_confirm_messages">メッセージを確認</string>
     <string name="pref_confirm_messages_summary">あなたがメッセージを受信して読んだときに、連絡先に知らせる</string>
     <string name="pref_prevent_screenshots">スクリーンショットを防ぐ</string>
@@ -181,7 +183,7 @@
     <string name="unpublish_pgp_message">出席情報告知から OpenPGP 公開鍵を削除してもよろしいですか?\n連絡先はあなたに OpenPGP 暗号化メッセージを送信できなくなります。</string>
     <string name="openpgp_has_been_published">OpenPGP 公開鍵を公開しました。</string>
     <string name="mgmt_account_enable">アカウントを有効化</string>
-    <string name="mgmt_account_delete_confirm_text">アカウントを削除すると会話履歴がすべて消去されます</string>
+    <string name="mgmt_account_delete_confirm_text">アカウントを削除してよろしいですか?アカウントを削除すると会話履歴がすべて消去されます</string>
     <string name="attach_record_voice">音声を録音</string>
     <string name="account_settings_jabber_id">XMPP アドレス</string>
     <string name="block_jabber_id">XMPP アドレスをブロック</string>
@@ -291,10 +293,10 @@
     <string name="pref_expert_options_other">その他</string>
     <string name="pref_autojoin">ブックマーク同期</string>
     <string name="toast_message_omemo_fingerprint">OMEMO フィンガープリントをクリップボードにコピーしました</string>
-    <string name="conference_banned">このグループチャットから出禁にされています</string>
+    <string name="conference_banned">このグループチャットへの参加はブロックされています</string>
     <string name="conference_members_only">このグループチャットはメンバー制です</string>
     <string name="conference_resource_constraint">リソース制限</string>
-    <string name="conference_kicked">このグループチャットから蹴り出されています</string>
+    <string name="conference_kicked">このグループチャットから退出させられました</string>
     <string name="conference_shutdown">このグループチャットは閉鎖されました</string>
     <string name="conference_unknown_error">あなたはもうこのグループチャットに参加していません</string>
     <string name="conference_technical_problems">技術的理由の為、あなたはこのグループチャットを離れました</string>
@@ -508,7 +510,10 @@
     <string name="no_storage_permission">%1$s に外部ストレージへのアクセス権を付与してください</string>
     <string name="no_camera_permission">%1$s にカメラへのアクセス権を付与</string>
     <string name="sync_with_contacts">連絡先と同期</string>
-    <string name="sync_with_contacts_long">%1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。\nこれにより、連絡先のフルネームとアバターが表示されます。\n\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んで照合するだけです。</string>
+    <string name="sync_with_contacts_long">%1$s はあなたのアドレス帳にアクセスして、あなたのXMPP 連絡先名簿と照合する権限を求めています。
+\nこれにより、連絡先のフルネームとアバターが表示されます。
+\n
+\n%1$s は、あなたのサーバーに何かをアップロードすることなく、あなたのアドレス帳を読み込んで照合するだけです。</string>
     <string name="notify_on_all_messages">すべてのメッセージで通知</string>
     <string name="notify_only_when_highlighted">メンションされたときにのみ通知</string>
     <string name="notify_never">通知は無効</string>
@@ -894,24 +899,24 @@
     <string name="rtp_state_ending_call">通話終了</string>
     <string name="answer_call">応答</string>
     <string name="dismiss_call">拒否</string>
-    <string name="rtp_state_finding_device">デバイス発見</string>
-    <string name="rtp_state_ringing">鳴動</string>
+    <string name="rtp_state_finding_device">デバイスを探索中</string>
+    <string name="rtp_state_ringing">呼び出し中</string>
     <string name="rtp_state_declined_or_busy">取込中</string>
     <string name="rtp_state_connectivity_error">通話に接続できません</string>
     <string name="rtp_state_connectivity_lost_error">接続切断</string>
     <string name="rtp_state_retracted">撤回された通話</string>
     <string name="rtp_state_application_failure">アプリの失敗</string>
     <string name="rtp_state_security_error">検証に問題</string>
-    <string name="hang_up">電話を切る</string>
+    <string name="hang_up">通話を切る</string>
     <string name="ongoing_call">継続中の通話</string>
-    <string name="ongoing_video_call">継続中の映像通話</string>
-    <string name="reconnecting_call">通話再接続中</string>
-    <string name="reconnecting_video_call">ビデオ通話再接続中</string>
-    <string name="disable_tor_to_make_call">通話するのに Tor を無効化</string>
+    <string name="ongoing_video_call">継続中のビデオ通話</string>
+    <string name="reconnecting_call">通話に再接続中</string>
+    <string name="reconnecting_video_call">ビデオ通話に再接続中</string>
+    <string name="disable_tor_to_make_call">通話を行う際にはTorを無効にしてください</string>
     <string name="incoming_call">着信通話</string>
-    <string name="missed_call_timestamp">不在着信通話・%s</string>
+    <string name="missed_call_timestamp">不在着信・%s</string>
     <string name="outgoing_call">発信通話</string>
-    <string name="missed_call">不在着信通話</string>
+    <string name="missed_call">不在着信</string>
     <plurals name="n_missed_calls_from_x">
         <item quantity="other">%2$sから%1$d件の不在着信</item>
     </plurals>
@@ -930,7 +935,7 @@
     <string name="return_to_ongoing_call">継続中の通話に戻る</string>
     <string name="could_not_switch_camera">カメラを切り替えできません</string>
     <string name="add_to_favorites">最上に留める</string>
-    <string name="remove_from_favorites">最上から留めるのをやめる</string>
+    <string name="remove_from_favorites">最上部へのピン止めを外す</string>
     <string name="gpx_track">GPX 追跡</string>
     <string name="could_not_correct_message">メッセージを修正できません</string>
     <string name="search_all_conversations">すべての会話</string>
@@ -942,9 +947,9 @@
     <string name="not_encrypted">非暗号化</string>
     <string name="exit">終了</string>
     <string name="record_voice_mail">音声メールを録音</string>
-    <string name="play_audio">音声再生</string>
-    <string name="pause_audio">音声一時中断</string>
-    <string name="add_contact_or_create_or_join_group_chat">連絡先を追加、作成またはグループチャットに参加、または談話室を発見する</string>
+    <string name="play_audio">音声を再生</string>
+    <string name="pause_audio">音声を一時停止</string>
+    <string name="add_contact_or_create_or_join_group_chat">連絡先を追加、作成またはグループチャットに参加、またはチャンネルを発見する</string>
     <plurals name="view_users">
         <item quantity="other">%1$d人の参加者を表示</item>
     </plurals>
@@ -954,7 +959,7 @@
     <string name="failed_deliveries">配信に失敗</string>
     <string name="more_options">更なるオプション</string>
     <string name="no_application_found">アプリケーションが見つかりません</string>
-    <string name="invite_to_app">会話に招待</string>
+    <string name="invite_to_app">Conversationsに招待</string>
     <string name="unable_to_parse_invite">招待を解析できません</string>
     <string name="server_does_not_support_easy_onboarding_invites">サーバーは招待の作成をサポートしていません</string>
     <string name="no_active_accounts_support_this">この機能をサポートするアクティブなアカウントがありません</string>
@@ -966,19 +971,33 @@
     <string name="account_status_temporary_auth_failure">一時的な認証失敗</string>
     <string name="delete_avatar">アバターを削除</string>
     <string name="audio_video_disabled_tor">Tor使用中のため通話できません</string>
-    <string name="switch_to_video">ビデオ通話切替</string>
+    <string name="switch_to_video">ビデオ通話へ切替</string>
     <string name="this_account_is_logged_out">このアカウントをログアウトしました</string>
     <string name="action_directions">ルート</string>
-    <string name="reject_switch_to_video">ビデオ通話を却下する</string>
-    <string name="incoming_call_duration_timestamp">着信通話 (%s) · %s</string>
+    <string name="reject_switch_to_video">ビデオ通話を拒否</string>
+    <string name="incoming_call_duration_timestamp">着信中の通話 (%s) · %s</string>
     <string name="pref_up_push_account_title">XMPPアカウント</string>
     <string name="group_chats">グループ</string>
     <string name="outdated_backup_file_format">選択したファイルは、旧式のファイル形式ので復元できません</string>
     <string name="search_group_chats">グループを検索</string>
-    <string name="outgoing_call_duration_timestamp">発信通話 (%s) · %s</string>
+    <string name="outgoing_call_duration_timestamp">発信中の通話 (%s) · %s</string>
     <string name="account_state_logged_out">ログアウトしました</string>
     <string name="outgoing_call_timestamp">発信通話 · %s</string>
     <string name="audiobook">オーディオブック</string>
     <string name="channel_discover_opt_in_message">談話室の発見は&lt;a href=https://search.jabber.network&gt;search.jabber.network&lt;/a&gt;というサービスを利用します.&lt;br&gt;&lt;br&gt;利用するとIPアドレスと検索語はそのサービスに送信されます。詳細についてはそのサービスの&lt;a href=https://search.jabber.network/privacy&gt;個人情報保護方針&lt;/a&gt;を参照してください。</string>
     <string name="restore_warning_continued">自分で保存したバックアップしか復元しないでください!</string>
+    <string name="pref_up_push_server_summary">XMPP経由でPushメッセージを端末に転送するユーザー指定のPushサーバー。</string>
+    <string name="log_in">ログイン</string>
+    <string name="hide_notification">通知を表示しない</string>
+    <string name="delete_from_server">アカウントをサーバーから削除</string>
+    <string name="log_out">ログアウト</string>
+    <string name="no_account_deactivated">なし(無効)</string>
+    <string name="decline">拒否</string>
+    <string name="pref_up_push_server_title">Pushサーバー</string>
+    <string name="save_as_group_chat">グループチャットとして保存</string>
+    <string name="unified_push_distributor">UnifiedPushディストリビューター</string>
+    <string name="report_spam">スパムを報告</string>
+    <string name="pref_up_push_account_summary">Pushメッセージを受信する際に経由するアカウント。</string>
+    <string name="could_not_delete_account_from_server">サーバーからアカウントを削除できませんでした</string>
+    <string name="category_about">概要</string>
 </resources>

src/main/res/values-ko/strings.xml 🔗

@@ -396,4 +396,4 @@
     <string name="medium">중간</string>
     <string name="title_activity_show_location">위치 표시 </string>
     <string name="rtp_state_declined_or_busy">바쁨</string>
-    </resources>
+</resources>

src/main/res/values-nb-rNO/strings.xml 🔗

@@ -465,4 +465,4 @@
     <string name="create_dialog_group_chat_name">Gruppesludringsnavn</string>
     <string name="create_group_chat">Opprett gruppesludring</string>
     <string name="rtp_state_declined_or_busy">Opptatt</string>
-    </resources>
+</resources>

src/main/res/values-pl/strings.xml 🔗

@@ -524,7 +524,10 @@
     <string name="no_storage_permission">Pozwól %1$s na dostęp do zewnętrznego magazynu</string>
     <string name="no_camera_permission">Pozwól %1$s na dostępu do aparatu</string>
     <string name="sync_with_contacts">Synchronizuj z kontaktami</string>
-    <string name="sync_with_contacts_long">%1$s potrzebuje dostępu do twojej książki adresowej aby dopasować ją z twoją listą kontaktów XMPP.\nDzięki temu wyświetlone zostaną pełne nazwy i awatary kontaktów.\n\n%1$s użyje książki adresowej wyłącznie do lokalnego dopasowania bez wysyłania czegokolwiek na serwer.</string>
+    <string name="sync_with_contacts_long">%1$s potrzebuje dostępu do twojej książki adresowej aby dopasować ją z twoją listą kontaktów XMPP.
+\nDzięki temu wyświetlone zostaną pełne nazwy i awatary kontaktów.
+\n
+\n%1$s użyje książki adresowej wyłącznie do lokalnego dopasowania bez wysyłania czegokolwiek na serwer.</string>
     <string name="notify_on_all_messages">Powiadom o wszystkich wiadomościach</string>
     <string name="notify_only_when_highlighted">Powiadamiaj tylko w przypadku wzmianki o mnie</string>
     <string name="notify_never">Powiadomienia wyłączone</string>

src/main/res/values-pt-rBR/strings.xml 🔗

@@ -31,13 +31,8 @@
     <string name="minutes_ago">%d minutos atrás</string>
     <plurals name="x_unread_conversations">
         <item quantity="one">%d conversa não lida</item>
-
-    
         <item quantity="many">%d conversas não lidas</item>
-
-    
         <item quantity="other">%d conversas não lidas</item>
-
     </plurals>
     <string name="sending">enviando...</string>
     <string name="message_decrypting">Descriptografando a mensagem. Por favor, aguarde...</string>
@@ -520,7 +515,9 @@
     <string name="no_storage_permission">Permita o acesso do %1$s ao armazenamento externo</string>
     <string name="no_camera_permission">Permita o acesso do %1$s à câmera</string>
     <string name="sync_with_contacts">Sincronizar com os contatos</string>
-    <string name="sync_with_contacts_long">%1$s gostaria de obter a permissão para acessar seu livro de endereços e fazer a correspondência entre ele e a sua lista de contatos do XMPP. Isso permitirá exibir os nomes completos e avatares dos seus contatos.\n\n%1$s fará a leitura e a correspondência do seu livro de endereços localmente, sem enviar os seus contatos para o servidor em uso.</string>
+    <string name="sync_with_contacts_long">%1$s gostaria de obter a permissão para acessar seu livro de endereços e fazer a correspondência entre ele e a sua lista de contatos do XMPP. Isso permitirá exibir os nomes completos e avatares dos seus contatos.
+\n
+\n%1$s fará a leitura e a correspondência do seu livro de endereços localmente, sem enviar os seus contatos para o servidor em uso.</string>
     <string name="notify_on_all_messages">Notificar em todas as mensagens</string>
     <string name="notify_only_when_highlighted">Notificar somente quando for mencionado</string>
     <string name="notify_never">Notificações desabilitadas</string>
@@ -1005,5 +1002,4 @@
     <string name="audio_video_disabled_tor">As chamadas estão desabilitadas ao usar Tor</string>
     <string name="switch_to_video">Mudar para vídeo</string>
     <string name="reject_switch_to_video">Recusar requisição de mudança para vídeo</string>
-
-</resources>
+</resources>

src/main/res/values-pt/strings.xml 🔗

@@ -410,4 +410,4 @@
     <string name="medium">Médio</string>
     <string name="title_activity_show_location">Exibir localização</string>
     <string name="rtp_state_declined_or_busy">Ocupado</string>
-    </resources>
+</resources>

src/main/res/values-ro-rRO/strings.xml 🔗

@@ -323,8 +323,8 @@
     <string name="jabber_id_copied_to_clipboard">Adresă XMPP copiată în memorie</string>
     <string name="error_message_copied_to_clipboard">Mesaj de eroare copiat în memorie</string>
     <string name="web_address">adresă web</string>
-    <string name="scan_qr_code">Scanează cod de bare 2D</string>
-    <string name="show_qr_code">Arată cod de bare 2D</string>
+    <string name="scan_qr_code">Scanează cod QR</string>
+    <string name="show_qr_code">Arată cod QR</string>
     <string name="show_block_list">Listă contacte blocate</string>
     <string name="account_details">Detalii cont</string>
     <string name="confirm">Confirmă</string>
@@ -625,7 +625,7 @@
     <string name="pref_blind_trust_before_verification_summary">Ai încredere în toate dispozitivele noi ale contactelor care nu au fost verificate anterior, dar cere confirmarea manuală pentru dispozitivele noi ale contactelor verificate.</string>
     <string name="blindly_trusted_omemo_keys">Încredere oarbă în aceste chei OMEMO, aceasta înseamnă că ar putea fi altcineva sau cineva și-a strecurat propriile chei.</string>
     <string name="not_trusted">De neîncredere</string>
-    <string name="invalid_barcode">Cod de bare 2D invalid</string>
+    <string name="invalid_barcode">Cod QR invalid</string>
     <string name="pref_clean_cache_summary">Curățare dosar temporar (folosit de aplicația cameră foto)</string>
     <string name="pref_clean_cache">Curăța memoria temporară</string>
     <string name="pref_clean_private_storage">Curăță stocarea privată</string>
@@ -1036,10 +1036,14 @@
     <string name="this_account_is_logged_out">V-ați deconectat de la acest cont</string>
     <string name="log_in">Conectați-vă</string>
     <string name="hide_notification">Ascunde notificare</string>
-    <string name="contact_uses_unverified_keys">Persoana de contact utilizează dispozitive neverificate. Scanați codul de bare 2D al acestora pentru a efectua verificarea și a împiedica atacurile MITM active.</string>
+    <string name="contact_uses_unverified_keys">Persoana de contact utilizează dispozitive neverificate. Scanați codul QR al acestora pentru a efectua verificarea și a împiedica atacurile MITM active.</string>
     <string name="log_out">Deconectare</string>
     <string name="account_state_logged_out">Deconectat</string>
-    <string name="unverified_devices">Folosiți dispozitive neverificate. Scanați codul de bare 2D pe celelalte dispozitive pentru a efectua verificarea și a împiedica atacurile MITM active.</string>
+    <string name="unverified_devices">Folosiți dispozitive neverificate. Scanați codul QR pe celelalte dispozitive pentru a efectua verificarea și a împiedica atacurile MITM active.</string>
     <string name="report_spam_and_block">Raportează spam și blochează spamerul</string>
     <string name="report_spam">Raportează spam</string>
+    <string name="welcome_header_quicksy">Bine ați venit la Quicksy!</string>
+    <string name="quicksy_wants_your_consent">Quicksy vă solicită consimțământul pentru a utiliza datele dumneavoastră</string>
+    <string name="contact_list_integration_not_available">Integrarea agendei nu este disponibilă</string>
+    <string name="privacy_policy">Politica de confidențialitate</string>
 </resources>

src/main/res/values-ru/strings.xml 🔗

@@ -55,7 +55,7 @@
     <string name="remove_bookmark_text">Вы хотите удалить %s из избранного? Беседы, связанные с данной закладкой, будут сохранены.</string>
     <string name="register_account">Создать новую учётную запись на сервере</string>
     <string name="change_password_on_server">Изменить пароль на сервере</string>
-    <string name="share_with">Поделиться с</string>
+    <string name="share_with">Поделиться с…</string>
     <string name="start_conversation">Начать беседу</string>
     <string name="invite_contact">Пригласить контакт</string>
     <string name="invite">Пригласить</string>
@@ -88,7 +88,9 @@
     <string name="clear_conversation_history">Очистить историю</string>
     <string name="clear_histor_msg">Вы хотите удалить все сообщения в этой беседе?\n\n<b>Внимание:</b> Данная операция не повлияет на сообщения, хранящиеся на других устройствах или серверах.</string>
     <string name="delete_file_dialog">Удалить файл</string>
-    <string name="delete_file_dialog_msg">Вы уверены, что хотите удалить этот файл?\n\n<b>Предупреждение:</b> Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах.</string>
+    <string name="delete_file_dialog_msg">Вы уверены, что хотите удалить этот файл?
+\n
+\n<b>Предупреждение:</b> Данная операция не удалит копии этого файла, хранящиеся на других устройствах или серверах. </string>
     <string name="also_end_conversation">Закрыть эту беседу</string>
     <string name="choose_presence">Выберите устройство</string>
     <string name="send_unencrypted_message">Нешифрованное сообщение</string>
@@ -97,7 +99,7 @@
     <string name="send_omemo_message">OMEMO-зашифр. сообщение</string>
     <string name="send_omemo_x509_message">v\\OMEMO-зашифр. сообщение</string>
     <string name="send_pgp_message">OpenPGP зашифр. сообщение</string>
-    <string name="your_nick_has_been_changed">Имя уже используется</string>
+    <string name="your_nick_has_been_changed">Используется новое имя</string>
     <string name="send_unencrypted">Отправить в незашифрованном виде</string>
     <string name="decryption_failed">Расшифровка не удалась. Вероятно, что у вас нет надлежащего ключа.</string>
     <string name="openkeychain_required">Установите OpenKeychain</string>
@@ -128,7 +130,7 @@
     <string name="pref_notification_grace_period_summary">Время, на которое уведомления будут отключены, когда вы пользуетесь аккаунтом на другом устройстве.</string>
     <string name="pref_advanced_options">Дополнительно</string>
     <string name="pref_never_send_crash">Не отправлять отчёты об ошибках</string>
-    <string name="pref_never_send_crash_summary">Отправляя отчеты об ошибках, вы помогаете разработке этого приложения</string>
+    <string name="pref_never_send_crash_summary">Отправляя отчёты об ошибках, вы помогаете разработке Quicksy</string>
     <string name="pref_confirm_messages">Отчёты о получении</string>
     <string name="pref_confirm_messages_summary">Позволяет вашим контактам видеть, когда вы получили и прочитали их сообщения</string>
     <string name="pref_prevent_screenshots">Запретить скриншоты</string>
@@ -314,8 +316,8 @@
     <string name="jabber_id_copied_to_clipboard">XMPP-адрес скопирован в буфер обмена</string>
     <string name="error_message_copied_to_clipboard">Сообщение об ошибке скопировано в буфер обмена</string>
     <string name="web_address">веб-адрес</string>
-    <string name="scan_qr_code">Сканировать 2D штрихкод</string>
-    <string name="show_qr_code">Показать 2D штрихкод</string>
+    <string name="scan_qr_code">Сканировать QR-код</string>
+    <string name="show_qr_code">Показать QR-код</string>
     <string name="show_block_list">Показать чёрный список</string>
     <string name="account_details">Сведения об учётной записи</string>
     <string name="confirm">Подтвердить</string>
@@ -512,7 +514,10 @@
     <string name="no_storage_permission">Предоставить %1$s разрешение на использование внешнего накопителя</string>
     <string name="no_camera_permission">Предоставить %1$s разрешение на использование камеры</string>
     <string name="sync_with_contacts">Синхронизировать с контактами</string>
-    <string name="sync_with_contacts_long">%1$s нужно разрешение на доступ к контактам, чтобы соотнести их с вашими XMPP-контактами.\nЭто позволит отобразить полные имена и аватары контактов.\n\n%1$s сделает это локально, без отправки чего-либо на ваш сервер.</string>
+    <string name="sync_with_contacts_long">%1$s нужно разрешение на доступ к контактам, чтобы соотнести их с вашими XMPP-контактами.
+\nЭто позволит отобразить полные имена и аватары контактов.
+\n
+\n%1$s сделает это локально, без отправки чего-либо на ваш сервер.</string>
     <string name="notify_on_all_messages">Все сообщения</string>
     <string name="notify_only_when_highlighted">Уведомлять только при упоминании</string>
     <string name="notify_never">Уведомления выключены</string>
@@ -523,7 +528,9 @@
     <string name="large_images_only">Только большие изображения</string>
     <string name="battery_optimizations_enabled">Оптимизации энергопотребления разрешены</string>
     <string name="battery_optimizations_enabled_explained">Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений.\nРекомендуем её отключить.</string>
-    <string name="battery_optimizations_enabled_dialog">Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений.\nСейчас появится предложение её отключить.</string>
+    <string name="battery_optimizations_enabled_dialog">Ваше устройство использует агрессивную оптимизацию энергопотребления %1$s, что может привести к задержке уведомлений и даже потере сообщений.
+\n
+\nСейчас появится предложение её отключить.</string>
     <string name="disable">Запретить</string>
     <string name="selection_too_large">Выбранная область слишком большая</string>
     <string name="no_accounts">(Нет активированных учётных записей)</string>
@@ -532,7 +539,7 @@
     <string name="send_corrected_message">Отправить исправленное сообщение</string>
     <string name="no_keys_just_confirm">Вы уже пометили отпечаток этого человека как доверенный. Выбрав \"Готово\", вы только подтвердите, что %s является участником конференции.</string>
     <string name="this_account_is_disabled">Вы отключили эту учётную запись</string>
-    <string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу</string>
+    <string name="security_error_invalid_file_access">Ошибка безопасности: недействительный доступ к файлу!</string>
     <string name="no_application_to_share_uri">Не найдено приложения для передачи URI</string>
     <string name="share_uri_with">Отправить URI…</string>
     <string name="agree_and_continue">Согласиться и продолжить</string>
@@ -543,7 +550,7 @@
     <string name="use_own_provider">Использовать свой провайдер</string>
     <string name="pick_your_username">Выберите имя пользователя</string>
     <string name="pref_manually_change_presence">Управлять доступностью вручную</string>
-    <string name="pref_manually_change_presence_summary">Устанавливать свою доступность при редактировании статусного сообщения</string>
+    <string name="pref_manually_change_presence_summary">Устанавливать свою доступность при редактировании статусного сообщения.</string>
     <string name="status_message">Статусное собщение</string>
     <string name="presence_chat">Свободен для общения</string>
     <string name="presence_online">В сети</string>
@@ -610,10 +617,10 @@
     <string name="pref_blind_trust_before_verification_summary">Автоматически доверять всем новым устройствам контактов, которые не были подтверждены ранее, но запрашивать ручное подтверждение каждый раз, когда подтвержденный контакт добавляет новое устройство.</string>
     <string name="blindly_trusted_omemo_keys">Принятие OMEMO-ключей вслепую. Это означает, что собеседник может оказаться недоверенным лицом.</string>
     <string name="not_trusted">Недоверенный</string>
-    <string name="invalid_barcode">Некорректный 2D штрихкод</string>
+    <string name="invalid_barcode">Некорректный QR-код</string>
     <string name="pref_clean_cache_summary">Очистить кэш (используется камерой)</string>
     <string name="pref_clean_cache">Очистить кэш</string>
-    <string name="pref_clean_private_storage">Очистить приватное хранилище.</string>
+    <string name="pref_clean_private_storage">Очистить приватное хранилище</string>
     <string name="pref_clean_private_storage_summary">Очистить закрытое хранилище, где хранятся файлы (Файлы можно заново скачать с сервера)</string>
     <string name="i_followed_this_link_from_a_trusted_source">Открывать ссылки из надёжного источника</string>
     <string name="verifying_omemo_keys_trusted_source">Вы подтверждаете OMEMO-ключи %1$s после нажатия на ссылку. Это безопасно только если вы перешли по ссылке из доверенного источника, где только %2$s мог разместить эту ссылку.</string>
@@ -621,7 +628,8 @@
     <string name="show_inactive_devices">Показывать неактивные</string>
     <string name="hide_inactive_devices">Скрыть неактивные</string>
     <string name="distrust_omemo_key">Прекратить доверять устройству</string>
-    <string name="distrust_omemo_key_text">Вы действительно хотите удалить устройство из доверенных?\Устройство и сообщения, полученные с этого устройства, будут помечаться как недоверенные.</string>
+    <string name="distrust_omemo_key_text">Вы действительно хотите удалить устройство из доверенных?
+\nЭто устройство и сообщения, полученные с него, будут помечаться как недоверенные.</string>
     <plurals name="seconds">
         <item quantity="one">%d секунда</item>
         <item quantity="few">%d секунды</item>
@@ -664,7 +672,7 @@
     <string name="not_fetching_history_retention_period">Не загружаем сообщения, в соответствии с локальным сроком хранения.</string>
     <string name="transcoding_video">Сжимание видео</string>
     <string name="corresponding_conversations_closed">Соответствующие беседы закрыты.</string>
-    <string name="contact_blocked_past_tense">Контакт заблокирован</string>
+    <string name="contact_blocked_past_tense">Контакт заблокирован.</string>
     <string name="pref_notifications_from_strangers">Уведомления от неизвестных контактов</string>
     <string name="pref_notifications_from_strangers_summary">Уведомлять о сообщениях и звонках от незнакомых контактов.</string>
     <string name="received_message_from_stranger">Получено сообщение от неизвестного контакта</string>
@@ -825,7 +833,7 @@
     <string name="try_again_in_x">Пожалуйста, попробуйте еще раз через %s</string>
     <string name="rate_limited">У вас есть ограничение скорости</string>
     <string name="too_many_attempts">Слишком много попыток</string>
-    <string name="the_app_is_out_of_date">Вы используете устаревшую версию приложения</string>
+    <string name="the_app_is_out_of_date">Вы используете устаревшую версию этого приложения.</string>
     <string name="update">Обновить</string>
     <string name="logged_in_with_another_device">Этот номер телефона в данный момент авторизирован на другом устройстве.</string>
     <string name="enter_your_name_instructions">Пожалуйста, введите ваше имя, чтобы другие люди, у которых нет вас в списке контактов, знали кто вы.</string>
@@ -863,7 +871,7 @@
     <string name="channel_already_exists">Этот канал уже существует</string>
     <string name="joined_an_existing_channel">Вы присоединились к существующему каналу</string>
     <string name="unable_to_set_channel_configuration">Не удалось сохранить настройки канала</string>
-    <string name="allow_participants_to_edit_subject">Разрешить всем редактировать тему.</string>
+    <string name="allow_participants_to_edit_subject">Разрешить всем редактировать тему</string>
     <string name="allow_participants_to_invite_others">Разрешить всем приглашать других</string>
     <string name="anyone_can_edit_subject">Кто угодно может редактировать тему.</string>
     <string name="owners_can_edit_subject">Владельцы могут редактировать тему.</string>
@@ -991,7 +999,7 @@
     <string name="rtp_state_reconnecting">Переподключение</string>
     <string name="no_xmpp_adddress_found">Адрес XMPP не найден</string>
     <string name="pref_up_push_account_title">Учётная запись XMPP</string>
-    <string name="conference_technical_problems">Вы покинули эту беседу из-за технических причин</string>
+    <string name="conference_technical_problems">Вы покинули данный групповой чат по техническим причинам</string>
     <string name="rtp_state_content_add">Добавить дополнительные треки\?</string>
     <string name="reconnecting_call">Переподключение к звонку</string>
     <string name="reconnecting_video_call">Переподключение к видеовызову</string>
@@ -1008,7 +1016,7 @@
     <string name="pref_autojoin">Синхронизировать закладки</string>
     <string name="pref_autojoin_summary">Устанавливать флаг \"автоприсоединение\" при входе в- и выходе из MUC, и реагировать на изменения от других клиентов.</string>
     <string name="search_group_chats">Поиск по групповым беседам</string>
-    <string name="download_failed_invalid_file">Загрузка провалена: неверный файл</string>
+    <string name="download_failed_invalid_file">Загрузка неудачна: Неизвестный файл</string>
     <string name="rtp_state_content_add_video">Перейти на видеовызов\?</string>
     <string name="outgoing_call_duration_timestamp">Исходящий вызов (%s) · %s</string>
     <string name="incoming_call_duration_timestamp">Входящий вызов (%s) · %s</string>
@@ -1038,10 +1046,10 @@
     <string name="this_account_is_logged_out">Вы вышли из этой учётной записи</string>
     <string name="log_in">Войти</string>
     <string name="hide_notification">Скрыть уведомление</string>
-    <string name="contact_uses_unverified_keys">Ваш контакт использует неподтверждённые устройства. Отсканируйте его штрих-код для проверки и предотвращения атаки посредника.</string>
+    <string name="contact_uses_unverified_keys">Ваш контакт использует неподтверждённые устройства. Отсканируйте его QR-код для проверки и предотвращения атаки посредника.</string>
     <string name="log_out">Выйти</string>
     <string name="account_state_logged_out">Деавторизован</string>
-    <string name="unverified_devices">Вы используете неподтверждённые устройства. Отсканируйте штрих-код на подтверждённом устройстве для проверки и предотвращения атаки посредника.</string>
+    <string name="unverified_devices">Вы используете неподтверждённые устройства. Отсканируйте QR-код на подтверждённом устройстве для проверки и предотвращения атаки посредника.</string>
     <string name="report_spam_and_block">Пожаловаться на спам и заблокировать</string>
     <string name="report_spam">Пожаловаться на спам</string>
 </resources>

src/main/res/values-sr/strings.xml 🔗

@@ -31,13 +31,8 @@
     <string name="minutes_ago">пре %d минута</string>
     <plurals name="x_unread_conversations">
         <item quantity="one">%d непрочитана порука</item>
-
-    
         <item quantity="few">%d непрочитане поруке</item>
-
-    
         <item quantity="other">%d непрочитаних порука</item>
-
     </plurals>
     <string name="sending">шаљем…</string>
     <string name="message_decrypting">Дешифрујем поруку, сачекајте…</string>
@@ -682,4 +677,4 @@
     <string name="silent_messages_channel_name">Тихе поруке</string>
     <string name="video_compression_channel_name">Видео компресија</string>
     <string name="rtp_state_declined_or_busy">Заузет</string>
-    </resources>
+</resources>

src/main/res/values-szl/strings.xml 🔗

@@ -31,13 +31,8 @@
     <string name="minutes_ago">%d minut tymu</string>
     <plurals name="x_unread_conversations">
         <item quantity="one">%d niyprzeczytano kōnwersacyjo</item>
-
-    
         <item quantity="few">%d niyprzeczytane kōnwersacyje</item>
-
-    
         <item quantity="other">%d niyprzeczytanych kōnwersacyji</item>
-
     </plurals>
     <string name="sending">wysyłanie…</string>
     <string name="message_decrypting">Ôdszyfrowowanie wiadōmości. To weźnie ino chwila…</string>
@@ -1008,4 +1003,4 @@
     <string name="plain_text_document">Dokumynt ze samym tekstym</string>
     <string name="account_registrations_are_not_supported">Registracyjo kōnt niy je spiyrano</string>
     <string name="no_xmpp_adddress_found">Żodno adresa XMPP niyznojdziōno</string>
-    </resources>
+</resources>

src/main/res/values-uk/strings.xml 🔗

@@ -507,7 +507,7 @@
     <string name="shared_image_with_x">Зображення надіслано %s</string>
     <string name="shared_images_with_x">Зображення надіслано %s</string>
     <string name="shared_text_with_x">Текст надіслано %s</string>
-    <string name="sync_with_contacts">Синхронізувати контакти</string>
+    <string name="sync_with_contacts">Інтеграція зі списком контактів</string>
     <string name="notify_on_all_messages">Сповіщати про всі повідомлення</string>
     <string name="notify_only_when_highlighted">Сповіщати, лише якщо згадують</string>
     <string name="notify_never">Сповіщення вимкнено</string>
@@ -600,7 +600,7 @@
     <string name="pref_blind_trust_before_verification_summary">Автоматично довіряти всім новим пристроям співрозмовників, які ще не пройшли перевірки, запитувати підтвердження вручну щоразу, як перевірений контакт додає свій новий пристрій.</string>
     <string name="blindly_trusted_omemo_keys">Ключі OMEMO, яким Ви довіряєте наосліп, тобто співрозмовник може бути не тим, кому Ви довіряєте.</string>
     <string name="not_trusted">Недовірений</string>
-    <string name="invalid_barcode">Недійсний QR-код </string>
+    <string name="invalid_barcode">Недійсний QR-код</string>
     <string name="pref_clean_cache_summary">Очистити теку з кешем (використовується застосунком Камера)</string>
     <string name="pref_clean_cache">Очистити кеш</string>
     <string name="pref_clean_private_storage">Очистити приватне сховище</string>
@@ -989,10 +989,9 @@
     <string name="pref_up_push_server_summary">Оберіть сервер для доставки push-повідомлень через XMPP на Ваш пристрій.</string>
     <string name="error_security_exception">Застосунок для передачі зображення не надав достатніх дозволів.</string>
     <string name="audio_video_disabled_tor">Виклики вимкнені при використанні Tor</string>
-    <string name="sync_with_contacts_long">%1$s потребує дозволу на доступ до контактів, щоб порівняти їх з Вашими XMPP-контактами.
-\nТаким чином можна буде показувати піктограми і повні імена користувачів.
+    <string name="sync_with_contacts_long">%1$s обробляє Ваш список контактів локально, на Вашому пристрої, щоб показати імена та зображення профілю для відповідних контактів у XMPP.
 \n
-\n%1$s лише прочитає Вашу адресну книгу і зіставлятиме інформацію про контакти локально, нічого не завантажуючи на сервер.</string>
+\nУсі дані списку контактів залишаються на Вашому пристрої!</string>
     <string name="missed_calls_channel_name">Пропущені виклики</string>
     <string name="data_saver_enabled_explained">Ваша операційна система обмежує для %1$s доступ до Інтернету у фоновому режимі. Щоб отримувати сповіщення про нові повідомлення, Вам потрібно дозволити %1$s необмежений доступ, коли заощадження трафіку увімкнено.
 \n%1$s намагатиметься по можливості економити трафік.</string>
@@ -1074,4 +1073,8 @@
     <string name="account_state_logged_out">Ви вийшли</string>
     <string name="report_spam_and_block">Повідомити про спам і заблокувати спамера</string>
     <string name="report_spam">Повідомити про спам</string>
+    <string name="welcome_header_quicksy">Вітаємо у Quicksy!</string>
+    <string name="quicksy_wants_your_consent">Quicksy просить згоду на використання Ваших даних</string>
+    <string name="privacy_policy">Політика конфіденційності</string>
+    <string name="contact_list_integration_not_available">Інтеграція зі списком контактів недоступна</string>
 </resources>

src/main/res/values-vi/strings.xml 🔗

@@ -507,7 +507,10 @@
     <string name="no_storage_permission">Cấp quyền truy cập bộ nhớ cho %1$s</string>
     <string name="no_camera_permission">Cấp quyền truy cập máy ảnh cho %1$s</string>
     <string name="sync_with_contacts">Đồng bộ với danh bạ</string>
-    <string name="sync_with_contacts_long">%1$s muốn quyền truy cập sổ địa chỉ của bạn để nối nó với danh sách liên hệ XMPP của bạn.\nViệc này sẽ hiển thị họ tên và ảnh đại diện của các liên hệ của bạn.\n\n%1$s sẽ chỉ đọc sổ địa chỉ của bạn và nối nó một cách cục bộ mà không tải gì cả lên máy chủ của bạn.</string>
+    <string name="sync_with_contacts_long">%1$s muốn quyền truy cập sổ địa chỉ của bạn để nối nó với danh sách liên hệ XMPP của bạn.
+\nViệc này sẽ hiển thị họ tên và ảnh đại diện của các liên hệ của bạn.
+\n
+\n%1$s sẽ chỉ đọc sổ địa chỉ của bạn và nối nó một cách cục bộ mà không tải gì cả lên máy chủ của bạn.</string>
     <string name="notify_on_all_messages">Thông báo tất cả tin nhắn</string>
     <string name="notify_only_when_highlighted">Chỉ thông báo khi được nhắc đến</string>
     <string name="notify_never">Đã tắt thông báo</string>

src/main/res/values-zh-rCN/strings.xml 🔗

@@ -554,7 +554,7 @@
     <string name="this_field_is_required">此字段是必需的</string>
     <string name="correct_message">更正消息</string>
     <string name="send_corrected_message">发送更正后的消息</string>
-    <string name="no_keys_just_confirm">您已经验证了此用户的指纹。选择“完成”确认 %s 是此群聊的一员。</string>
+    <string name="no_keys_just_confirm">您已信任此人的指纹。选择“完成”即表示您确认 %s 是此群聊的一员。</string>
     <string name="this_account_is_disabled">您已禁用了此账号</string>
     <string name="security_error_invalid_file_access">安全错误:文件访问无效!</string>
     <string name="no_application_to_share_uri">未找到可以分享 URI 的应用</string>
@@ -1032,4 +1032,8 @@
     <string name="unverified_devices">您正在使用未经验证的设备。扫描您其他设备的二维码进行验证并阻止主动式中间人攻击。</string>
     <string name="report_spam_and_block">报告垃圾消息并屏蔽垃圾消息发送者</string>
     <string name="report_spam">报告垃圾消息</string>
+    <string name="welcome_header_quicksy">欢迎使用 Quicksy!</string>
+    <string name="quicksy_wants_your_consent">Quicksy 请求您同意使用您的数据</string>
+    <string name="privacy_policy">隐私政策</string>
+    <string name="contact_list_integration_not_available">通讯录集成不可用</string>
 </resources>

src/main/res/values/strings.xml 🔗

@@ -518,9 +518,10 @@
     <string name="shared_text_with_x">Text shared with %s</string>
     <string name="no_storage_permission">Grant %1$s access to external storage</string>
     <string name="no_camera_permission">Grant %1$s access to the camera</string>
-    <string name="sync_with_contacts">Synchronize with contacts</string>
-    <string name="sync_with_contacts_long">%1$s wants permission to access your address book to match it with your Jabber contact list.\nThis will display your contacts’ full names and avatars.\n\n%1$s will only read your address book and match it locally without uploading anything to your server.</string>
-    <string name="sync_with_contacts_quicksy"><![CDATA[Quicksy needs access to your contacts’ phone numbers to make suggestions about possible contacts who are already on Quicksy.<br><br>We will not store a copy of those phone numbers.\n\nFor more information read our <a href="https://quicksy.im/#privacy">privacy policy</a>.<br><br>You will now be asked to grant permission to access your contacts.]]></string>
+    <string name="quicksy_wants_your_consent">Quicksy asks for your consent to use your data</string>
+    <string name="sync_with_contacts">Contact list integration</string>
+    <string name="sync_with_contacts_long">%1$s processes your contact list locally, on your device, to show you the names and profile pictures for matching contacts on the Jabber network.\n\nNo contact list data ever leaves your device!</string>
+    <string name="sync_with_contacts_quicksy_static" translatable="false"><![CDATA[Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts, who are already using the app, even when the app is closed or not in use.<br><br>Find more information in our <a href="https://quicksy.im/privacy.htm">Privacy Policy</a>.]]></string>
     <string name="notify_on_all_messages">Notify on all messages</string>
     <string name="notify_only_when_highlighted">Notify only when mentioned</string>
     <string name="notify_never">Notifications disabled</string>
@@ -546,8 +547,8 @@
     <string name="no_application_to_share_uri">No app found to share URI</string>
     <string name="share_uri_with">Share URI with…</string>
     <string name="welcome_header" translatable="false">Join the Conversation</string>
-    <string name="welcome_header_quicksy" translatable="false">Have some Quick Conversations</string>
-    <string name="welcome_text_quicksy_static" translatable="false"><![CDATA[Quicksy is a spin off of the popular XMPP client Conversations with automatic contact discovery.<br><br>You sign up with your phone number and Quicksy will automatically—based on the phone numbers in your address book—suggest possible contacts to you.<br>Quicksy stores your contacts’ phone numbers to make suggestions about possible contacts who are already on Quicksy.<br>By signing up you agree to our <a href="https://quicksy.im/privacy.htm">Privacy Policy</a> and our <a href="https://quicksy.im/tos.htm">Terms &amp; Conditions</a>.]]></string>
+    <string name="welcome_header_quicksy">Welcome to Quicksy!</string>
+    <string name="welcome_text_quicksy_static" translatable="false"><![CDATA[· Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts who are already on Quicksy even when the app is closed or not in use.<br>· Quicksy shares and stores images, audio recordings, videos and other media to deliver them to the intended recipients. Files will be stored for up to 30 days.<br><br>Find more information in our <a href="https://quicksy.im/privacy.htm">Privacy Policy</a>.]]></string>
     <string name="agree_and_continue">Agree and continue</string>
     <string name="magic_create_text">A guide is set up for account creation on ChatterboxTown.\nYou will be able to communicate with users of other providers by giving them your full Jabber ID.</string>
     <string name="your_full_jid_will_be">Your full Jabber ID will be: %s</string>
@@ -1030,4 +1031,6 @@
     <string name="unverified_devices">You are using unverified devices. Scan the QR Code on your other devices to perform verification and impede active MITM attacks.</string>
     <string name="report_spam">Report spam</string>
     <string name="report_spam_and_block">Report spam and block spammer</string>
+    <string name="privacy_policy">Privacy policy</string>
+    <string name="contact_list_integration_not_available">Contact list integration is not available</string>
 </resources>

src/playstore/AndroidManifest.xml 🔗

@@ -1,6 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
+
+    <!-- Remove this line if Google Play Store is giving you a hard time -->
+    <uses-permission android:name="android.permission.READ_CONTACTS"/>
+
     <application android:icon="@mipmap/new_launcher">
 
         <meta-data

src/quicksy/AndroidManifest.xml 🔗

@@ -6,6 +6,9 @@
         android:name="android.permission.REQUEST_INSTALL_PACKAGES"
         tools:node="remove" />
 
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+
     <application
         android:icon="@mipmap/new_launcher"
         tools:ignore="GoogleAppIndexingWarning"

src/quicksy/java/eu/siacs/conversations/ui/EnterNameActivity.java 🔗

@@ -1,20 +1,23 @@
 package eu.siacs.conversations.ui;
 
 import android.content.Intent;
-import androidx.databinding.DataBindingUtil;
 import android.os.Bundle;
-import androidx.appcompat.widget.Toolbar;
 import android.view.View;
 
-import java.util.concurrent.atomic.AtomicBoolean;
+import androidx.appcompat.widget.Toolbar;
+import androidx.databinding.DataBindingUtil;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityEnterNameBinding;
 import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.services.AbstractQuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.AccountUtils;
 
-public class EnterNameActivity extends XmppActivity implements XmppConnectionService.OnAccountUpdate {
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class EnterNameActivity extends XmppActivity
+        implements XmppConnectionService.OnAccountUpdate {
 
     private ActivityEnterNameBinding binding;
 
@@ -28,23 +31,28 @@ public class EnterNameActivity extends XmppActivity implements XmppConnectionSer
         this.binding = DataBindingUtil.setContentView(this, R.layout.activity_enter_name);
         setSupportActionBar((Toolbar) this.binding.toolbar);
         this.binding.next.setOnClickListener(this::next);
-        this.setNick.set(savedInstanceState != null && savedInstanceState.getBoolean("set_nick",false));
+        this.setNick.set(
+                savedInstanceState != null && savedInstanceState.getBoolean("set_nick", false));
     }
 
-    private void next(View view) {
-        if (account != null) {
-
-            String name = this.binding.name.getText().toString().trim();
-
-            account.setDisplayName(name);
-
-            xmppConnectionService.publishDisplayName(account);
-
-            Intent intent = new Intent(this, PublishProfilePictureActivity.class);
-            intent.putExtra(PublishProfilePictureActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+    private void next(final View view) {
+        if (account == null) {
+            return;
+        }
+        final String name = this.binding.name.getText().toString().trim();
+        account.setDisplayName(name);
+        xmppConnectionService.publishDisplayName(account);
+        final Intent intent;
+        if (AbstractQuickConversationsService.isQuicksyPlayStore()) {
+            intent = new Intent(getApplicationContext(), StartConversationActivity.class);
+            intent.putExtra("init", true);
+            intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+        } else {
+            intent = new Intent(this, PublishProfilePictureActivity.class);
             intent.putExtra("setup", true);
-            startActivity(intent);
         }
+        intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
+        startActivity(intent);
         finish();
     }
 
@@ -66,7 +74,7 @@ public class EnterNameActivity extends XmppActivity implements XmppConnectionSer
     }
 
     private void checkSuggestPreviousNick() {
-        String displayName = this.account == null ? null : this.account.getDisplayName();
+        final String displayName = this.account == null ? null : this.account.getDisplayName();
         if (displayName != null) {
             if (setNick.compareAndSet(false, true) && this.binding.name.getText().length() == 0) {
                 this.binding.name.getText().append(displayName);

src/quicksy/res/values-de/strings.xml 🔗

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="pref_notification_grace_period_summary">Zeitspanne, in der Quicksy still bleibt, nachdem es Aktivitäten auf einem anderen Gerät erkannt hat</string>
-    <string name="pref_never_send_crash_summary">Mit dem Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung von Quicksy</string>
+    <string name="pref_never_send_crash_summary">Durch das Einsenden von Absturzberichten hilfst du bei der Weiterentwicklung von Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Informiere deine Kontakte, wann du Quicksy nutzt</string>
     <string name="huawei_protected_apps_summary">Um weiterhin Benachrichtigungen zu erhalten, auch wenn der Bildschirm ausgeschaltet ist, musst du Quicksy zur Liste der geschützten Apps hinzufügen.</string>
     <string name="set_profile_picture">Quicksy Profilbild</string>

src/quicksy/res/values-ja/strings.xml 🔗

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="pref_notification_grace_period_summary">別のデバイスで活動を見た後、Quicksy を静かにする時間の長さ</string>
-    <string name="pref_never_send_crash_summary">スタックトレースを送信することで、あなたは Quicksy の継続的な開発を支援しています</string>
+    <string name="pref_never_send_crash_summary">スタックトレースを送信すると、 Quicksyの開発の助けとなります</string>
     <string name="pref_broadcast_last_activity_summary">Quicksy を使用するときに、すべての連絡先に知らせましょう</string>
     <string name="huawei_protected_apps_summary">画面がオフになっている場合でも通知を受信し続けるには、保護されたアプリのリストに Quicksy を追加する必要があります。</string>
     <string name="set_profile_picture">Quicksy プロフィール写真</string>
@@ -9,4 +9,4 @@
     <string name="unable_to_verify_server_identity">サーバーの同一性を確認できません。</string>
     <string name="unknown_security_error">未知のセキュリティエラー。</string>
     <string name="timeout_while_connecting_to_server">サーバーへの接続中にタイムアウトが発生しました。</string>
-</resources>
+</resources>

src/quicksy/res/values-ru/strings.xml 🔗

@@ -4,7 +4,7 @@
     <string name="pref_never_send_crash_summary">Отправляя отчёты об ошибках, вы помогаете в разработке Quicksy</string>
     <string name="pref_broadcast_last_activity_summary">Извещать собеседников, когда вы пользуетесь Quicksy</string>
     <string name="huawei_protected_apps_summary">Чтобы продолжать получать уведомления, даже если экран выключен, вам необходимо добавить Quicksy в список защищенных приложений.</string>
-    <string name="set_profile_picture">Аватар профиля Quicksy</string>
+    <string name="set_profile_picture">Аватар Quicksy</string>
     <string name="not_available_in_your_country">Quicksy недоступен в Вашем регионе.</string>
     <string name="unable_to_verify_server_identity">Не удалось подтвердить сервер.</string>
     <string name="unknown_security_error">Неизвестная ошибка безопасности.</string>