Merge commit '086a72731b75df4735cb4f18f00b66f938997918'

Stephen Paul Weber created

* commit '086a72731b75df4735cb4f18f00b66f938997918':
  add mime type and icon for pcap files
  refactor HTTP slot requester
  better wording for no channel binding error
  Translated using Weblate (Italian)
  Translated using Weblate (Portuguese (Brazil))
  Translated using Weblate (Italian)
  Added translation using Weblate (Irish)
  Translated using Weblate (Irish)
  Added translation using Weblate (Irish)
  Translated using Weblate (Russian)
  Translated using Weblate (Russian)
  Translated using Weblate (Russian)
  Translated using Weblate (Polish)
  Translated using Weblate (Serbian)
  Translated using Weblate (Russian)

Change summary

fastlane/metadata/android/it-IT/changelogs/4214204.txt                        |   2 
fastlane/metadata/android/pl-PL/changelogs/4214204.txt                        |   2 
src/conversations/res/values-ga/strings.xml                                   |   6 
src/main/java/eu/siacs/conversations/crypto/PgpEngine.java                    | 352 
src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java       |   8 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java   |  80 
src/main/java/eu/siacs/conversations/entities/Edit.java                       |  14 
src/main/java/eu/siacs/conversations/entities/MucOptions.java                 |   6 
src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java           | 143 
src/main/java/eu/siacs/conversations/entities/ReadByMarker.java               | 320 
src/main/java/eu/siacs/conversations/generator/IqGenerator.java               |  26 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java          |   5 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java           | 169 
src/main/java/eu/siacs/conversations/http/Method.java                         |  51 
src/main/java/eu/siacs/conversations/http/SlotRequester.java                  | 128 
src/main/java/eu/siacs/conversations/parser/IqParser.java                     |   4 
src/main/java/eu/siacs/conversations/parser/MessageParser.java                |   1 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java         |   4 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java             |   4 
src/main/java/eu/siacs/conversations/services/CallIntegration.java            |   5 
src/main/java/eu/siacs/conversations/services/MessageArchiveService.java      | 231 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      |  16 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java             |   3 
src/main/java/eu/siacs/conversations/ui/CreatePrivateGroupChatDialog.java     |  44 
src/main/java/eu/siacs/conversations/ui/CreatePublicChannelDialog.java        |   2 
src/main/java/eu/siacs/conversations/ui/JoinConferenceDialog.java             | 224 
src/main/java/eu/siacs/conversations/ui/ShortcutActivity.java                 |  54 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java                     |   3 
src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java        |   2 
src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java             |  11 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java           |   2 
src/main/java/eu/siacs/conversations/ui/service/CameraManager.java            | 144 
src/main/java/eu/siacs/conversations/ui/util/AvatarWorkerTask.java            |  35 
src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java |  82 
src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java               |  24 
src/main/java/eu/siacs/conversations/utils/CursorUtils.java                   |   4 
src/main/java/eu/siacs/conversations/utils/GeoHelper.java                     |   9 
src/main/java/eu/siacs/conversations/utils/ImStyleParser.java                 |  28 
src/main/java/eu/siacs/conversations/utils/MimeUtils.java                     |   8 
src/main/java/eu/siacs/conversations/xml/Namespace.java                       |   1 
src/main/java/eu/siacs/conversations/xmpp/IqErrorResponseException.java       |  33 
src/main/java/eu/siacs/conversations/xmpp/IqResponseException.java            |   8 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                 | 158 
src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java                     |   3 
src/main/java/im/conversations/android/xmpp/model/muc/Affiliation.java        |   2 
src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java          |   5 
src/main/java/im/conversations/android/xmpp/model/upload/Slot.java            |  10 
src/main/res/drawable/ic_help_center_48dp.xml                                 |  16 
src/main/res/drawable/ic_lan_24dp.xml                                         |  12 
src/main/res/layout/activity_edit_account.xml                                 |   1 
src/main/res/values-ga/strings.xml                                            |   2 
src/main/res/values-it/strings.xml                                            |   3 
src/main/res/values-pt-rBR/strings.xml                                        |   3 
src/main/res/values-ru/strings.xml                                            |   4 
src/main/res/values-sr/strings.xml                                            |   3 
src/main/res/values/strings.xml                                               |   2 
src/quicksy/fastlane/metadata/android/ru-RU/full_description.txt              |   8 
57 files changed, 1,415 insertions(+), 1,115 deletions(-)

Detailed changes

src/conversations/res/values-ga/strings.xml 🔗

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="pick_a_server">Roghnaigh do freastalaí XMPP</string>
+    <string name="use_conversations.im">Bain úsáid as conversations.im</string>
+    <string name="create_new_account">Oscail cuntas nua</string>
+</resources>

src/main/java/eu/siacs/conversations/crypto/PgpEngine.java 🔗

@@ -3,27 +3,10 @@ package eu.siacs.conversations.crypto;
 import android.app.PendingIntent;
 import android.content.Intent;
 import android.util.Log;
-
 import androidx.annotation.StringRes;
-
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
-
-import org.openintents.openpgp.OpenPgpError;
-import org.openintents.openpgp.OpenPgpSignatureResult;
-import org.openintents.openpgp.util.OpenPgpApi;
-import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
@@ -35,6 +18,18 @@ import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.UiCallback;
 import eu.siacs.conversations.utils.AsciiArmor;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
 
 public class PgpEngine {
     private final OpenPgpApi api;
@@ -48,9 +43,20 @@ public class PgpEngine {
     private static void logError(Account account, OpenPgpError error) {
         if (error != null) {
             error.describeContents();
-            Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error '" + error.getMessage() + "' code=" + error.getErrorId() + " class=" + error.getClass().getName());
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid().toString()
+                            + ": OpenKeychain error '"
+                            + error.getMessage()
+                            + "' code="
+                            + error.getErrorId()
+                            + " class="
+                            + error.getClass().getName());
         } else {
-            Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": OpenKeychain error with no message");
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid().toString()
+                            + ": OpenKeychain error with no message");
         }
     }
 
@@ -60,8 +66,7 @@ public class PgpEngine {
         final Conversation conversation = (Conversation) message.getConversation();
         if (conversation.getMode() == Conversation.MODE_SINGLE) {
             long[] keys = {
-                    conversation.getContact().getPgpKeyId(),
-                    conversation.getAccount().getPgpId()
+                conversation.getContact().getPgpKeyId(), conversation.getAccount().getPgpId()
             };
             params.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keys);
         } else {
@@ -78,75 +83,95 @@ public class PgpEngine {
             }
             InputStream is = new ByteArrayInputStream(body.getBytes());
             final OutputStream os = new ByteArrayOutputStream();
-            api.executeApiAsync(params, is, os, result -> {
-                switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
-                    case OpenPgpApi.RESULT_CODE_SUCCESS:
-                        try {
-                            os.flush();
-                            final ArrayList<String> encryptedMessageBody = new ArrayList<>();
-                            final String[] lines = os.toString().split("\n");
-                            for (int i = 2; i < lines.length - 1; ++i) {
-                                if (!lines[i].contains("Version")) {
-                                    encryptedMessageBody.add(lines[i].trim());
+            api.executeApiAsync(
+                    params,
+                    is,
+                    os,
+                    result -> {
+                        switch (result.getIntExtra(
+                                OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+                            case OpenPgpApi.RESULT_CODE_SUCCESS:
+                                try {
+                                    os.flush();
+                                    final ArrayList<String> encryptedMessageBody =
+                                            new ArrayList<>();
+                                    final String[] lines = os.toString().split("\n");
+                                    for (int i = 2; i < lines.length - 1; ++i) {
+                                        if (!lines[i].contains("Version")) {
+                                            encryptedMessageBody.add(lines[i].trim());
+                                        }
+                                    }
+                                    message.setEncryptedBody(
+                                            Joiner.on('\n').join(encryptedMessageBody));
+                                    message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+                                    mXmppConnectionService.sendMessage(message);
+                                    callback.success(message);
+                                } catch (IOException e) {
+                                    callback.error(R.string.openpgp_error, message);
                                 }
-                            }
-                            message.setEncryptedBody(Joiner.on('\n').join(encryptedMessageBody));
-                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
-                            mXmppConnectionService.sendMessage(message);
-                            callback.success(message);
-                        } catch (IOException e) {
-                            callback.error(R.string.openpgp_error, message);
-                        }
 
-                        break;
-                    case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
-                        callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
-                        break;
-                    case OpenPgpApi.RESULT_CODE_ERROR:
-                        OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
-                        String errorMessage = error != null ? error.getMessage() : null;
-                        @StringRes final int res;
-                        if (errorMessage != null && errorMessage.startsWith("Bad key for encryption")) {
-                            res = R.string.bad_key_for_encryption;
-                        } else {
-                            res = R.string.openpgp_error;
+                                break;
+                            case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+                                callback.userInputRequired(
+                                        result.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+                                        message);
+                                break;
+                            case OpenPgpApi.RESULT_CODE_ERROR:
+                                OpenPgpError error =
+                                        result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+                                String errorMessage = error != null ? error.getMessage() : null;
+                                @StringRes final int res;
+                                if (errorMessage != null
+                                        && errorMessage.startsWith("Bad key for encryption")) {
+                                    res = R.string.bad_key_for_encryption;
+                                } else {
+                                    res = R.string.openpgp_error;
+                                }
+                                logError(conversation.getAccount(), error);
+                                callback.error(res, message);
+                                break;
                         }
-                        logError(conversation.getAccount(), error);
-                        callback.error(res, message);
-                        break;
-                }
-            });
+                    });
         } else {
             try {
-                DownloadableFile inputFile = this.mXmppConnectionService
-                        .getFileBackend().getFile(message, true);
-                DownloadableFile outputFile = this.mXmppConnectionService
-                        .getFileBackend().getFile(message, false);
+                DownloadableFile inputFile =
+                        this.mXmppConnectionService.getFileBackend().getFile(message, true);
+                DownloadableFile outputFile =
+                        this.mXmppConnectionService.getFileBackend().getFile(message, false);
                 outputFile.getParentFile().mkdirs();
                 outputFile.createNewFile();
                 final InputStream is = new FileInputStream(inputFile);
                 final OutputStream os = new FileOutputStream(outputFile);
-                api.executeApiAsync(params, is, os, result -> {
-                    switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
-                        case OpenPgpApi.RESULT_CODE_SUCCESS:
-                            try {
-                                os.flush();
-                            } catch (IOException ignored) {
-                                //ignored
+                api.executeApiAsync(
+                        params,
+                        is,
+                        os,
+                        result -> {
+                            switch (result.getIntExtra(
+                                    OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+                                case OpenPgpApi.RESULT_CODE_SUCCESS:
+                                    try {
+                                        os.flush();
+                                    } catch (IOException ignored) {
+                                        // ignored
+                                    }
+                                    FileBackend.close(os);
+                                    mXmppConnectionService.sendMessage(message);
+                                    callback.success(message);
+                                    break;
+                                case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+                                    callback.userInputRequired(
+                                            result.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+                                            message);
+                                    break;
+                                case OpenPgpApi.RESULT_CODE_ERROR:
+                                    logError(
+                                            conversation.getAccount(),
+                                            result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
+                                    callback.error(R.string.openpgp_error, message);
+                                    break;
                             }
-                            FileBackend.close(os);
-                            mXmppConnectionService.sendMessage(message);
-                            callback.success(message);
-                            break;
-                        case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
-                            callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
-                            break;
-                        case OpenPgpApi.RESULT_CODE_ERROR:
-                            logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
-                            callback.error(R.string.openpgp_error, message);
-                            break;
-                    }
-                });
+                        });
             } catch (final IOException e) {
                 callback.error(R.string.openpgp_error, message);
             }
@@ -168,11 +193,11 @@ public class PgpEngine {
         final InputStream is = new ByteArrayInputStream(Strings.nullToEmpty(status).getBytes());
         final ByteArrayOutputStream os = new ByteArrayOutputStream();
         final Intent result = api.executeApi(params, is, os);
-        switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
-                OpenPgpApi.RESULT_CODE_ERROR)) {
+        switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
             case OpenPgpApi.RESULT_CODE_SUCCESS:
-                final OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
-                //TODO unsure that sigResult.getResult() is either 1, 2 or 3
+                final OpenPgpSignatureResult sigResult =
+                        result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
+                // TODO unsure that sigResult.getResult() is either 1, 2 or 3
                 if (sigResult != null) {
                     return sigResult.getKeyId();
                 } else {
@@ -190,22 +215,31 @@ public class PgpEngine {
     public void chooseKey(final Account account, final UiCallback<Account> callback) {
         Intent p = new Intent();
         p.setAction(OpenPgpApi.ACTION_GET_SIGN_KEY_ID);
-        api.executeApiAsync(p, null, null, result -> {
-            switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
-                case OpenPgpApi.RESULT_CODE_SUCCESS:
-                    callback.success(account);
-                    return;
-                case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
-                    callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
-                    return;
-                case OpenPgpApi.RESULT_CODE_ERROR:
-                    logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
-                    callback.error(R.string.openpgp_error, account);
-            }
-        });
+        api.executeApiAsync(
+                p,
+                null,
+                null,
+                result -> {
+                    switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+                        case OpenPgpApi.RESULT_CODE_SUCCESS:
+                            callback.success(account);
+                            return;
+                        case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+                            callback.userInputRequired(
+                                    result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
+                            return;
+                        case OpenPgpApi.RESULT_CODE_ERROR:
+                            logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
+                            callback.error(R.string.openpgp_error, account);
+                    }
+                });
     }
 
-    public void generateSignature(Intent intent, final Account account, String status, final UiCallback<String> callback) {
+    public void generateSignature(
+            Intent intent,
+            final Account account,
+            String status,
+            final UiCallback<String> callback) {
         if (account.getPgpId() == 0) {
             return;
         }
@@ -215,70 +249,86 @@ public class PgpEngine {
         params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
         InputStream is = new ByteArrayInputStream(status.getBytes());
         final OutputStream os = new ByteArrayOutputStream();
-        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": signing status message \"" + status + "\"");
-        api.executeApiAsync(params, is, os, result -> {
-            switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
-                case OpenPgpApi.RESULT_CODE_SUCCESS:
-                    final ArrayList<String> signature = new ArrayList<>();
-                    try {
-                        os.flush();
-                        boolean sig = false;
-                        for (final String line : Splitter.on('\n').split(os.toString())) {
-                            if (sig) {
-                                if (line.contains("END PGP SIGNATURE")) {
-                                    sig = false;
-                                } else {
-                                    if (!line.contains("Version")) {
-                                        signature.add(line.trim());
+        Log.d(
+                Config.LOGTAG,
+                account.getJid().asBareJid() + ": signing status message \"" + status + "\"");
+        api.executeApiAsync(
+                params,
+                is,
+                os,
+                result -> {
+                    switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+                        case OpenPgpApi.RESULT_CODE_SUCCESS:
+                            final ArrayList<String> signature = new ArrayList<>();
+                            try {
+                                os.flush();
+                                boolean sig = false;
+                                for (final String line : Splitter.on('\n').split(os.toString())) {
+                                    if (sig) {
+                                        if (line.contains("END PGP SIGNATURE")) {
+                                            sig = false;
+                                        } else {
+                                            if (!line.contains("Version")) {
+                                                signature.add(line.trim());
+                                            }
+                                        }
+                                    }
+                                    if (line.contains("BEGIN PGP SIGNATURE")) {
+                                        sig = true;
                                     }
                                 }
+                            } catch (IOException e) {
+                                callback.error(R.string.openpgp_error, null);
+                                return;
                             }
-                            if (line.contains("BEGIN PGP SIGNATURE")) {
-                                sig = true;
+                            callback.success(Joiner.on('\n').join(signature));
+                            return;
+                        case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+                            callback.userInputRequired(
+                                    result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
+                            return;
+                        case OpenPgpApi.RESULT_CODE_ERROR:
+                            OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+                            if (error != null
+                                    && "signing subkey not found!".equals(error.getMessage())) {
+                                callback.error(0, null);
+                            } else {
+                                logError(account, error);
+                                callback.error(R.string.unable_to_connect_to_keychain, null);
                             }
-                        }
-                    } catch (IOException e) {
-                        callback.error(R.string.openpgp_error, null);
-                        return;
                     }
-                    callback.success(Joiner.on('\n').join(signature));
-                    return;
-                case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
-                    callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
-                    return;
-                case OpenPgpApi.RESULT_CODE_ERROR:
-                    OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
-                    if (error != null && "signing subkey not found!".equals(error.getMessage())) {
-                        callback.error(0, null);
-                    } else {
-                        logError(account, error);
-                        callback.error(R.string.unable_to_connect_to_keychain, null);
-                    }
-            }
-        });
+                });
     }
 
     public void hasKey(final Contact contact, final UiCallback<Contact> callback) {
         Intent params = new Intent();
         params.setAction(OpenPgpApi.ACTION_GET_KEY);
         params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
-        api.executeApiAsync(params, null, null, new IOpenPgpCallback() {
+        api.executeApiAsync(
+                params,
+                null,
+                null,
+                new IOpenPgpCallback() {
 
-            @Override
-            public void onReturn(Intent result) {
-                switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
-                    case OpenPgpApi.RESULT_CODE_SUCCESS:
-                        callback.success(contact);
-                        return;
-                    case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
-                        callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact);
-                        return;
-                    case OpenPgpApi.RESULT_CODE_ERROR:
-                        logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
-                        callback.error(R.string.openpgp_error, contact);
-                }
-            }
-        });
+                    @Override
+                    public void onReturn(Intent result) {
+                        switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
+                            case OpenPgpApi.RESULT_CODE_SUCCESS:
+                                callback.success(contact);
+                                return;
+                            case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
+                                callback.userInputRequired(
+                                        result.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
+                                        contact);
+                                return;
+                            case OpenPgpApi.RESULT_CODE_ERROR:
+                                logError(
+                                        contact.getAccount(),
+                                        result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
+                                callback.error(R.string.openpgp_error, contact);
+                        }
+                    }
+                });
     }
 
     public PendingIntent getIntentForKey(long pgpKeyId) {
@@ -288,6 +338,6 @@ public class PgpEngine {
         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
         final ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]);
         Intent result = api.executeApi(params, inputStream, outputStream);
-        return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+        return result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
     }
 }

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -951,7 +951,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
                                     Config.LOGTAG,
                                     getLogprefix(account)
                                             + "Error received while publishing bundle: "
-                                            + response.toString());
+                                            + response);
                         }
                         pepBroken = true;
                     }
@@ -1416,7 +1416,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
                                     Config.LOGTAG,
                                     AxolotlService.getLogprefix(account)
                                             + "Already have session for "
-                                            + address.toString()
+                                            + address
                                             + ", adding to cache...");
                             XmppAxolotlSession session =
                                     new XmppAxolotlSession(
@@ -1462,7 +1462,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
                             Config.LOGTAG,
                             AxolotlService.getLogprefix(account)
                                     + "Already have session for "
-                                    + address.toString()
+                                    + address
                                     + ", adding to cache...");
                     XmppAxolotlSession session =
                             new XmppAxolotlSession(account, axolotlStore, address, identityKey);
@@ -1538,7 +1538,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
                         Config.LOGTAG,
                         AxolotlService.getLogprefix(account)
                                 + "Already fetching bundle for "
-                                + address.toString());
+                                + address);
             }
         }
 

src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java 🔗

@@ -2,7 +2,10 @@ package eu.siacs.conversations.crypto.axolotl;
 
 import android.util.Base64;
 import android.util.Log;
-
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.utils.Compatibility;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.Jid;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
@@ -10,7 +13,6 @@ import java.security.NoSuchProviderException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
 import java.util.List;
-
 import javax.crypto.BadPaddingException;
 import javax.crypto.Cipher;
 import javax.crypto.IllegalBlockSizeException;
@@ -20,11 +22,6 @@ import javax.crypto.SecretKey;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.utils.Compatibility;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.Jid;
-
 public class XmppAxolotlMessage {
     public static final String CONTAINERTAG = "encrypted";
     private static final String HEADER = "header";
@@ -45,7 +42,8 @@ public class XmppAxolotlMessage {
     private byte[] authtagPlusInnerKey = null;
     private byte[] iv = null;
 
-    private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
+    private XmppAxolotlMessage(final Element axolotlMessage, final Jid from)
+            throws IllegalArgumentException {
         this.from = from;
         Element header = axolotlMessage.findChild(HEADER);
         try {
@@ -62,7 +60,8 @@ public class XmppAxolotlMessage {
                         int recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
                         byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
                         boolean isPreKey = keyElement.getAttributeAsBoolean("prekey");
-                        this.keys.add(new XmppAxolotlSession.AxolotlKey(recipientId, key, isPreKey));
+                        this.keys.add(
+                                new XmppAxolotlSession.AxolotlKey(recipientId, key, isPreKey));
                     } catch (NumberFormatException e) {
                         throw new IllegalArgumentException("invalid remote id");
                     }
@@ -74,11 +73,12 @@ public class XmppAxolotlMessage {
                     iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
                     break;
                 default:
-                    Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
+                    Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement);
                     break;
             }
         }
-        final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX);
+        final Element payloadElement =
+                axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX);
         if (payloadElement != null) {
             ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT);
         }
@@ -151,9 +151,16 @@ public class XmppAxolotlMessage {
         try {
             SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
             IvParameterSpec ivSpec = new IvParameterSpec(iv);
-            Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+            Cipher cipher =
+                    Compatibility.twentyEight()
+                            ? Cipher.getInstance(CIPHERMODE)
+                            : Cipher.getInstance(CIPHERMODE, PROVIDER);
             cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
-            this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes());
+            this.ciphertext =
+                    cipher.doFinal(
+                            Config.OMEMO_PADDING
+                                    ? getPaddedBytes(plaintext)
+                                    : plaintext.getBytes());
             if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) {
                 this.authtagPlusInnerKey = new byte[16 + 16];
                 byte[] ciphertext = new byte[this.ciphertext.length - 16];
@@ -162,8 +169,12 @@ public class XmppAxolotlMessage {
                 System.arraycopy(this.innerKey, 0, authtagPlusInnerKey, 0, this.innerKey.length);
                 this.ciphertext = ciphertext;
             }
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
-                | IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
+        } catch (NoSuchAlgorithmException
+                | NoSuchPaddingException
+                | InvalidKeyException
+                | IllegalBlockSizeException
+                | BadPaddingException
+                | NoSuchProviderException
                 | InvalidAlgorithmParameterException e) {
             throw new CryptoFailedException(e);
         }
@@ -222,7 +233,8 @@ public class XmppAxolotlMessage {
         return encryptionElement;
     }
 
-    private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
+    private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId)
+            throws CryptoFailedException {
         ArrayList<XmppAxolotlSession.AxolotlKey> possibleKeys = new ArrayList<>();
         for (XmppAxolotlSession.AxolotlKey key : keys) {
             if (key.deviceId == sourceDeviceId) {
@@ -235,17 +247,22 @@ public class XmppAxolotlMessage {
         return session.processReceiving(possibleKeys);
     }
 
-    XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
-        return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV());
+    XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId)
+            throws CryptoFailedException {
+        return new XmppAxolotlKeyTransportMessage(
+                session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV());
     }
 
-    public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
+    public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId)
+            throws CryptoFailedException {
         XmppAxolotlPlaintextMessage plaintextMessage = null;
         byte[] key = unpackKey(session, sourceDeviceId);
         if (key != null) {
             try {
                 if (key.length < 32) {
-                    throw new OutdatedSenderException("Key did not contain auth tag. Sender needs to update their OMEMO client");
+                    throw new OutdatedSenderException(
+                            "Key did not contain auth tag. Sender needs to update their OMEMO"
+                                    + " client");
                 }
                 final int authTagLength = key.length - 16;
                 byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
@@ -256,18 +273,28 @@ public class XmppAxolotlMessage {
                 ciphertext = newCipherText;
                 key = newKey;
 
-                final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
+                final Cipher cipher =
+                        Compatibility.twentyEight()
+                                ? Cipher.getInstance(CIPHERMODE)
+                                : Cipher.getInstance(CIPHERMODE, PROVIDER);
                 SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
                 IvParameterSpec ivSpec = new IvParameterSpec(iv);
 
                 cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
 
                 String plaintext = new String(cipher.doFinal(ciphertext));
-                plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint());
-
-            } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
-                    | InvalidAlgorithmParameterException | IllegalBlockSizeException
-                    | BadPaddingException | NoSuchProviderException e) {
+                plaintextMessage =
+                        new XmppAxolotlPlaintextMessage(
+                                Config.OMEMO_PADDING ? plaintext.trim() : plaintext,
+                                session.getFingerprint());
+
+            } catch (NoSuchAlgorithmException
+                    | NoSuchPaddingException
+                    | InvalidKeyException
+                    | InvalidAlgorithmParameterException
+                    | IllegalBlockSizeException
+                    | BadPaddingException
+                    | NoSuchProviderException e) {
                 throw new CryptoFailedException(e);
             }
         }
@@ -287,7 +314,6 @@ public class XmppAxolotlMessage {
             return plaintext;
         }
 
-
         public String getFingerprint() {
             return fingerprint;
         }

src/main/java/eu/siacs/conversations/entities/Edit.java 🔗

@@ -1,12 +1,12 @@
 package eu.siacs.conversations.entities;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.util.ArrayList;
-import java.util.List;
-
 public class Edit {
 
     private final String editedId;
@@ -45,7 +45,8 @@ public class Edit {
 
     private static Edit fromJson(JSONObject jsonObject) throws JSONException {
         String edited = jsonObject.has("edited_id") ? jsonObject.getString("edited_id") : null;
-        String serverMsgId = jsonObject.has("server_msg_id") ? jsonObject.getString("server_msg_id") : null;
+        String serverMsgId =
+                jsonObject.has("server_msg_id") ? jsonObject.getString("server_msg_id") : null;
         return new Edit(edited, serverMsgId);
     }
 
@@ -83,9 +84,8 @@ public class Edit {
 
         Edit edit = (Edit) o;
 
-        if (editedId != null ? !editedId.equals(edit.editedId) : edit.editedId != null)
-            return false;
-        return serverMsgId != null ? serverMsgId.equals(edit.serverMsgId) : edit.serverMsgId == null;
+        if (!Objects.equals(editedId, edit.editedId)) return false;
+        return Objects.equals(serverMsgId, edit.serverMsgId);
     }
 
     @Override

src/main/java/eu/siacs/conversations/entities/MucOptions.java 🔗

@@ -33,6 +33,7 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Set;
 
 public class MucOptions {
@@ -1094,9 +1095,8 @@ public class MucOptions {
 
             if (role != user.role) return false;
             if (affiliation != user.affiliation) return false;
-            if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
-                return false;
-            return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
+            if (!Objects.equals(realJid, user.realJid)) return false;
+            return Objects.equals(fullJid, user.fullJid);
         }
 
         public boolean isDomain() {

src/main/java/eu/siacs/conversations/entities/PresenceTemplate.java 🔗

@@ -2,80 +2,77 @@ package eu.siacs.conversations.entities;
 
 import android.content.ContentValues;
 import android.database.Cursor;
-
+import java.util.Objects;
 
 public class PresenceTemplate extends AbstractEntity {
 
-	public static final String TABELNAME = "presence_templates";
-	public static final String LAST_USED = "last_used";
-	public static final String MESSAGE = "message";
-	public static final String STATUS = "status";
-
-	private long lastUsed = 0;
-	private String statusMessage;
-	private Presence.Status status = Presence.Status.ONLINE;
-
-	public PresenceTemplate(Presence.Status status, String statusMessage) {
-		this.status = status;
-		this.statusMessage = statusMessage;
-		this.lastUsed = System.currentTimeMillis();
-		this.uuid = java.util.UUID.randomUUID().toString();
-	}
-
-	private PresenceTemplate() {
-
-	}
-
-	@Override
-	public ContentValues getContentValues() {
-		final String show = status.toShowString();
-		ContentValues values = new ContentValues();
-		values.put(LAST_USED, lastUsed);
-		values.put(MESSAGE, statusMessage);
-		values.put(STATUS, show == null ? "" : show);
-		values.put(UUID, uuid);
-		return values;
-	}
-
-	public static PresenceTemplate fromCursor(Cursor cursor) {
-		PresenceTemplate template = new PresenceTemplate();
-		template.uuid = cursor.getString(cursor.getColumnIndex(UUID));
-		template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
-		template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
-		template.status = Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
-		return template;
-	}
-
-	public Presence.Status getStatus() {
-		return status;
-	}
-
-	public String getStatusMessage() {
-		return statusMessage;
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		PresenceTemplate template = (PresenceTemplate) o;
-
-		if (statusMessage != null ? !statusMessage.equals(template.statusMessage) : template.statusMessage != null)
-			return false;
-		return status == template.status;
-
-	}
-
-	@Override
-	public int hashCode() {
-		int result = statusMessage != null ? statusMessage.hashCode() : 0;
-		result = 31 * result + status.hashCode();
-		return result;
-	}
-
-	@Override
-	public String toString() {
-		return statusMessage;
-	}
+    public static final String TABELNAME = "presence_templates";
+    public static final String LAST_USED = "last_used";
+    public static final String MESSAGE = "message";
+    public static final String STATUS = "status";
+
+    private long lastUsed = 0;
+    private String statusMessage;
+    private Presence.Status status = Presence.Status.ONLINE;
+
+    public PresenceTemplate(Presence.Status status, String statusMessage) {
+        this.status = status;
+        this.statusMessage = statusMessage;
+        this.lastUsed = System.currentTimeMillis();
+        this.uuid = java.util.UUID.randomUUID().toString();
+    }
+
+    private PresenceTemplate() {}
+
+    @Override
+    public ContentValues getContentValues() {
+        final String show = status.toShowString();
+        ContentValues values = new ContentValues();
+        values.put(LAST_USED, lastUsed);
+        values.put(MESSAGE, statusMessage);
+        values.put(STATUS, show == null ? "" : show);
+        values.put(UUID, uuid);
+        return values;
+    }
+
+    public static PresenceTemplate fromCursor(Cursor cursor) {
+        PresenceTemplate template = new PresenceTemplate();
+        template.uuid = cursor.getString(cursor.getColumnIndex(UUID));
+        template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
+        template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
+        template.status =
+                Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
+        return template;
+    }
+
+    public Presence.Status getStatus() {
+        return status;
+    }
+
+    public String getStatusMessage() {
+        return statusMessage;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        PresenceTemplate template = (PresenceTemplate) o;
+
+        if (!Objects.equals(statusMessage, template.statusMessage)) return false;
+        return status == template.status;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = statusMessage != null ? statusMessage.hashCode() : 0;
+        result = 31 * result + status.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return statusMessage;
+    }
 }

src/main/java/eu/siacs/conversations/entities/ReadByMarker.java 🔗

@@ -1,171 +1,167 @@
 package eu.siacs.conversations.entities;
 
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
+import eu.siacs.conversations.xmpp.Jid;
 import java.util.Collection;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
-
-import eu.siacs.conversations.xmpp.Jid;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
 
 public class ReadByMarker {
 
-	private ReadByMarker() {
-
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (this == o) return true;
-		if (o == null || getClass() != o.getClass()) return false;
-
-		ReadByMarker marker = (ReadByMarker) o;
-
-		if (fullJid != null ? !fullJid.equals(marker.fullJid) : marker.fullJid != null)
-			return false;
-		return realJid != null ? realJid.equals(marker.realJid) : marker.realJid == null;
-
-	}
-
-	@Override
-	public int hashCode() {
-		int result = fullJid != null ? fullJid.hashCode() : 0;
-		result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
-		return result;
-	}
-
-	private Jid fullJid;
-	private Jid realJid;
-
-	public Jid getFullJid() {
-		return fullJid;
-	}
-
-	public Jid getRealJid() {
-		return realJid;
-	}
-
-	public JSONObject toJson() {
-		JSONObject jsonObject = new JSONObject();
-		if (fullJid != null) {
-			try {
-				jsonObject.put("fullJid", fullJid.toString());
-			} catch (JSONException e) {
-				//ignore
-			}
-		}
-		if (realJid != null) {
-			try {
-				jsonObject.put("realJid", realJid.toString());
-			} catch (JSONException e) {
-				//ignore
-			}
-		}
-		return jsonObject;
-	}
-
-	public static Set<ReadByMarker> fromJson(final JSONArray jsonArray) {
-		final Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
-		for(int i = 0; i < jsonArray.length(); ++i) {
-			try {
-				readByMarkers.add(fromJson(jsonArray.getJSONObject(i)));
-			} catch (JSONException e) {
-				//ignored
-			}
-		}
-		return readByMarkers;
-	}
-
-	public static ReadByMarker from(Jid fullJid, Jid realJid) {
-		final ReadByMarker marker = new ReadByMarker();
-		marker.fullJid = fullJid;
-		marker.realJid = realJid == null ? null : realJid.asBareJid();
-		return marker;
-	}
-
-	public static ReadByMarker from(Message message) {
-		final ReadByMarker marker = new ReadByMarker();
-		marker.fullJid = message.getCounterpart();
-		marker.realJid = message.getTrueCounterpart();
-		return marker;
-	}
-
-	public static ReadByMarker from(MucOptions.User user) {
-		final ReadByMarker marker = new ReadByMarker();
-		marker.fullJid = user.getFullJid();
-		marker.realJid = user.getRealJid();
-		return marker;
-	}
-
-	public static Set<ReadByMarker> from(Collection<MucOptions.User> users) {
-		final Set<ReadByMarker> markers = new CopyOnWriteArraySet<>();
-		for(MucOptions.User user : users) {
-			markers.add(from(user));
-		}
-		return markers;
-	}
-
-	public static ReadByMarker fromJson(JSONObject jsonObject) {
-		ReadByMarker marker = new ReadByMarker();
-		try {
-			marker.fullJid = Jid.of(jsonObject.getString("fullJid"));
-		} catch (JSONException | IllegalArgumentException e) {
-			marker.fullJid = null;
-		}
-		try {
-			marker.realJid = Jid.of(jsonObject.getString("realJid"));
-		} catch (JSONException | IllegalArgumentException e) {
-			marker.realJid = null;
-		}
-		return marker;
-	}
-
-	public static Set<ReadByMarker> fromJsonString(String json) {
-		try {
-			return fromJson(new JSONArray(json));
-		} catch (final JSONException | NullPointerException e) {
-			return new CopyOnWriteArraySet<>();
-		}
-	}
-
-	public static JSONArray toJson(final Set<ReadByMarker> readByMarkers) {
-		final JSONArray jsonArray = new JSONArray();
-		for(final ReadByMarker marker : readByMarkers) {
-			jsonArray.put(marker.toJson());
-		}
-		return jsonArray;
-	}
-
-	public static boolean contains(ReadByMarker needle, final Set<ReadByMarker> readByMarkers) {
-		for(final ReadByMarker marker : readByMarkers) {
-			if (marker.realJid != null && needle.realJid != null) {
-				if (marker.realJid.asBareJid().equals(needle.realJid.asBareJid())) {
-					return true;
-				}
-			} else if (marker.fullJid != null && needle.fullJid != null) {
-				if (marker.fullJid.equals(needle.fullJid)) {
-					return true;
-				}
-			}
-		}
-		return false;
-	}
-
-	public static boolean allUsersRepresented(Collection<MucOptions.User> users, Set<ReadByMarker> markers) {
-		for(MucOptions.User user : users) {
-			if (!contains(from(user),markers)) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	public static boolean allUsersRepresented(Collection<MucOptions.User> users, Set<ReadByMarker> markers, ReadByMarker marker) {
-		final Set<ReadByMarker> markersCopy = new CopyOnWriteArraySet<>(markers);
-		markersCopy.add(marker);
-		return allUsersRepresented(users, markersCopy);
-	}
-
+    private ReadByMarker() {}
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        ReadByMarker marker = (ReadByMarker) o;
+
+        if (!Objects.equals(fullJid, marker.fullJid)) return false;
+        return Objects.equals(realJid, marker.realJid);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = fullJid != null ? fullJid.hashCode() : 0;
+        result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
+        return result;
+    }
+
+    private Jid fullJid;
+    private Jid realJid;
+
+    public Jid getFullJid() {
+        return fullJid;
+    }
+
+    public Jid getRealJid() {
+        return realJid;
+    }
+
+    public JSONObject toJson() {
+        JSONObject jsonObject = new JSONObject();
+        if (fullJid != null) {
+            try {
+                jsonObject.put("fullJid", fullJid.toString());
+            } catch (JSONException e) {
+                // ignore
+            }
+        }
+        if (realJid != null) {
+            try {
+                jsonObject.put("realJid", realJid.toString());
+            } catch (JSONException e) {
+                // ignore
+            }
+        }
+        return jsonObject;
+    }
+
+    public static Set<ReadByMarker> fromJson(final JSONArray jsonArray) {
+        final Set<ReadByMarker> readByMarkers = new CopyOnWriteArraySet<>();
+        for (int i = 0; i < jsonArray.length(); ++i) {
+            try {
+                readByMarkers.add(fromJson(jsonArray.getJSONObject(i)));
+            } catch (JSONException e) {
+                // ignored
+            }
+        }
+        return readByMarkers;
+    }
+
+    public static ReadByMarker from(Jid fullJid, Jid realJid) {
+        final ReadByMarker marker = new ReadByMarker();
+        marker.fullJid = fullJid;
+        marker.realJid = realJid == null ? null : realJid.asBareJid();
+        return marker;
+    }
+
+    public static ReadByMarker from(Message message) {
+        final ReadByMarker marker = new ReadByMarker();
+        marker.fullJid = message.getCounterpart();
+        marker.realJid = message.getTrueCounterpart();
+        return marker;
+    }
+
+    public static ReadByMarker from(MucOptions.User user) {
+        final ReadByMarker marker = new ReadByMarker();
+        marker.fullJid = user.getFullJid();
+        marker.realJid = user.getRealJid();
+        return marker;
+    }
+
+    public static Set<ReadByMarker> from(Collection<MucOptions.User> users) {
+        final Set<ReadByMarker> markers = new CopyOnWriteArraySet<>();
+        for (MucOptions.User user : users) {
+            markers.add(from(user));
+        }
+        return markers;
+    }
+
+    public static ReadByMarker fromJson(JSONObject jsonObject) {
+        ReadByMarker marker = new ReadByMarker();
+        try {
+            marker.fullJid = Jid.of(jsonObject.getString("fullJid"));
+        } catch (JSONException | IllegalArgumentException e) {
+            marker.fullJid = null;
+        }
+        try {
+            marker.realJid = Jid.of(jsonObject.getString("realJid"));
+        } catch (JSONException | IllegalArgumentException e) {
+            marker.realJid = null;
+        }
+        return marker;
+    }
+
+    public static Set<ReadByMarker> fromJsonString(String json) {
+        try {
+            return fromJson(new JSONArray(json));
+        } catch (final JSONException | NullPointerException e) {
+            return new CopyOnWriteArraySet<>();
+        }
+    }
+
+    public static JSONArray toJson(final Set<ReadByMarker> readByMarkers) {
+        final JSONArray jsonArray = new JSONArray();
+        for (final ReadByMarker marker : readByMarkers) {
+            jsonArray.put(marker.toJson());
+        }
+        return jsonArray;
+    }
+
+    public static boolean contains(ReadByMarker needle, final Set<ReadByMarker> readByMarkers) {
+        for (final ReadByMarker marker : readByMarkers) {
+            if (marker.realJid != null && needle.realJid != null) {
+                if (marker.realJid.asBareJid().equals(needle.realJid.asBareJid())) {
+                    return true;
+                }
+            } else if (marker.fullJid != null && needle.fullJid != null) {
+                if (marker.fullJid.equals(needle.fullJid)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public static boolean allUsersRepresented(
+            Collection<MucOptions.User> users, Set<ReadByMarker> markers) {
+        for (MucOptions.User user : users) {
+            if (!contains(from(user), markers)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean allUsersRepresented(
+            Collection<MucOptions.User> users, Set<ReadByMarker> markers, ReadByMarker marker) {
+        final Set<ReadByMarker> markersCopy = new CopyOnWriteArraySet<>(markers);
+        markersCopy.add(marker);
+        return allUsersRepresented(users, markersCopy);
+    }
 }

src/main/java/eu/siacs/conversations/generator/IqGenerator.java 🔗

@@ -18,6 +18,22 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord;
 import java.io.ByteArrayOutputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Bookmark;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.services.MessageArchiveService;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.pep.Avatar;
+import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.upload.Request;
 import java.nio.ByteBuffer;
 import java.security.cert.CertificateEncodingException;
 import java.security.cert.X509Certificate;
@@ -494,13 +510,13 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public Iq requestHttpUploadLegacySlot(Jid host, DownloadableFile file, String mime) {
+    public Iq requestHttpUploadSlot(
+            final Jid host, final DownloadableFile file, final String mime) {
         final Iq packet = new Iq(Iq.Type.GET);
         packet.setTo(host);
-        Element request = packet.addChild("request", Namespace.HTTP_UPLOAD_LEGACY);
-        request.addChild("filename").setContent(convertFilename(file.getName()));
-        request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
-        request.addChild("content-type").setContent(mime);
+        final var request = packet.addExtension(new Request());
+        request.setFilename(convertFilename(file.getName()));
+        request.setSize(file.getExpectedSize());
         return packet;
     }
 

src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java 🔗

@@ -127,10 +127,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
                 }
             }
             HttpUploadConnection connection =
-                    new HttpUploadConnection(
-                            message,
-                            Method.determine(message.getConversation().getAccount()),
-                            this, cb);
+                    new HttpUploadConnection(message, this, cb);
             connection.init(delay);
             this.uploadConnections.add(connection);
         }

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java 🔗

@@ -3,20 +3,12 @@ package eu.siacs.conversations.http;
 import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
 import android.util.Log;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Future;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.DownloadableFile;
@@ -25,6 +17,10 @@ import eu.siacs.conversations.entities.Transferable;
 import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Future;
 import okhttp3.Call;
 import okhttp3.Callback;
 import okhttp3.OkHttpClient;
@@ -32,17 +28,14 @@ import okhttp3.Request;
 import okhttp3.RequestBody;
 import okhttp3.Response;
 
-public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
+public class HttpUploadConnection
+        implements Transferable, AbstractConnectionManager.ProgressListener {
 
-    static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
-            "Authorization",
-            "Cookie",
-            "Expires"
-    );
+    static final List<String> WHITE_LISTED_HEADERS =
+            Arrays.asList("Authorization", "Cookie", "Expires");
 
     private final HttpConnectionManager mHttpConnectionManager;
     private final XmppConnectionService mXmppConnectionService;
-    private final Method method;
     private boolean delayed = false;
     private DownloadableFile file;
     private final Message message;
@@ -54,9 +47,8 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
     private ListenableFuture<SlotRequester.Slot> slotFuture;
     private Runnable cb;
 
-    public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager, Runnable cb) {
+    public HttpUploadConnection(final Message message, final HttpConnectionManager httpConnectionManager, final Runnable cb) {
         this.message = message;
-        this.method = method;
         this.mHttpConnectionManager = httpConnectionManager;
         this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
         this.cb = cb;
@@ -90,13 +82,13 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
         final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
         if (slotFuture != null && !slotFuture.isDone()) {
             if (slotFuture.cancel(true)) {
-                Log.d(Config.LOGTAG,"cancelled slot requester");
+                Log.d(Config.LOGTAG, "cancelled slot requester");
             }
         }
         final Call call = this.mostRecentCall;
         if (call != null && !call.isCanceled()) {
             call.cancel();
-            Log.d(Config.LOGTAG,"cancelled HTTP request");
+            Log.d(Config.LOGTAG, "cancelled HTTP request");
         }
     }
 
@@ -104,8 +96,13 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
         finish();
         final Call call = this.mostRecentCall;
         final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
-        final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled());
-        mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
+        final boolean cancelled =
+                (call != null && call.isCanceled())
+                        || (slotFuture != null && slotFuture.isCancelled());
+        mXmppConnectionService.markMessage(
+                message,
+                Message.STATUS_SEND_FAILED,
+                cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
         if (cb != null) cb.run();
     }
 
@@ -118,7 +115,8 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
         final Account account = message.getConversation().getAccount();
         this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
         final String mime;
-        if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+        if (message.getEncryption() == Message.ENCRYPTION_PGP
+                || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
             mime = "application/pgp-encrypted";
         } else {
             mime = this.file.getMimeType();
@@ -132,75 +130,86 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
             this.file.setKeyAndIv(this.key);
         }
         this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
-        this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, message.getFileParams().getName(), mime);
-        Futures.addCallback(this.slotFuture, new FutureCallback<SlotRequester.Slot>() {
-            @Override
-            public void onSuccess(@Nullable SlotRequester.Slot result) {
-                HttpUploadConnection.this.slot = result;
-                try {
-                    HttpUploadConnection.this.upload();
-                } catch (final Exception e) {
-                    fail(e.getMessage());
-                }
-            }
+        message.resetFileParams();
+        this.slotFuture = new SlotRequester(mXmppConnectionService).request(account, file, mime);
+        Futures.addCallback(
+                this.slotFuture,
+                new FutureCallback<>() {
+                    @Override
+                    public void onSuccess(@Nullable SlotRequester.Slot result) {
+                        HttpUploadConnection.this.slot = result;
+                        try {
+                            HttpUploadConnection.this.upload();
+                        } catch (final Exception e) {
+                            fail(e.getMessage());
+                        }
+                    }
 
-            @Override
-            public void onFailure(@NonNull final Throwable throwable) {
-                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
-                // TODO consider fall back to jingle in 1-on-1 chats with exactly one online presence
-                fail(throwable.getMessage());
-            }
-        }, MoreExecutors.directExecutor());
+                    @Override
+                    public void onFailure(@NonNull final Throwable throwable) {
+                        Log.d(
+                                Config.LOGTAG,
+                                account.getJid().asBareJid() + ": unable to request slot",
+                                throwable);
+                        // TODO consider fall back to jingle in 1-on-1 chats with exactly one online
+                        // presence
+                        fail(throwable.getMessage());
+                    }
+                },
+                MoreExecutors.directExecutor());
         message.setTransferable(this);
         mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
     }
 
     private void upload() {
-        final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
-                slot.put,
-                message.getConversation().getAccount(),
-                0,
-                true
-        );
+        final OkHttpClient client =
+                mHttpConnectionManager.buildHttpClient(
+                        slot.put, message.getConversation().getAccount(), 0, true);
         final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
-        final Request request = new Request.Builder()
-                .url(slot.put)
-                .put(requestBody)
-                .headers(slot.headers)
-                .build();
+        final Request request =
+                new Request.Builder().url(slot.put).put(requestBody).headers(slot.headers).build();
         Log.d(Config.LOGTAG, "uploading file to " + slot.put);
         this.mostRecentCall = client.newCall(request);
-        this.mostRecentCall.enqueue(new Callback() {
-            @Override
-            public void onFailure(@NonNull Call call, IOException e) {
-                Log.d(Config.LOGTAG, "http upload failed", e);
-                fail(e.getMessage());
-            }
-
-            @Override
-            public void onResponse(@NonNull Call call, @NonNull Response response)  {
-                final int code = response.code();
-                if (code == 200 || code == 201) {
-                    Log.d(Config.LOGTAG, "finished uploading file");
-                    final String get;
-                    if (key != null) {
-                        get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
-                    } else {
-                        get = slot.get.toString();
+        this.mostRecentCall.enqueue(
+                new Callback() {
+                    @Override
+                    public void onFailure(@NonNull Call call, IOException e) {
+                        Log.d(Config.LOGTAG, "http upload failed", e);
+                        fail(e.getMessage());
                     }
-                    mXmppConnectionService.getFileBackend().updateFileParams(message, get);
-                    mXmppConnectionService.getFileBackend().updateMediaScanner(file);
-                    finish();
-                    if (!message.isPrivateMessage()) {
-                        message.setCounterpart(message.getConversation().getJid().asBareJid());
+
+                    @Override
+                    public void onResponse(@NonNull Call call, @NonNull Response response) {
+                        final int code = response.code();
+                        if (code == 200 || code == 201) {
+                            Log.d(Config.LOGTAG, "finished uploading file");
+                            final String get;
+                            if (key != null) {
+                                get =
+                                        AesGcmURL.toAesGcmUrl(
+                                                slot.get
+                                                        .newBuilder()
+                                                        .fragment(CryptoHelper.bytesToHex(key))
+                                                        .build());
+                            } else {
+                                get = slot.get.toString();
+                            }
+                            mXmppConnectionService.getFileBackend().updateFileParams(message, get);
+                            mXmppConnectionService.getFileBackend().updateMediaScanner(file);
+                            finish();
+                            if (!message.isPrivateMessage()) {
+                                message.setCounterpart(
+                                        message.getConversation().getJid().asBareJid());
+                            }
+                            mXmppConnectionService.resendMessage(message, delayed, cb);
+                        } else {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    "http upload failed because response code was " + code);
+                            fail("http upload failed because response code was " + code);
+                        }
                     }
-                    mXmppConnectionService.resendMessage(message, delayed, cb);
-                } else {
-                    Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
-                    fail("http upload failed because response code was " + code);
-                }
-            }
-        });
+                });
     }
 
     public Message getMessage() {

src/main/java/eu/siacs/conversations/http/Method.java 🔗

@@ -1,51 +0,0 @@
-/*
- * Copyright (c) 2018, Daniel Gultsch All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation and/or
- * other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors
- * may be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package eu.siacs.conversations.http;
-
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.xmpp.XmppConnection;
-
-public enum  Method {
-	HTTP_UPLOAD, HTTP_UPLOAD_LEGACY;
-
-	public static Method determine(Account account) {
-		XmppConnection.Features features = account.getXmppConnection() == null ? null : account.getXmppConnection().getFeatures();
-		if (features == null) {
-			return HTTP_UPLOAD;
-		}
-		if (features.useLegacyHttpUpload()) {
-			return HTTP_UPLOAD_LEGACY;
-		} else if (features.httpUpload(0)) {
-			return HTTP_UPLOAD;
-		} else {
-			return HTTP_UPLOAD;
-		}
-	}
-}

src/main/java/eu/siacs/conversations/http/SlotRequester.java 🔗

@@ -29,21 +29,22 @@
 
 package eu.siacs.conversations.http;
 
+import android.util.Log;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.SettableFuture;
-
-import java.util.Map;
-
+import com.google.common.util.concurrent.MoreExecutors;
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.parser.IqParser;
 import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
-import eu.siacs.conversations.xmpp.IqResponseException;
 import eu.siacs.conversations.xmpp.Jid;
 import im.conversations.android.xmpp.model.stanza.Iq;
+import im.conversations.android.xmpp.model.upload.Header;
+import im.conversations.android.xmpp.model.upload.Slot;
+import java.util.Map;
 import okhttp3.Headers;
 import okhttp3.HttpUrl;
 
@@ -55,83 +56,54 @@ public class SlotRequester {
         this.service = service;
     }
 
-    public ListenableFuture<Slot> request(Method method, Account account, DownloadableFile file, String name, String mime) {
-        if (method == Method.HTTP_UPLOAD_LEGACY) {
-            final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY);
-            return requestHttpUploadLegacy(account, host, file, mime);
-        } else {
-            final Jid host = account.getXmppConnection().findDiscoItemByFeature(Namespace.HTTP_UPLOAD);
-            return requestHttpUpload(account, host, file, name, mime);
+    public ListenableFuture<Slot> request(
+            final Account account, final DownloadableFile file, final String mime) {
+        final var result =
+                account.getXmppConnection()
+                        .getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+        if (result == null) {
+            return Futures.immediateFailedFuture(
+                    new IllegalStateException("No HTTP upload host found"));
         }
+        return requestHttpUpload(account, result.getKey(), file, mime);
     }
 
-    private ListenableFuture<Slot> requestHttpUploadLegacy(Account account, Jid host, DownloadableFile file, String mime) {
-        final SettableFuture<Slot> future = SettableFuture.create();
-        final Iq request = service.getIqGenerator().requestHttpUploadLegacySlot(host, file, mime);
-        service.sendIqPacket(account, request, (packet) -> {
-            if (packet.getType() == Iq.Type.RESULT) {
-                final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD_LEGACY);
-                if (slotElement != null) {
-                    try {
-                        final String putUrl = slotElement.findChildContent("put");
-                        final String getUrl = slotElement.findChildContent("get");
-                        if (getUrl != null && putUrl != null) {
-                            final Slot slot = new Slot(
-                                    HttpUrl.get(putUrl),
-                                    HttpUrl.get(getUrl),
-                                    Headers.of("Content-Type", mime == null ? "application/octet-stream" : mime)
-                            );
-                            future.set(slot);
-                            return;
-                        }
-                    } catch (final IllegalArgumentException e) {
-                        future.setException(e);
-                        return;
+    private ListenableFuture<Slot> requestHttpUpload(
+            final Account account, final Jid host, final DownloadableFile file, final String mime) {
+        final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, mime);
+        final var iqFuture = service.sendIqPacket(account, request);
+        return Futures.transform(
+                iqFuture,
+                response -> {
+                    final var slot =
+                            response.getExtension(
+                                    im.conversations.android.xmpp.model.upload.Slot.class);
+                    if (slot == null) {
+                        Log.d(Config.LOGTAG, "-->" + response);
+                        throw new IllegalStateException("Slot not found in IQ response");
                     }
-                }
-            }
-            future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
-        });
-        return future;
-    }
-
-    private ListenableFuture<Slot> requestHttpUpload(Account account, Jid host, DownloadableFile file, String fname, String mime) {
-        final SettableFuture<Slot> future = SettableFuture.create();
-        final Iq request = service.getIqGenerator().requestHttpUploadSlot(host, file, fname, mime);
-        service.sendIqPacket(account, request, (packet) -> {
-            if (packet.getType() == Iq.Type.RESULT) {
-                final Element slotElement = packet.findChild("slot", Namespace.HTTP_UPLOAD);
-                if (slotElement != null) {
-                    try {
-                        final Element put = slotElement.findChild("put");
-                        final Element get = slotElement.findChild("get");
-                        final String putUrl = put == null ? null : put.getAttribute("url");
-                        final String getUrl = get == null ? null : get.getAttribute("url");
-                        if (getUrl != null && putUrl != null) {
-                            final ImmutableMap.Builder<String, String> headers = new ImmutableMap.Builder<>();
-                            for (final Element child : put.getChildren()) {
-                                if ("header".equals(child.getName())) {
-                                    final String name = child.getAttribute("name");
-                                    final String value = child.getContent();
-                                    if (HttpUploadConnection.WHITE_LISTED_HEADERS.contains(name) && value != null && !value.trim().contains("\n")) {
-                                        headers.put(name, value.trim());
-                                    }
-                                }
-                            }
-                            headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
-                            final Slot slot = new Slot(HttpUrl.get(putUrl), HttpUrl.get(getUrl), headers.build());
-                            future.set(slot);
-                            return;
+                    final var getUrl = slot.getGetUrl();
+                    final var put = slot.getPut();
+                    if (getUrl == null || put == null) {
+                        throw new IllegalStateException("Missing get or put in slot response");
+                    }
+                    final var putUrl = put.getUrl();
+                    if (putUrl == null) {
+                        throw new IllegalStateException("Missing put url");
+                    }
+                    final var headers = new ImmutableMap.Builder<String, String>();
+                    for (final Header header : put.getHeaders()) {
+                        final String name = header.getHeaderName();
+                        final String value = header.getContent();
+                        if (Strings.isNullOrEmpty(value) || value.contains("\n")) {
+                            continue;
                         }
-                    } catch (final IllegalArgumentException e) {
-                        future.setException(e);
-                        return;
+                        headers.put(name, value.trim());
                     }
-                }
-            }
-            future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
-        });
-        return future;
+                    headers.put("Content-Type", mime == null ? "application/octet-stream" : mime);
+                    return new Slot(putUrl, getUrl, headers.buildKeepingLast());
+                },
+                MoreExecutors.directExecutor());
     }
 
     public static class Slot {

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

@@ -204,7 +204,7 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
                                         + "Encountered invalid <device> node in PEP ("
                                         + e.getMessage()
                                         + "):"
-                                        + device.toString()
+                                        + device
                                         + ", skipping...");
                     }
                 }
@@ -328,7 +328,7 @@ public class IqParser extends AbstractParser implements Consumer<Iq> {
                         AxolotlService.LOGPREFIX
                                 + " : "
                                 + "could not parse preKeyId from preKey "
-                                + preKeyPublicElement.toString());
+                                + preKeyPublicElement);
             } catch (Throwable e) {
                 Log.e(
                         Config.LOGTAG,

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

@@ -361,7 +361,6 @@ public class MessageParser extends AbstractParser
             }
         } else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
             final var retractions = items.getRetractions();
-            ;
             for (final var item : items.getItemMap(Conference.class).entrySet()) {
                 final Bookmark bookmark =
                         Bookmark.parseFromItem(item.getKey(), item.getValue(), account);

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -390,7 +390,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         final SQLiteDatabase db = getWritableDatabase();
         final Stopwatch stopwatch = Stopwatch.createStarted();
         db.execSQL(COPY_PREEXISTING_ENTRIES);
-        Log.d(Config.LOGTAG, "rebuilt message index in " + stopwatch.stop().toString());
+        Log.d(Config.LOGTAG, "rebuilt message index in " + stopwatch.stop());
     }
 
     public static synchronized DatabaseBackend getInstance(Context context) {
@@ -1978,7 +1978,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
                         + " is not null and conversationUuid=(select uuid from conversations where"
                         + " accountUuid=? and (contactJid=? or contactJid like ?)) order by"
                         + " timeSent desc";
-        final String[] args = {account, jid.toString(), jid.toString() + "/%"};
+        final String[] args = {account, jid.toString(), jid + "/%"};
         Cursor cursor = db.rawQuery(SQL + (limit > 0 ? " limit " + limit : ""), args);
         List<FilePath> filesPaths = new ArrayList<>();
         while (cursor.moveToNext()) {

src/main/java/eu/siacs/conversations/persistance/FileBackend.java 🔗

@@ -1722,7 +1722,7 @@ public class FileBackend {
             Log.d(Config.LOGTAG, "settled on char length " + chars + " with quality=" + quality);
             final Avatar avatar = new Avatar();
             avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
-            avatar.image = new String(mByteArrayOutputStream.toByteArray());
+            avatar.image = mByteArrayOutputStream.toString();
             if (format.equals(Bitmap.CompressFormat.WEBP)) {
                 avatar.type = "image/webp";
             } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
@@ -1767,7 +1767,7 @@ public class FileBackend {
             os.flush();
             os.close();
             avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
-            avatar.image = new String(mByteArrayOutputStream.toByteArray());
+            avatar.image = mByteArrayOutputStream.toString();
             avatar.height = options.outHeight;
             avatar.width = options.outWidth;
             avatar.type = options.outMimeType;

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

@@ -545,10 +545,7 @@ public class CallIntegration extends Connection {
             return false;
         }
         // SailfishOS's AppSupport do not support Call Integration
-        if (Build.MODEL.endsWith("(AppSupport)")) {
-            return false;
-        }
-        return true;
+        return !Build.MODEL.endsWith("(AppSupport)");
     }
 
     public static boolean notSelfManaged(final Context context) {

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

@@ -3,15 +3,7 @@ package eu.siacs.conversations.services;
 import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
 
 import android.util.Log;
-
 import androidx.annotation.NonNull;
-
-import java.math.BigInteger;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
@@ -25,6 +17,11 @@ import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
 import eu.siacs.conversations.xmpp.mam.MamReference;
 import im.conversations.android.xmpp.model.stanza.Iq;
 import im.conversations.android.xmpp.model.stanza.Message;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
 
 public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 
@@ -90,7 +87,6 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
             }
             return null;
         }
-
     }
 
     MessageArchiveService(final XmppConnectionService service) {
@@ -106,10 +102,13 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
                 }
             }
         }
-        MamReference mamReference = MamReference.max(
-                mXmppConnectionService.databaseBackend.getLastMessageReceived(account),
-                mXmppConnectionService.databaseBackend.getLastClearDate(account)
-        );
+        MamReference mamReference =
+                MamReference.max(
+                        mXmppConnectionService.databaseBackend.getLastMessageReceived(account),
+                        mXmppConnectionService.databaseBackend.getLastClearDate(account));
+        mamReference =
+                MamReference.max(
+                        mamReference, mXmppConnectionService.getAutomaticMessageDeletionDate());
         long endCatchup = account.getXmppConnection().getLastSessionEstablished();
         final Query query;
         if (mamReference.getTimestamp() == 0) {
@@ -118,7 +117,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
             long startCatchup = endCatchup - Config.MAM_MAX_CATCHUP;
             List<Conversation> conversations = mXmppConnectionService.getConversations();
             for (Conversation conversation : conversations) {
-                if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted().getTimestamp()) {
+                if (conversation.getMode() == Conversation.MODE_SINGLE
+                        && conversation.getAccount() == account
+                        && startCatchup > conversation.getLastMessageTransmitted().getTimestamp()) {
                     this.query(conversation, startCatchup, true);
                 }
             }
@@ -133,27 +134,21 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     void catchupMUC(final Conversation conversation) {
-        if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) {
-            query(conversation,
-                    new MamReference(0),
-                    0,
-                    true);
+        if (conversation.getLastMessageTransmitted().getTimestamp() < 0
+                && conversation.countMessages() == 0) {
+            query(conversation, new MamReference(0), 0, true);
         } else {
-            query(conversation,
-                    conversation.getLastMessageTransmitted(),
-                    0,
-                    true);
+            query(conversation, conversation.getLastMessageTransmitted(), 0, true);
         }
     }
 
     public Query query(final Conversation conversation) {
-        if (conversation.getLastMessageTransmitted().getTimestamp() < 0 && conversation.countMessages() == 0) {
-            return query(conversation,
-                    new MamReference(0),
-                    System.currentTimeMillis(),
-                    false);
+        if (conversation.getLastMessageTransmitted().getTimestamp() < 0
+                && conversation.countMessages() == 0) {
+            return query(conversation, new MamReference(0), System.currentTimeMillis(), false);
         } else {
-            return query(conversation,
+            return query(
+                    conversation,
                     conversation.getLastMessageTransmitted(),
                     conversation.getAccount().getXmppConnection().getLastSessionEstablished(),
                     false);
@@ -167,7 +162,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         } else {
             synchronized (this.queries) {
                 for (Query query : this.queries) {
-                    if (query.getAccount() == account && query.isCatchup() && ((conversation.getMode() == Conversation.MODE_SINGLE && query.getWith() == null) || query.getConversation() == conversation)) {
+                    if (query.getAccount() == account
+                            && query.isCatchup()
+                            && ((conversation.getMode() == Conversation.MODE_SINGLE
+                                            && query.getWith() == null)
+                                    || query.getConversation() == conversation)) {
                         return true;
                     }
                 }
@@ -177,10 +176,12 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     public Query query(final Conversation conversation, long end, boolean allowCatchup) {
-        return this.query(conversation, conversation.getLastMessageTransmitted(), end, allowCatchup);
+        return this.query(
+                conversation, conversation.getLastMessageTransmitted(), end, allowCatchup);
     }
 
-    public Query query(Conversation conversation, MamReference start, long end, boolean allowCatchup) {
+    public Query query(
+            Conversation conversation, MamReference start, long end, boolean allowCatchup) {
         synchronized (this.queries) {
             final Query query;
             final MamReference startActual = start;
@@ -189,9 +190,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
                 query.reference = conversation.getFirstMamReference();
             } else {
                 if (allowCatchup) {
-                    MamReference maxCatchup = MamReference.max(startActual, System.currentTimeMillis() - Config.MAM_MAX_CATCHUP);
+                    MamReference maxCatchup =
+                            MamReference.max(
+                                    startActual,
+                                    System.currentTimeMillis() - Config.MAM_MAX_CATCHUP);
                     if (maxCatchup.greaterThan(startActual)) {
-                        Query reverseCatchup = new Query(conversation, startActual, maxCatchup.getTimestamp(), false);
+                        Query reverseCatchup =
+                                new Query(
+                                        conversation,
+                                        startActual,
+                                        maxCatchup.getTimestamp(),
+                                        false);
                         this.queries.add(reverseCatchup);
                         this.execute(reverseCatchup);
                     }
@@ -230,40 +239,57 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         if (account.getStatus() == Account.State.ONLINE) {
             final Conversation conversation = query.getConversation();
             if (conversation != null && conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
-                throw new IllegalStateException("Attempted to run MAM query for archived conversation");
-            }
-            Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": running mam query " + query.toString());
-            final Iq packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query);
-            this.mXmppConnectionService.sendIqPacket(account, packet, (p) -> {
-                final Element fin = p.findChild("fin", query.version.namespace);
-                if (p.getType() == Iq.Type.TIMEOUT) {
-                    synchronized (this.queries) {
-                        this.queries.remove(query);
-                        if (query.hasCallback()) {
-                            query.callback(false);
+                throw new IllegalStateException(
+                        "Attempted to run MAM query for archived conversation");
+            }
+            Log.d(
+                    Config.LOGTAG,
+                    account.getJid().asBareJid().toString() + ": running mam query " + query);
+            final Iq packet =
+                    this.mXmppConnectionService
+                            .getIqGenerator()
+                            .queryMessageArchiveManagement(query);
+            this.mXmppConnectionService.sendIqPacket(
+                    account,
+                    packet,
+                    (p) -> {
+                        final Element fin = p.findChild("fin", query.version.namespace);
+                        if (p.getType() == Iq.Type.TIMEOUT) {
+                            synchronized (this.queries) {
+                                this.queries.remove(query);
+                                if (query.hasCallback()) {
+                                    query.callback(false);
+                                }
+                            }
+                        } else if (p.getType() == Iq.Type.RESULT && fin != null) {
+                            final boolean running;
+                            synchronized (this.queries) {
+                                running = this.queries.contains(query);
+                            }
+                            if (running) {
+                                processFin(query, fin);
+                            } else {
+                                Log.d(
+                                        Config.LOGTAG,
+                                        account.getJid().asBareJid()
+                                                + ": ignoring MAM iq result because query had been"
+                                                + " killed");
+                            }
+                        } else if (p.getType() == Iq.Type.RESULT && query.isLegacy()) {
+                            // do nothing
+                        } else {
+                            Log.d(
+                                    Config.LOGTAG,
+                                    account.getJid().asBareJid().toString()
+                                            + ": error executing mam: "
+                                            + p);
+                            try {
+                                finalizeQuery(query, true);
+                            } catch (final IllegalStateException e) {
+                                // ignored
+                            }
                         }
-                    }
-                } else if (p.getType() == Iq.Type.RESULT && fin != null) {
-                    final boolean running;
-                    synchronized (this.queries) {
-                        running = this.queries.contains(query);
-                    }
-                    if (running) {
-                        processFin(query, fin);
-                    } else {
-                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring MAM iq result because query had been killed");
-                    }
-                } else if (p.getType() == Iq.Type.RESULT && query.isLegacy()) {
-                    //do nothing
-                } else {
-                    Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": error executing mam: " + p.toString());
-                    try {
-                        finalizeQuery(query, true);
-                    } catch (final IllegalStateException e) {
-                        //ignored
-                    }
-                }
-            });
+                    });
         } else {
             synchronized (this.pendingQueries) {
                 this.pendingQueries.add(query);
@@ -319,7 +345,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
             for (Query query : queries) {
                 if (query.account == conversation.getAccount() && query.isCatchup()) {
                     final Jid with = query.getWith() == null ? null : query.getWith().asBareJid();
-                    if ((conversation.getMode() == Conversational.MODE_SINGLE && with == null) || (conversation.getJid().asBareJid().equals(with))) {
+                    if ((conversation.getMode() == Conversational.MODE_SINGLE && with == null)
+                            || (conversation.getJid().asBareJid().equals(with))) {
                         return true;
                     }
                 }
@@ -328,7 +355,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         return false;
     }
 
-    boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) {
+    boolean queryInProgress(
+            Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) {
         synchronized (this.queries) {
             for (Query query : queries) {
                 if (query.conversation == conversation) {
@@ -360,12 +388,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         String count = set == null ? null : set.findChildContent("count");
         Element first = set == null ? null : set.findChild("first");
         Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
-        boolean abort = (!query.isCatchup() && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
+        boolean abort =
+                (!query.isCatchup() && query.getTotalCount() >= Config.PAGE_SIZE)
+                        || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
         if (query.getConversation() != null) {
             query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
         }
         if (complete || relevant == null || abort) {
-            //TODO: FIX done logic to look at complete. using count is probably unreliable because it can be ommited and doesn’t work with paging.
+            // TODO: FIX done logic to look at complete. using count is probably unreliable because
+            // it can be ommited and doesn’t work with paging.
             boolean done;
             if (query.isCatchup()) {
                 done = false;
@@ -383,9 +414,21 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
             done = done || (query.getActualMessageCount() == 0 && !query.isCatchup());
             this.finalizeQuery(query, done);
 
-            Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": finished mam after " + query.getTotalCount() + "(" + query.getActualMessageCount() + ") messages. messages left=" + !done + " count=" + count);
+            Log.d(
+                    Config.LOGTAG,
+                    query.getAccount().getJid().asBareJid()
+                            + ": finished mam after "
+                            + query.getTotalCount()
+                            + "("
+                            + query.getActualMessageCount()
+                            + ") messages. messages left="
+                            + !done
+                            + " count="
+                            + count);
             if (query.isCatchup() && query.getActualMessageCount() > 0) {
-                mXmppConnectionService.getNotificationService().finishBacklog(true, query.getAccount());
+                mXmppConnectionService
+                        .getNotificationService()
+                        .finishBacklog(true, query.getAccount());
             }
             if (query.isCatchup() && query.getPagingOrder() == PagingOrder.NORMAL && !complete && query.getConversation() != null) {
                 // Going forward we stopped without completing due to limits
@@ -415,11 +458,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
     void kill(final Conversation conversation) {
         final ArrayList<Query> toBeKilled = new ArrayList<>();
         synchronized (this.pendingQueries) {
-            for (final Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext(); ) {
+            for (final Iterator<Query> iterator = this.pendingQueries.iterator();
+                    iterator.hasNext(); ) {
                 final Query query = iterator.next();
                 if (query.getConversation() == conversation) {
                     iterator.remove();
-                    Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": killed pending MAM query for archived conversation");
+                    Log.d(
+                            Config.LOGTAG,
+                            conversation.getAccount().getJid().asBareJid()
+                                    + ": killed pending MAM query for archived conversation");
                 }
             }
         }
@@ -436,7 +483,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
     }
 
     private void kill(Query query) {
-        Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": killing mam query prematurely");
+        Log.d(
+                Config.LOGTAG,
+                query.getAccount().getJid().asBareJid() + ": killing mam query prematurely");
         query.callback = null;
         this.finalizeQuery(query, false);
         if (query.isCatchup() && query.getActualMessageCount() > 0) {
@@ -448,11 +497,20 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
     private void processPostponed(Query query) {
         query.account.getAxolotlService().processPostponed();
         query.pendingReceiptRequests.removeAll(query.receiptRequests);
-        Log.d(Config.LOGTAG, query.getAccount().getJid().asBareJid() + ": found " + query.pendingReceiptRequests.size() + " pending receipt requests");
+        Log.d(
+                Config.LOGTAG,
+                query.getAccount().getJid().asBareJid()
+                        + ": found "
+                        + query.pendingReceiptRequests.size()
+                        + " pending receipt requests");
         Iterator<ReceiptRequest> iterator = query.pendingReceiptRequests.iterator();
         while (iterator.hasNext()) {
             ReceiptRequest rr = iterator.next();
-            mXmppConnectionService.sendMessagePacket(query.account, mXmppConnectionService.getMessageGenerator().received(query.account, rr.getJid(), rr.getId()));
+            mXmppConnectionService.sendMessagePacket(
+                    query.account,
+                    mXmppConnectionService
+                            .getMessageGenerator()
+                            .received(query.account, rr.getJid(), rr.getId()));
             iterator.remove();
         }
     }
@@ -473,7 +531,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
 
     @Override
     public void onAdvancedStreamFeaturesAvailable(Account account) {
-        if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) {
+        if (account.getXmppConnection() != null
+                && account.getXmppConnection().getFeatures().mam()) {
             this.catchup(account);
         }
     }
@@ -500,14 +559,17 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         private boolean catchup = true;
         public final Version version;
 
-
         Query(Conversation conversation, MamReference start, long end, boolean catchup, PagingOrder order) {
             this(conversation, start, end, catchup);
             this.pagingOrder = order;
         }
 
         Query(Conversation conversation, MamReference start, long end, boolean catchup) {
-            this(conversation.getAccount(), Version.get(conversation.getAccount(), conversation), catchup ? start : start.timeOnly(), end);
+            this(
+                    conversation.getAccount(),
+                    Version.get(conversation.getAccount(), conversation),
+                    catchup ? start : start.timeOnly(),
+                    end);
             this.conversation = conversation;
             this.pagingOrder = catchup ? PagingOrder.NORMAL : PagingOrder.REVERSE;
             this.catchup = catchup;
@@ -530,7 +592,12 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
         }
 
         private Query page(String reference) {
-            Query query = new Query(this.account, this.version, new MamReference(this.start, reference), this.end);
+            Query query =
+                    new Query(
+                            this.account,
+                            this.version,
+                            new MamReference(this.start, reference),
+                            this.end);
             query.conversation = conversation;
             query.totalCount = totalCount;
             query.actualCount = actualCount;

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

@@ -117,6 +117,7 @@ import io.ipfs.cid.Cid;
 
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.Config;
@@ -234,6 +235,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
@@ -583,7 +585,7 @@ public class XmppConnectionService extends Service {
     private LruCache<String, Drawable> mDrawableCache;
     private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
     private final BroadcastReceiver mInternalRestrictedEventReceiver =
-            new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS));
+            new RestrictedEventReceiver(List.of(TorServiceUtils.ACTION_STATUS));
     private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
     private EmojiSearch emojiSearch = null;
 
@@ -5136,7 +5138,7 @@ public class XmppConnectionService extends Service {
                                         if (packet.getType() == Iq.Type.RESULT) {
                                             callback.onPushSucceeded();
                                         } else {
-                                            Log.d(Config.LOGTAG, "failed: " + packet.toString());
+                                            Log.d(Config.LOGTAG, "failed: " + packet);
                                             callback.onPushFailed();
                                         }
                                     }
@@ -5761,7 +5763,7 @@ public class XmppConnectionService extends Service {
                         if (error == null) {
                             Log.d(Config.LOGTAG, ERROR + "(server error)");
                         } else {
-                            Log.d(Config.LOGTAG, ERROR + error.toString());
+                            Log.d(Config.LOGTAG, ERROR + error);
                         }
                     }
                     if (callback != null) {
@@ -6671,6 +6673,14 @@ public class XmppConnectionService extends Service {
         connection.sendCreateAccountWithCaptchaPacket(id, data);
     }
 
+    public ListenableFuture<Iq> sendIqPacket(final Account account, final Iq request) {
+        final XmppConnection connection = account.getXmppConnection();
+        if (connection == null) {
+            return Futures.immediateFailedFuture(new TimeoutException());
+        }
+        return connection.sendIqPacket(request);
+    }
+
     public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
         sendIqPacket(account, packet, callback, null);
     }

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

@@ -604,8 +604,7 @@ public class ConversationFragment extends XmppFragment
                 @Override
                 public void onClick(View v) {
                     Object tag = v.getTag();
-                    if (tag instanceof SendButtonAction) {
-                        SendButtonAction action = (SendButtonAction) tag;
+                    if (tag instanceof SendButtonAction action) {
                         switch (action) {
                             case TAKE_PHOTO:
                             case RECORD_VIDEO:

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

@@ -4,20 +4,15 @@ import android.app.Dialog;
 import android.content.Context;
 import android.os.Bundle;
 import android.widget.AutoCompleteTextView;
-
 import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.DialogFragment;
-
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import java.util.ArrayList;
-import java.util.List;
-
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.DialogCreateConferenceBinding;
 import eu.siacs.conversations.ui.util.DelayedHintHelper;
+import java.util.ArrayList;
+import java.util.List;
 
 public class CreatePrivateGroupChatDialog extends DialogFragment {
 
@@ -41,23 +36,36 @@ public class CreatePrivateGroupChatDialog extends DialogFragment {
     @NonNull
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
+        final MaterialAlertDialogBuilder builder =
+                new MaterialAlertDialogBuilder(requireActivity());
         builder.setTitle(R.string.create_private_group_chat);
-        final DialogCreateConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_create_conference, null, false);
+        final DialogCreateConferenceBinding binding =
+                DataBindingUtil.inflate(
+                        getActivity().getLayoutInflater(),
+                        R.layout.dialog_create_conference,
+                        null,
+                        false);
         ArrayList<String> mActivatedAccounts = getArguments().getStringArrayList(ACCOUNTS_LIST_KEY);
-        StartConversationActivity.populateAccountSpinner(getActivity(), mActivatedAccounts, binding.account);
+        StartConversationActivity.populateAccountSpinner(
+                getActivity(), mActivatedAccounts, binding.account);
         builder.setView(binding.getRoot());
-        builder.setPositiveButton(R.string.choose_participants, (dialog, which) -> mListener.onCreateDialogPositiveClick(binding.account, binding.groupChatName.getText().toString().trim()));
+        builder.setPositiveButton(
+                R.string.choose_participants,
+                (dialog, which) ->
+                        mListener.onCreateDialogPositiveClick(
+                                binding.account,
+                                binding.groupChatName.getText().toString().trim()));
         builder.setNegativeButton(R.string.cancel, null);
         DelayedHintHelper.setHint(R.string.providing_a_name_is_optional, binding.groupChatName);
-        binding.groupChatName.setOnEditorActionListener((v, actionId, event) -> {
-            mListener.onCreateDialogPositiveClick(binding.account, binding.groupChatName.getText().toString().trim());
-            return true;
-        });
+        binding.groupChatName.setOnEditorActionListener(
+                (v, actionId, event) -> {
+                    mListener.onCreateDialogPositiveClick(
+                            binding.account, binding.groupChatName.getText().toString().trim());
+                    return true;
+                });
         return builder.create();
     }
 
-
     public interface CreateConferenceDialogListener {
         void onCreateDialogPositiveClick(AutoCompleteTextView spinner, String subject);
     }
@@ -68,8 +76,8 @@ public class CreatePrivateGroupChatDialog extends DialogFragment {
         try {
             mListener = (CreateConferenceDialogListener) context;
         } catch (ClassCastException e) {
-            throw new ClassCastException(context.toString()
-                    + " must implement CreateConferenceDialogListener");
+            throw new ClassCastException(
+                    context + " must implement CreateConferenceDialogListener");
         }
     }
 

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

@@ -288,7 +288,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
             mListener = (CreatePublicChannelDialogListener) context;
         } catch (ClassCastException e) {
             throw new ClassCastException(
-                    context.toString() + " must implement CreateConferenceDialogListener");
+                    context + " must implement CreateConferenceDialogListener");
         }
     }
 

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

@@ -6,120 +6,140 @@ import android.content.Context;
 import android.content.DialogInterface;
 import android.os.Bundle;
 import android.widget.AutoCompleteTextView;
-import android.widget.Spinner;
-
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.DialogFragment;
-
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 import com.google.android.material.textfield.TextInputLayout;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.DialogJoinConferenceBinding;
 import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
 import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
 import eu.siacs.conversations.ui.util.DelayedHintHelper;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
 
 public class JoinConferenceDialog extends DialogFragment implements OnBackendConnected {
 
-	private static final String PREFILLED_JID_KEY = "prefilled_jid";
-	private static final String PREFILLED_PASSWORD_KEY = "prefilled_password";
-	private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
-	private JoinConferenceDialogListener mListener;
-	private KnownHostsAdapter knownHostsAdapter;
-
-	public static JoinConferenceDialog newInstance(String prefilledJid, String password, List<String> accounts) {
-		JoinConferenceDialog dialog = new JoinConferenceDialog();
-		Bundle bundle = new Bundle();
-		bundle.putString(PREFILLED_JID_KEY, prefilledJid);
-		bundle.putString(PREFILLED_PASSWORD_KEY, password);
-		bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) accounts);
-		dialog.setArguments(bundle);
-		return dialog;
-	}
-
-	@Override
-	public void onActivityCreated(Bundle savedInstanceState) {
-		super.onActivityCreated(savedInstanceState);
-		setRetainInstance(true);
-	}
-
-	@NonNull
-	@Override
-	public Dialog onCreateDialog(Bundle savedInstanceState) {
-		final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity());
-		builder.setTitle(R.string.join_public_channel);
-		final DialogJoinConferenceBinding binding = DataBindingUtil.inflate(getActivity().getLayoutInflater(), R.layout.dialog_join_conference, null, false);
-		DelayedHintHelper.setHint(R.string.channel_full_jid_example, binding.jid);
-		this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
-		binding.jid.setAdapter(knownHostsAdapter);
-		String prefilledJid = getArguments().getString(PREFILLED_JID_KEY);
-		if (prefilledJid != null) {
-			binding.jid.append(prefilledJid);
-		}
-		StartConversationActivity.populateAccountSpinner(getActivity(), getArguments().getStringArrayList(ACCOUNTS_LIST_KEY), binding.account);
-		builder.setView(binding.getRoot());
-		builder.setPositiveButton(R.string.join, null);
-		builder.setNegativeButton(R.string.cancel, null);
-		AlertDialog dialog = builder.create();
-		dialog.show();
-		dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.jid.getText().toString().equals(getArguments().getString(PREFILLED_JID_KEY)) ? getArguments().getString(PREFILLED_PASSWORD_KEY) : null));
-		binding.jid.setOnEditorActionListener((v, actionId, event) -> {
-			mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.jid.getText().toString().equals(getArguments().getString(PREFILLED_JID_KEY)) ? getArguments().getString(PREFILLED_PASSWORD_KEY) : null);
-			return true;
-		});
-		return dialog;
-	}
-
-	@Override
-	public void onBackendConnected() {
-		refreshKnownHosts();
-	}
-
-	private void refreshKnownHosts() {
-		Activity activity = getActivity();
-		if (activity instanceof XmppActivity) {
-			Collection<String> hosts = ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts();
-			this.knownHostsAdapter.refresh(hosts);
-		}
-	}
-
-	@Override
-	public void onAttach(@NonNull final Context context) {
-		super.onAttach(context);
-		try {
-			mListener = (JoinConferenceDialogListener) context;
-		} catch (ClassCastException e) {
-			throw new ClassCastException(context.toString()
-					+ " must implement JoinConferenceDialogListener");
-		}
-	}
-
-	@Override
-	public void onDestroyView() {
-		Dialog dialog = getDialog();
-		if (dialog != null && getRetainInstance()) {
-			dialog.setDismissMessage(null);
-		}
-		super.onDestroyView();
-	}
-
-	@Override
-	public void onStart() {
-		super.onStart();
-		final Activity activity = getActivity();
-		if (activity instanceof XmppActivity && ((XmppActivity) activity).xmppConnectionService != null) {
-			refreshKnownHosts();
-		}
-	}
-
-	public interface JoinConferenceDialogListener {
-		void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, String password);
-	}
+    private static final String PREFILLED_JID_KEY = "prefilled_jid";
+    private static final String PREFILLED_PASSWORD_KEY = "prefilled_password";
+    private static final String ACCOUNTS_LIST_KEY = "activated_accounts_list";
+    private JoinConferenceDialogListener mListener;
+    private KnownHostsAdapter knownHostsAdapter;
+
+    public static JoinConferenceDialog newInstance(String prefilledJid, String password, List<String> accounts) {
+        JoinConferenceDialog dialog = new JoinConferenceDialog();
+        Bundle bundle = new Bundle();
+        bundle.putString(PREFILLED_JID_KEY, prefilledJid);
+        bundle.putString(PREFILLED_PASSWORD_KEY, password);
+        bundle.putStringArrayList(ACCOUNTS_LIST_KEY, (ArrayList<String>) accounts);
+        dialog.setArguments(bundle);
+        return dialog;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        setRetainInstance(true);
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final MaterialAlertDialogBuilder builder =
+                new MaterialAlertDialogBuilder(requireActivity());
+        builder.setTitle(R.string.join_public_channel);
+        final DialogJoinConferenceBinding binding =
+                DataBindingUtil.inflate(
+                        getActivity().getLayoutInflater(),
+                        R.layout.dialog_join_conference,
+                        null,
+                        false);
+        DelayedHintHelper.setHint(R.string.channel_full_jid_example, binding.jid);
+        this.knownHostsAdapter = new KnownHostsAdapter(getActivity(), R.layout.item_autocomplete);
+        binding.jid.setAdapter(knownHostsAdapter);
+        String prefilledJid = getArguments().getString(PREFILLED_JID_KEY);
+        if (prefilledJid != null) {
+            binding.jid.append(prefilledJid);
+        }
+        StartConversationActivity.populateAccountSpinner(
+                getActivity(),
+                getArguments().getStringArrayList(ACCOUNTS_LIST_KEY),
+                binding.account);
+        builder.setView(binding.getRoot());
+        builder.setPositiveButton(R.string.join, null);
+        builder.setNegativeButton(R.string.cancel, null);
+        AlertDialog dialog = builder.create();
+        dialog.show();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(view -> mListener.onJoinDialogPositiveClick(dialog, binding.account, binding.accountJidLayout, binding.jid, binding.jid.getText().toString().equals(getArguments().getString(PREFILLED_JID_KEY)) ? getArguments().getString(PREFILLED_PASSWORD_KEY) : null));
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE)
+                .setOnClickListener(
+                        view ->
+                                mListener.onJoinDialogPositiveClick(
+                                        dialog,
+                                        binding.account,
+                                        binding.accountJidLayout,
+                                        binding.jid,
+                                        binding.jid.getText().toString().equals(getArguments().getString(PREFILLED_JID_KEY)) ? getArguments().getString(PREFILLED_PASSWORD_KEY) : null
+                                ));
+        binding.jid.setOnEditorActionListener(
+                (v, actionId, event) -> {
+                    mListener.onJoinDialogPositiveClick(
+                            dialog, binding.account, binding.accountJidLayout, binding.jid,
+                            binding.jid.getText().toString().equals(getArguments().getString(PREFILLED_JID_KEY)) ? getArguments().getString(PREFILLED_PASSWORD_KEY) : null
+                    );
+                    return true;
+                });
+        return dialog;
+    }
+
+    @Override
+    public void onBackendConnected() {
+        refreshKnownHosts();
+    }
+
+    private void refreshKnownHosts() {
+        Activity activity = getActivity();
+        if (activity instanceof XmppActivity) {
+            Collection<String> hosts =
+                    ((XmppActivity) activity).xmppConnectionService.getKnownConferenceHosts();
+            this.knownHostsAdapter.refresh(hosts);
+        }
+    }
+
+    @Override
+    public void onAttach(@NonNull final Context context) {
+        super.onAttach(context);
+        try {
+            mListener = (JoinConferenceDialogListener) context;
+        } catch (ClassCastException e) {
+            throw new ClassCastException(context + " must implement JoinConferenceDialogListener");
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        Dialog dialog = getDialog();
+        if (dialog != null && getRetainInstance()) {
+            dialog.setDismissMessage(null);
+        }
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        final Activity activity = getActivity();
+        if (activity instanceof XmppActivity
+                && ((XmppActivity) activity).xmppConnectionService != null) {
+            refreshKnownHosts();
+        }
+    }
+
+    public interface JoinConferenceDialogListener {
+        void onJoinDialogPositiveClick(Dialog dialog, AutoCompleteTextView spinner, TextInputLayout jidLayout, AutoCompleteTextView jid, String password);
+    }
 }

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

@@ -5,50 +5,57 @@ import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.inputmethod.InputMethodManager;
-
 import androidx.appcompat.app.ActionBar;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.ListItem;
+import java.util.Collections;
+import java.util.List;
 
 public class ShortcutActivity extends AbstractSearchableListItemActivity {
 
-    private static final List<String> BLACKLISTED_ACTIVITIES = Arrays.asList("com.teslacoilsw.launcher.ChooseActionIntentActivity");
+    private static final List<String> BLACKLISTED_ACTIVITIES =
+            List.of("com.teslacoilsw.launcher.ChooseActionIntentActivity");
 
     @Override
-    protected void refreshUiReal() {
-
-    }
+    protected void refreshUiReal() {}
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        getListView().setOnItemClickListener((parent, view, position, id) -> {
-
-            final ComponentName callingActivity = getCallingActivity();
+        getListView()
+                .setOnItemClickListener(
+                        (parent, view, position, id) -> {
+                            final ComponentName callingActivity = getCallingActivity();
 
-            final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
-            imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY);
+                            final InputMethodManager imm =
+                                    (InputMethodManager)
+                                            getSystemService(Context.INPUT_METHOD_SERVICE);
+                            imm.hideSoftInputFromWindow(
+                                    getSearchEditText().getWindowToken(),
+                                    InputMethodManager.HIDE_IMPLICIT_ONLY);
 
-            ListItem listItem = getListItems().get(position);
-            final boolean legacy = BLACKLISTED_ACTIVITIES.contains(callingActivity == null ? null : callingActivity.getClassName());
-            Intent shortcut = xmppConnectionService.getShortcutService().createShortcut(((Contact) listItem), legacy);
-            setResult(RESULT_OK,shortcut);
-            finish();
-        });
+                            ListItem listItem = getListItems().get(position);
+                            final boolean legacy =
+                                    BLACKLISTED_ACTIVITIES.contains(
+                                            callingActivity == null
+                                                    ? null
+                                                    : callingActivity.getClassName());
+                            Intent shortcut =
+                                    xmppConnectionService
+                                            .getShortcutService()
+                                            .createShortcut(((Contact) listItem), legacy);
+                            setResult(RESULT_OK, shortcut);
+                            finish();
+                        });
     }
 
     @Override
     public void onStart() {
         super.onStart();
         ActionBar bar = getSupportActionBar();
-        if(bar != null){
+        if (bar != null) {
             bar.setTitle(R.string.create_shortcut);
         }
     }
@@ -63,8 +70,7 @@ public class ShortcutActivity extends AbstractSearchableListItemActivity {
         for (final Account account : xmppConnectionService.getAccounts()) {
             if (account.isEnabled()) {
                 for (final Contact contact : account.getRoster().getContacts()) {
-                    if (contact.showInContactList()
-                            && contact.match(this, needle)) {
+                    if (contact.showInContactList() && contact.match(this, needle)) {
                         getListItems().add(contact);
                     }
                 }

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

@@ -216,8 +216,7 @@ public abstract class XmppActivity extends ActionBarActivity {
     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
         if (imageView != null) {
             final Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AsyncDrawable) {
-                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+            if (drawable instanceof AsyncDrawable asyncDrawable) {
                 return asyncDrawable.getBitmapWorkerTask();
             }
         }

src/main/java/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java 🔗

@@ -30,7 +30,7 @@ public class KnownHostsAdapter extends ArrayAdapter<String> {
                         final String local = split[0].toLowerCase(Locale.ENGLISH);
                         if (Config.QUICKSY_DOMAIN != null
                                 && E164_PATTERN.matcher(local).matches()) {
-                            builder.add(local + '@' + Config.QUICKSY_DOMAIN.toString());
+                            builder.add(local + '@' + Config.QUICKSY_DOMAIN);
                         } else {
                             for (String domain : domains) {
                                 builder.add(local + '@' + domain);

src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java 🔗

@@ -79,10 +79,8 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
         this.mediaSize = Math.round(activity.getResources().getDimension(mediaSize));
     }
 
-    @SuppressWarnings("rawtypes")
-    public static void setMediaSize(final RecyclerView recyclerView, int mediaSize) {
-        final RecyclerView.Adapter adapter = recyclerView.getAdapter();
-        if (adapter instanceof MediaAdapter mediaAdapter) {
+    public static void setMediaSize(final RecyclerView recyclerView, final int mediaSize) {
+        if (recyclerView.getAdapter() instanceof MediaAdapter mediaAdapter) {
             mediaAdapter.setMediaSize(mediaSize);
         }
     }
@@ -141,6 +139,9 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
             return R.drawable.ic_email_48dp;
         } else if (mime.equals("application/webxdc+zip")) {
             return R.drawable.toys_and_games_24dp;
+        } else if (Arrays.asList("application/x-pcapng", "application/vnd.tcpdump.pcap")
+                .contains(mime)) {
+            return R.drawable.ic_lan_24dp;
         } else {
             return R.drawable.ic_help_center_48dp;
         }
@@ -273,7 +274,7 @@ public class MediaAdapter extends RecyclerView.Adapter<MediaAdapter.MediaViewHol
         }
     }
 
-    static class MediaViewHolder extends RecyclerView.ViewHolder {
+    public static class MediaViewHolder extends RecyclerView.ViewHolder {
 
         private final ItemMediaBinding binding;
 

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -2039,7 +2039,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 
     private abstract static class MessageItemViewHolder /*extends RecyclerView.ViewHolder*/ {
 
-        private View itemView;
+        private final View itemView;
 
         private MessageItemViewHolder(@NonNull View itemView) {
             this.itemView = itemView;

src/main/java/eu/siacs/conversations/ui/service/CameraManager.java 🔗

@@ -25,9 +25,8 @@ import android.hardware.Camera.CameraInfo;
 import android.hardware.Camera.PreviewCallback;
 import android.util.Log;
 import android.view.TextureView;
-
 import com.google.zxing.PlanarYUVLuminanceSource;
-
+import eu.siacs.conversations.Config;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -35,8 +34,6 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
-import eu.siacs.conversations.Config;
-
 /**
  * @author Andreas Schildbach
  */
@@ -69,7 +66,10 @@ public final class CameraManager {
         return cameraInfo.orientation;
     }
 
-    public Camera open(final TextureView textureView, final int displayOrientation, final boolean continuousAutoFocus)
+    public Camera open(
+            final TextureView textureView,
+            final int displayOrientation,
+            final boolean continuousAutoFocus)
             throws IOException {
         final int cameraId = determineCameraId();
         Camera.getCameraInfo(cameraId, cameraInfo);
@@ -80,8 +80,7 @@ public final class CameraManager {
             camera.setDisplayOrientation((720 - displayOrientation - cameraInfo.orientation) % 360);
         else if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK)
             camera.setDisplayOrientation((720 - displayOrientation + cameraInfo.orientation) % 360);
-        else
-            throw new IllegalStateException("facing: " + cameraInfo.facing);
+        else throw new IllegalStateException("facing: " + cameraInfo.facing);
 
         camera.setPreviewTexture(textureView.getSurfaceTexture());
 
@@ -105,18 +104,22 @@ public final class CameraManager {
         boolean isTexturePortrait = width < height;
         boolean isCameraPortrait = cameraResolution.width < cameraResolution.height;
         if (isTexturePortrait == isCameraPortrait) {
-            widthFactor = (float)cameraResolution.width / width;
-            heightFactor = (float)cameraResolution.height / height;
+            widthFactor = (float) cameraResolution.width / width;
+            heightFactor = (float) cameraResolution.height / height;
             orientedFrame = new Rect(frame);
         } else {
-            widthFactor = (float)cameraResolution.width / height;
-            heightFactor = (float)cameraResolution.height / width;
+            widthFactor = (float) cameraResolution.width / height;
+            heightFactor = (float) cameraResolution.height / width;
             // Swap X and Y coordinates to flip frame to the same orientation as cameraResolution
             orientedFrame = new Rect(frame.top, frame.left, frame.bottom, frame.right);
         }
 
-        framePreview = new RectF(orientedFrame.left * widthFactor, orientedFrame.top * heightFactor,
-                orientedFrame.right * widthFactor, orientedFrame.bottom * heightFactor);
+        framePreview =
+                new RectF(
+                        orientedFrame.left * widthFactor,
+                        orientedFrame.top * heightFactor,
+                        orientedFrame.right * widthFactor,
+                        orientedFrame.bottom * heightFactor);
 
         final String savedParameters = parameters == null ? null : parameters.flatten();
 
@@ -130,7 +133,7 @@ public final class CameraManager {
                     camera.setParameters(parameters2);
                     setDesiredCameraParameters(camera, cameraResolution, continuousAutoFocus);
                 } catch (final RuntimeException x2) {
-                    Log.d(Config.LOGTAG,"problem setting camera parameters", x2);
+                    Log.d(Config.LOGTAG, "problem setting camera parameters", x2);
                 }
             }
         }
@@ -139,7 +142,7 @@ public final class CameraManager {
             camera.startPreview();
             return camera;
         } catch (final RuntimeException x) {
-            Log.w(Config.LOGTAG,"something went wrong while starting camera preview", x);
+            Log.w(Config.LOGTAG, "something went wrong while starting camera preview", x);
             camera.release();
             throw x;
         }
@@ -152,15 +155,13 @@ public final class CameraManager {
         // prefer back-facing camera
         for (int i = 0; i < cameraCount; i++) {
             Camera.getCameraInfo(i, cameraInfo);
-            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK)
-                return i;
+            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) return i;
         }
 
         // fall back to front-facing camera
         for (int i = 0; i < cameraCount; i++) {
             Camera.getCameraInfo(i, cameraInfo);
-            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
-                return i;
+            if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) return i;
         }
 
         return -1;
@@ -171,29 +172,28 @@ public final class CameraManager {
             try {
                 camera.stopPreview();
             } catch (final RuntimeException x) {
-                Log.w(Config.LOGTAG,"something went wrong while stopping camera preview", x);
+                Log.w(Config.LOGTAG, "something went wrong while stopping camera preview", x);
             }
 
             camera.release();
         }
     }
 
-    private static final Comparator<Camera.Size> numPixelComparator = new Comparator<Camera.Size>() {
-        @Override
-        public int compare(final Camera.Size size1, final Camera.Size size2) {
-            final int pixels1 = size1.height * size1.width;
-            final int pixels2 = size2.height * size2.width;
-
-            if (pixels1 < pixels2)
-                return 1;
-            else if (pixels1 > pixels2)
-                return -1;
-            else
-                return 0;
-        }
-    };
+    private static final Comparator<Camera.Size> numPixelComparator =
+            new Comparator<Camera.Size>() {
+                @Override
+                public int compare(final Camera.Size size1, final Camera.Size size2) {
+                    final int pixels1 = size1.height * size1.width;
+                    final int pixels2 = size2.height * size2.width;
+
+                    if (pixels1 < pixels2) return 1;
+                    else if (pixels1 > pixels2) return -1;
+                    else return 0;
+                }
+            };
 
-    private static Camera.Size findBestPreviewSizeValue(final Camera.Parameters parameters, int width, int height) {
+    private static Camera.Size findBestPreviewSizeValue(
+            final Camera.Parameters parameters, int width, int height) {
         if (height > width) {
             final int temp = width;
             width = height;
@@ -203,11 +203,11 @@ public final class CameraManager {
         final float screenAspectRatio = (float) width / (float) height;
 
         final List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
-        if (rawSupportedSizes == null)
-            return parameters.getPreviewSize();
+        if (rawSupportedSizes == null) return parameters.getPreviewSize();
 
         // sort by size, descending
-        final List<Camera.Size> supportedPreviewSizes = new ArrayList<Camera.Size>(rawSupportedSizes);
+        final List<Camera.Size> supportedPreviewSizes =
+                new ArrayList<Camera.Size>(rawSupportedSizes);
         Collections.sort(supportedPreviewSizes, numPixelComparator);
 
         Camera.Size bestSize = null;
@@ -217,8 +217,7 @@ public final class CameraManager {
             final int realWidth = supportedPreviewSize.width;
             final int realHeight = supportedPreviewSize.height;
             final int realPixels = realWidth * realHeight;
-            if (realPixels < MIN_PREVIEW_PIXELS || realPixels > MAX_PREVIEW_PIXELS)
-                continue;
+            if (realPixels < MIN_PREVIEW_PIXELS || realPixels > MAX_PREVIEW_PIXELS) continue;
 
             final boolean isCandidatePortrait = realWidth < realHeight;
             final int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
@@ -234,27 +233,32 @@ public final class CameraManager {
             }
         }
 
-        if (bestSize != null)
-            return bestSize;
-        else
-            return parameters.getPreviewSize();
+        if (bestSize != null) return bestSize;
+        else return parameters.getPreviewSize();
     }
 
     @SuppressLint("InlinedApi")
-    private static void setDesiredCameraParameters(final Camera camera, final Camera.Size cameraResolution,
+    private static void setDesiredCameraParameters(
+            final Camera camera,
+            final Camera.Size cameraResolution,
             final boolean continuousAutoFocus) {
         final Camera.Parameters parameters = camera.getParameters();
-        if (parameters == null)
-            return;
+        if (parameters == null) return;
 
         final List<String> supportedFocusModes = parameters.getSupportedFocusModes();
-        final String focusMode = continuousAutoFocus
-                ? findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
-                        Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO, Camera.Parameters.FOCUS_MODE_AUTO,
-                        Camera.Parameters.FOCUS_MODE_MACRO)
-                : findValue(supportedFocusModes, Camera.Parameters.FOCUS_MODE_AUTO, Camera.Parameters.FOCUS_MODE_MACRO);
-        if (focusMode != null)
-            parameters.setFocusMode(focusMode);
+        final String focusMode =
+                continuousAutoFocus
+                        ? findValue(
+                                supportedFocusModes,
+                                Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
+                                Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
+                                Camera.Parameters.FOCUS_MODE_AUTO,
+                                Camera.Parameters.FOCUS_MODE_MACRO)
+                        : findValue(
+                                supportedFocusModes,
+                                Camera.Parameters.FOCUS_MODE_AUTO,
+                                Camera.Parameters.FOCUS_MODE_MACRO);
+        if (focusMode != null) parameters.setFocusMode(focusMode);
 
         parameters.setPreviewSize(cameraResolution.width, cameraResolution.height);
 
@@ -265,26 +269,31 @@ public final class CameraManager {
         try {
             camera.setOneShotPreviewCallback(callback);
         } catch (final RuntimeException x) {
-            Log.d(Config.LOGTAG,"problem requesting preview frame, callback won't be called", x);
+            Log.d(Config.LOGTAG, "problem requesting preview frame, callback won't be called", x);
         }
     }
 
     public PlanarYUVLuminanceSource buildLuminanceSource(final byte[] data) {
-        return new PlanarYUVLuminanceSource(data, cameraResolution.width, cameraResolution.height,
-                (int) framePreview.left, (int) framePreview.top, (int) framePreview.width(),
-                (int) framePreview.height(), false);
+        return new PlanarYUVLuminanceSource(
+                data,
+                cameraResolution.width,
+                cameraResolution.height,
+                (int) framePreview.left,
+                (int) framePreview.top,
+                (int) framePreview.width(),
+                (int) framePreview.height(),
+                false);
     }
 
     public void setTorch(final boolean enabled) {
-        if (enabled != getTorchEnabled(camera))
-            setTorchEnabled(camera, enabled);
+        if (enabled != getTorchEnabled(camera)) setTorchEnabled(camera, enabled);
     }
 
     private static boolean getTorchEnabled(final Camera camera) {
         final Camera.Parameters parameters = camera.getParameters();
         if (parameters != null) {
             final String flashMode = camera.getParameters().getFlashMode();
-            return flashMode != null && (Camera.Parameters.FLASH_MODE_ON.equals(flashMode)
+            return (Camera.Parameters.FLASH_MODE_ON.equals(flashMode)
                     || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode));
         }
 
@@ -298,10 +307,12 @@ public final class CameraManager {
         if (supportedFlashModes != null) {
             final String flashMode;
             if (enabled)
-                flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_TORCH,
-                        Camera.Parameters.FLASH_MODE_ON);
-            else
-                flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF);
+                flashMode =
+                        findValue(
+                                supportedFlashModes,
+                                Camera.Parameters.FLASH_MODE_TORCH,
+                                Camera.Parameters.FLASH_MODE_ON);
+            else flashMode = findValue(supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF);
 
             if (flashMode != null) {
                 camera.cancelAutoFocus(); // autofocus can cause conflict
@@ -314,8 +325,7 @@ public final class CameraManager {
 
     private static String findValue(final Collection<String> values, final String... valuesToFind) {
         for (final String valueToFind : valuesToFind)
-            if (values.contains(valueToFind))
-                return valueToFind;
+            if (values.contains(valueToFind)) return valueToFind;
 
         return null;
     }

src/main/java/eu/siacs/conversations/ui/util/AvatarWorkerTask.java 🔗

@@ -9,23 +9,19 @@ import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.Build;
 import android.widget.ImageView;
-
 import androidx.annotation.DimenRes;
-
-import java.lang.ref.WeakReference;
-import java.util.concurrent.RejectedExecutionException;
-
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.ui.XmppActivity;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.RejectedExecutionException;
 
 public class AvatarWorkerTask extends AsyncTask<AvatarService.Avatarable, Void, Drawable> {
     private final WeakReference<ImageView> imageViewReference;
     private final WeakReference<XmppActivity> activityReference;
     private AvatarService.Avatarable avatarable = null;
-    private @DimenRes
-    final int size;
+    private @DimenRes final int size;
 
     public AvatarWorkerTask(ImageView imageView, @DimenRes int size) {
         imageViewReference = new WeakReference<>(imageView);
@@ -47,7 +43,8 @@ public class AvatarWorkerTask extends AsyncTask<AvatarService.Avatarable, Void,
         if (activity == null) {
             return null;
         }
-        return activity.avatarService().get(avatarable, (int) activity.getResources().getDimension(size), isCancelled());
+        return activity.avatarService()
+                .get(avatarable, (int) activity.getResources().getDimension(size), isCancelled());
     }
 
     @Override
@@ -70,11 +67,12 @@ public class AvatarWorkerTask extends AsyncTask<AvatarService.Avatarable, Void,
         }
     }
 
-    public static boolean cancelPotentialWork(AvatarService.Avatarable avatarable, ImageView imageView) {
+    public static boolean cancelPotentialWork(
+            AvatarService.Avatarable avatarable, ImageView imageView) {
         final AvatarWorkerTask workerTask = getBitmapWorkerTask(imageView);
 
         if (workerTask != null) {
-            final AvatarService.Avatarable old= workerTask.avatarable;
+            final AvatarService.Avatarable old = workerTask.avatarable;
             if (old == null || avatarable != old) {
                 workerTask.cancel(true);
             } else {
@@ -87,15 +85,17 @@ public class AvatarWorkerTask extends AsyncTask<AvatarService.Avatarable, Void,
     public static AvatarWorkerTask getBitmapWorkerTask(ImageView imageView) {
         if (imageView != null) {
             final Drawable drawable = imageView.getDrawable();
-            if (drawable instanceof AsyncDrawable) {
-                final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
+            if (drawable instanceof AsyncDrawable asyncDrawable) {
                 return asyncDrawable.getAvatarWorkerTask();
             }
         }
         return null;
     }
 
-    public static void loadAvatar(final AvatarService.Avatarable avatarable, final ImageView imageView, final @DimenRes int size) {
+    public static void loadAvatar(
+            final AvatarService.Avatarable avatarable,
+            final ImageView imageView,
+            final @DimenRes int size) {
         if (cancelPotentialWork(avatarable, imageView)) {
             final XmppActivity activity = XmppActivity.find(imageView);
             if (activity == null) {
@@ -114,7 +114,8 @@ public class AvatarWorkerTask extends AsyncTask<AvatarService.Avatarable, Void,
                 imageView.setBackgroundColor(avatarable.getAvatarBackgroundColor());
                 imageView.setImageDrawable(null);
                 final AvatarWorkerTask task = new AvatarWorkerTask(imageView, size);
-                final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task);
+                final AsyncDrawable asyncDrawable =
+                        new AsyncDrawable(activity.getResources(), null, task);
                 imageView.setImageDrawable(asyncDrawable);
                 try {
                     task.execute(avatarable);
@@ -124,12 +125,14 @@ public class AvatarWorkerTask extends AsyncTask<AvatarService.Avatarable, Void,
         }
     }
 
-    private static void setContentDescription(final AvatarService.Avatarable avatarable, final ImageView imageView) {
+    private static void setContentDescription(
+            final AvatarService.Avatarable avatarable, final ImageView imageView) {
         final Context context = imageView.getContext();
         if (avatarable instanceof Account) {
             imageView.setContentDescription(context.getString(R.string.your_avatar));
         } else {
-            imageView.setContentDescription(context.getString(R.string.avatar_for_x, avatarable.getAvatarName()));
+            imageView.setContentDescription(
+                    context.getString(R.string.avatar_for_x, avatarable.getAvatarName()));
         }
     }
 

src/main/java/eu/siacs/conversations/ui/util/MucDetailsContextMenuHelper.java 🔗

@@ -18,7 +18,6 @@ import androidx.databinding.DataBindingUtil;
 import java.util.ArrayList;
 
 import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.DialogQuickeditBinding;
@@ -50,9 +49,8 @@ public final class MucDetailsContextMenuHelper {
     public static void onCreateContextMenu(ContextMenu menu, View v) {
         final XmppActivity activity = XmppActivity.find(v);
         final Object tag = v.getTag();
-        if (tag instanceof MucOptions.User && activity != null) {
+        if (tag instanceof User user && activity != null) {
             activity.getMenuInflater().inflate(R.menu.muc_details_context, menu);
-            final MucOptions.User user = (MucOptions.User) tag;
             String name;
             final Contact contact = user.getContact();
             if (contact != null && contact.showInContactList()) {
@@ -63,7 +61,8 @@ public final class MucDetailsContextMenuHelper {
                 name = user.getNick();
             }
             menu.setHeaderTitle(name);
-            MucDetailsContextMenuHelper.configureMucDetailsContextMenu(activity, menu, user.getConversation(), user);
+            MucDetailsContextMenuHelper.configureMucDetailsContextMenu(
+                    activity, menu, user.getConversation(), user);
         }
     }
 
@@ -108,9 +107,12 @@ public final class MucDetailsContextMenuHelper {
         return new Pair<>(items.toArray(new CharSequence[items.size()]), actions.toArray(new Integer[actions.size()]));
     }
 
-    public static void configureMucDetailsContextMenu(XmppActivity activity, Menu menu, Conversation conversation, User user) {
+    public static void configureMucDetailsContextMenu(
+            XmppActivity activity, Menu menu, Conversation conversation, User user) {
         final MucOptions mucOptions = conversation.getMucOptions();
-        final boolean advancedMode = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("advanced_muc_mode", false);
+        final boolean advancedMode =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+                        .getBoolean("advanced_muc_mode", false);
         final boolean showMucPm = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("show_muc_pm", false);
         final boolean isGroupChat = mucOptions.isPrivateAndNonAnonymous();
         MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
@@ -136,15 +138,19 @@ public final class MucDetailsContextMenuHelper {
             MenuItem startConversation = menu.findItem(R.id.start_conversation);
             MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
             MenuItem managePermissions = menu.findItem(R.id.manage_permissions);
-            removeFromRoom.setTitle(isGroupChat ? R.string.remove_from_room : R.string.remove_from_channel);
+            removeFromRoom.setTitle(
+                    isGroupChat ? R.string.remove_from_room : R.string.remove_from_channel);
             MenuItem invite = menu.findItem(R.id.invite);
             startConversation.setVisible(true);
             final Contact contact = user.getContact();
             final User self = conversation.getMucOptions().getSelf();
-            if ((contact != null && contact.showInRoster()) || mucOptions.isPrivateAndNonAnonymous()) {
+            if ((contact != null && contact.showInRoster())
+                    || mucOptions.isPrivateAndNonAnonymous()) {
                 showContactDetails.setVisible(contact == null || !contact.isSelf());
             }
-            if ((activity instanceof ConferenceDetailsActivity || activity instanceof MucUsersActivity) && user.getRole() == MucOptions.Role.NONE) {
+            if ((activity instanceof ConferenceDetailsActivity
+                            || activity instanceof MucUsersActivity)
+                    && user.getRole() == MucOptions.Role.NONE) {
                 invite.setVisible(true);
             }
             boolean managePermissionsVisible = false;
@@ -202,15 +208,20 @@ public final class MucDetailsContextMenuHelper {
             .setNegativeButton(R.string.no, null).show();
     }
 
-    public static boolean onContextItemSelected(MenuItem item, User user, XmppActivity activity, final String fingerprint) {
+    public static boolean onContextItemSelected(
+            MenuItem item, User user, XmppActivity activity, final String fingerprint) {
         final Conversation conversation = user.getConversation();
-        final XmppConnectionService.OnAffiliationChanged onAffiliationChanged = activity instanceof XmppConnectionService.OnAffiliationChanged ? (XmppConnectionService.OnAffiliationChanged) activity : null;
+        final XmppConnectionService.OnAffiliationChanged onAffiliationChanged =
+                activity instanceof XmppConnectionService.OnAffiliationChanged
+                        ? (XmppConnectionService.OnAffiliationChanged) activity
+                        : null;
         Jid jid = user.getRealJid();
         switch (item.getItemId()) {
             case R.id.action_contact_details:
                 final Jid realJid = user.getRealJid();
                 final Account account = conversation.getAccount();
-                final Contact contact = realJid == null ? null : account.getRoster().getContact(realJid);
+                final Contact contact =
+                        realJid == null ? null : account.getRoster().getContact(realJid);
                 if (contact != null) {
                     activity.switchToContactDetails(contact, fingerprint);
                 }
@@ -320,37 +331,60 @@ public final class MucDetailsContextMenuHelper {
         }
     }
 
-    private static void removeFromRoom(final User user, XmppActivity activity, XmppConnectionService.OnAffiliationChanged onAffiliationChanged) {
+    private static void removeFromRoom(
+            final User user,
+            XmppActivity activity,
+            XmppConnectionService.OnAffiliationChanged onAffiliationChanged) {
         final Conversation conversation = user.getConversation();
         if (conversation.getMucOptions().membersOnly()) {
-            activity.xmppConnectionService.changeAffiliationInConference(conversation, user.getRealJid(), MucOptions.Affiliation.NONE, onAffiliationChanged);
+            activity.xmppConnectionService.changeAffiliationInConference(
+                    conversation,
+                    user.getRealJid(),
+                    MucOptions.Affiliation.NONE,
+                    onAffiliationChanged);
             if (user.getRole() != MucOptions.Role.NONE) {
-                activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE);
+                activity.xmppConnectionService.changeRoleInConference(
+                        conversation, user.getName(), MucOptions.Role.NONE);
             }
         } else {
             final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity);
             builder.setTitle(R.string.ban_from_conference);
             String jid = user.getRealJid().asBareJid().toString();
-            SpannableString message = new SpannableString(activity.getString(R.string.removing_from_public_conference, jid));
+            SpannableString message =
+                    new SpannableString(
+                            activity.getString(R.string.removing_from_public_conference, jid));
             int start = message.toString().indexOf(jid);
             if (start >= 0) {
-                message.setSpan(new TypefaceSpan("monospace"), start, start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+                message.setSpan(
+                        new TypefaceSpan("monospace"),
+                        start,
+                        start + jid.length(),
+                        Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
             }
             builder.setMessage(message);
             builder.setNegativeButton(R.string.cancel, null);
-            builder.setPositiveButton(R.string.ban_now, (dialog, which) -> {
-                activity.xmppConnectionService.changeAffiliationInConference(conversation, user.getRealJid(), MucOptions.Affiliation.OUTCAST, onAffiliationChanged);
-                if (user.getRole() != MucOptions.Role.NONE) {
-                    activity.xmppConnectionService.changeRoleInConference(conversation, user.getName(), MucOptions.Role.NONE);
-                }
-            });
+            builder.setPositiveButton(
+                    R.string.ban_now,
+                    (dialog, which) -> {
+                        activity.xmppConnectionService.changeAffiliationInConference(
+                                conversation,
+                                user.getRealJid(),
+                                MucOptions.Affiliation.OUTCAST,
+                                onAffiliationChanged);
+                        if (user.getRole() != MucOptions.Role.NONE) {
+                            activity.xmppConnectionService.changeRoleInConference(
+                                    conversation, user.getName(), MucOptions.Role.NONE);
+                        }
+                    });
             builder.create().show();
         }
     }
 
     private static void startConversation(User user, XmppActivity activity) {
         if (user.getRealJid() != null) {
-            Conversation newConversation = activity.xmppConnectionService.findOrCreateConversation(user.getAccount(), user.getRealJid().asBareJid(), false, true);
+            Conversation newConversation =
+                    activity.xmppConnectionService.findOrCreateConversation(
+                            user.getAccount(), user.getRealJid().asBareJid(), false, true);
             activity.switchToConversation(newConversation);
         }
     }

src/main/java/eu/siacs/conversations/ui/widget/ScannerView.java 🔗

@@ -28,22 +28,18 @@ import android.graphics.Rect;
 import android.graphics.RectF;
 import android.util.AttributeSet;
 import android.view.View;
-
 import androidx.core.content.ContextCompat;
-
 import com.google.zxing.ResultPoint;
-
+import eu.siacs.conversations.R;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
-import eu.siacs.conversations.R;
-
 /**
  * @author Andreas Schildbach
  */
 public class ScannerView extends View {
-    private static final long LASER_ANIMATION_DELAY_MS = 100l;
+    private static final long LASER_ANIMATION_DELAY_MS = 100L;
     private static final int DOT_OPACITY = 0xa0;
     private static final int DOT_TTL_MS = 500;
 
@@ -81,8 +77,12 @@ public class ScannerView extends View {
         dotPaint.setAntiAlias(true);
     }
 
-    public void setFraming(final Rect frame, final RectF framePreview, final int displayRotation,
-            final int cameraRotation, final boolean cameraFlip) {
+    public void setFraming(
+            final Rect frame,
+            final RectF framePreview,
+            final int displayRotation,
+            final int cameraRotation,
+            final boolean cameraFlip) {
         this.frame = frame;
         matrix.setRectToRect(framePreview, new RectF(frame), ScaleToFit.FILL);
         matrix.postRotate(-displayRotation, frame.exactCenterX(), frame.exactCenterY());
@@ -99,15 +99,14 @@ public class ScannerView extends View {
     }
 
     public void addDot(final ResultPoint dot) {
-        dots.put(new float[] { dot.getX(), dot.getY() }, System.currentTimeMillis());
+        dots.put(new float[] {dot.getX(), dot.getY()}, System.currentTimeMillis());
 
         invalidate();
     }
 
     @Override
     public void onDraw(final Canvas canvas) {
-        if (frame == null)
-            return;
+        if (frame == null) return;
 
         final long now = System.currentTimeMillis();
 
@@ -142,7 +141,8 @@ public class ScannerView extends View {
         canvas.drawRect(frame, laserPaint);
 
         // draw points
-        for (final Iterator<Map.Entry<float[], Long>> i = dots.entrySet().iterator(); i.hasNext();) {
+        for (final Iterator<Map.Entry<float[], Long>> i = dots.entrySet().iterator();
+                i.hasNext(); ) {
             final Map.Entry<float[], Long> entry = i.next();
             final long age = now - entry.getValue();
             if (age < DOT_TTL_MS) {

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

@@ -9,8 +9,7 @@ public class CursorUtils {
 
     public static void upgradeCursorWindowSize(final Cursor cursor) {
         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
-            if (cursor instanceof AbstractWindowedCursor) {
-                final AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
+            if (cursor instanceof AbstractWindowedCursor windowedCursor) {
                 windowedCursor.setWindow(new CursorWindow("4M", 4 * 1024 * 1024));
             }
             if (cursor instanceof SQLiteCursor) {
@@ -18,5 +17,4 @@ public class CursorUtils {
             }
         }
     }
-
 }

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

@@ -12,8 +12,8 @@ import eu.siacs.conversations.entities.Conversational;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.ui.ShareLocationActivity;
 import eu.siacs.conversations.ui.ShowLocationActivity;
-import java.io.UnsupportedEncodingException;
 import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.regex.Matcher;
 import org.osmdroid.util.GeoPoint;
@@ -160,11 +160,8 @@ public class GeoHelper {
 
     private static String getLabel(Context context, Message message) {
         if (message.getStatus() == Message.STATUS_RECEIVED) {
-            try {
-                return URLEncoder.encode(UIHelper.getMessageDisplayName(message), "UTF-8");
-            } catch (UnsupportedEncodingException e) {
-                throw new AssertionError(e);
-            }
+            return URLEncoder.encode(
+                    UIHelper.getMessageDisplayName(message), StandardCharsets.UTF_8);
         } else {
             return context.getString(R.string.me);
         }

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

@@ -35,11 +35,11 @@ import java.util.List;
 
 public class ImStyleParser {
 
-    private final static List<Character> KEYWORDS = Arrays.asList('*', '_', '~', '`');
-    private final static List<Character> NO_SUB_PARSING_KEYWORDS = Arrays.asList('`');
-    private final static List<Character> BLOCK_KEYWORDS = Arrays.asList('`');
-    private final static boolean ALLOW_EMPTY = false;
-    private final static boolean PARSE_HIGHER_ORDER_END = true;
+    private static final List<Character> KEYWORDS = Arrays.asList('*', '_', '~', '`');
+    private static final List<Character> NO_SUB_PARSING_KEYWORDS = List.of('`');
+    private static final List<Character> BLOCK_KEYWORDS = List.of('`');
+    private static final boolean ALLOW_EMPTY = false;
+    private static final boolean PARSE_HIGHER_ORDER_END = true;
 
     public static List<Style> parse(CharSequence text) {
         return parse(text, 0, text.length() - 1);
@@ -49,7 +49,9 @@ public class ImStyleParser {
         List<Style> styles = new ArrayList<>();
         for (int i = start; i <= end; ++i) {
             char c = text.charAt(i);
-            if (KEYWORDS.contains(c) && precededByWhiteSpace(text, i, start) && !followedByWhitespace(text, i, end)) {
+            if (KEYWORDS.contains(c)
+                    && precededByWhiteSpace(text, i, start)
+                    && !followedByWhitespace(text, i, end)) {
                 if (BLOCK_KEYWORDS.contains(c) && isCharRepeatedTwoTimes(text, c, i + 1, end)) {
                     int to = seekEndBlock(text, c, i + 3, end);
                     if (to != -1 && (to != i + 5 || ALLOW_EMPTY)) {
@@ -92,7 +94,8 @@ public class ImStyleParser {
                 if (!PARSE_HIGHER_ORDER_END || followedByWhitespace(text, i, end)) {
                     return i;
                 } else {
-                    int higherOrder = seekHigherOrderEndWithoutNewBeginning(text, needle, i + 1, end);
+                    int higherOrder =
+                            seekHigherOrderEndWithoutNewBeginning(text, needle, i + 1, end);
                     if (higherOrder != -1) {
                         return higherOrder;
                     }
@@ -105,12 +108,17 @@ public class ImStyleParser {
         return -1;
     }
 
-    private static int seekHigherOrderEndWithoutNewBeginning(CharSequence text, char needle, int start, int end) {
+    private static int seekHigherOrderEndWithoutNewBeginning(
+            CharSequence text, char needle, int start, int end) {
         for (int i = start; i <= end; ++i) {
             char c = text.charAt(i);
-            if (c == needle && precededByWhiteSpace(text, i, start) && !followedByWhitespace(text, i, end)) {
+            if (c == needle
+                    && precededByWhiteSpace(text, i, start)
+                    && !followedByWhitespace(text, i, end)) {
                 return -1; // new beginning
-            } else if (c == needle && !Character.isWhitespace(text.charAt(i - 1)) && followedByWhitespace(text, i, end)) {
+            } else if (c == needle
+                    && !Character.isWhitespace(text.charAt(i - 1))
+                    && followedByWhitespace(text, i, end)) {
                 return i;
             } else if (c == '\n') {
                 return -1;

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

@@ -145,6 +145,11 @@ public final class MimeUtils {
         add("application/vnd.sun.xml.writer.global", "sxg");
         add("application/vnd.sun.xml.writer.template", "stw");
         add("application/vnd.visio", "vsd");
+        // https://www.iana.org/assignments/media-types/application/vnd.tcpdump.pcap
+        add("application/vnd.tcpdump.pcap", "pcap");
+        add("application/vnd.tcpdump.pcap", "cap");
+        add("application/vnd.tcpdump.pcap", "dmp");
+        add("application/x-pcapng", "pcapng");
         add("application/x-7z-compressed", "7z");
         add("application/x-abiword", "abw");
         add("application/x-apple-diskimage", "dmg");
@@ -428,8 +433,7 @@ public final class MimeUtils {
     }
 
     // mime types that are more reliant by path
-    private static final Collection<String> PATH_PRECEDENCE_MIME_TYPE =
-            Arrays.asList("audio/x-m4b");
+    private static final Collection<String> PATH_PRECEDENCE_MIME_TYPE = List.of("audio/x-m4b");
 
     private static void add(String mimeType, String extension) {
         // If we have an existing x -> y mapping, we do not want to

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -33,7 +33,6 @@ public final class Namespace {
     public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
     public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
     public static final String HTTP_UPLOAD = "urn:xmpp:http:upload:0";
-    public static final String HTTP_UPLOAD_LEGACY = "urn:xmpp:http:upload";
     public static final String STANZA_IDS = "urn:xmpp:sid:0";
     public static final String IDLE = "urn:xmpp:idle:1";
     public static final String DATA = "jabber:x:data";

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

@@ -0,0 +1,33 @@
+package eu.siacs.conversations.xmpp;
+
+import im.conversations.android.xmpp.model.stanza.Iq;
+
+public class IqErrorResponseException extends Exception {
+
+    private final Iq response;
+
+    public IqErrorResponseException(final Iq response) {
+        super(message(response));
+        this.response = response;
+    }
+
+    public Iq getResponse() {
+        return this.response;
+    }
+
+    public static String message(final Iq iq) {
+        final var error = iq.getError();
+        if (error == null) {
+            return "missing error element in response";
+        }
+        final var text = error.getTextAsString();
+        if (text != null) {
+            return text;
+        }
+        final var condition = error.getCondition();
+        if (condition != null) {
+            return condition.getName();
+        }
+        return "no condition attached to error";
+    }
+}

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

@@ -70,6 +70,8 @@ import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.X509KeyManager;
 import javax.net.ssl.X509TrustManager;
 
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
 import de.gultsch.common.Patterns;
 import eu.siacs.conversations.AppSettings;
 import eu.siacs.conversations.BuildConfig;
@@ -150,6 +152,45 @@ import im.conversations.android.xmpp.model.streams.StreamError;
 import im.conversations.android.xmpp.model.tls.Proceed;
 import im.conversations.android.xmpp.model.tls.StartTls;
 import im.conversations.android.xmpp.processor.BindProcessor;
+import 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.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509KeyManager;
+import javax.net.ssl.X509TrustManager;
 import okhttp3.HttpUrl;
 
 public class XmppConnection implements Runnable {
@@ -2571,6 +2612,21 @@ public class XmppConnection implements Runnable {
         return String.format("%s.%s", BuildConfig.APP_NAME, CryptoHelper.random(3));
     }
 
+    public ListenableFuture<Iq> sendIqPacket(final Iq request) {
+        final SettableFuture<Iq> settable = SettableFuture.create();
+        this.sendIqPacket(
+                request,
+                response -> {
+                    final var type = response.getType();
+                    switch (type) {
+                        case RESULT -> settable.set(response);
+                        case TIMEOUT -> settable.setException(new TimeoutException());
+                        default -> settable.setException(new IqErrorResponseException(response));
+                    }
+                });
+        return settable;
+    }
+
     public String sendIqPacket(final Iq packet, final Consumer<Iq> callback) {
         return sendIqPacket(packet, callback, null);
     }
@@ -2776,6 +2832,18 @@ public class XmppConnection implements Runnable {
         }
     }
 
+    public Entry<Jid, ServiceDiscoveryResult> getServiceDiscoveryResultByFeature(
+            final String feature) {
+        synchronized (this.disco) {
+            for (final var cursor : this.disco.entrySet()) {
+                if (cursor.getValue().getFeatures().contains(feature)) {
+                    return cursor;
+                }
+            }
+            return null;
+        }
+    }
+
     public Jid findDiscoItemByFeature(final String feature) {
         final var items = findDiscoItemsByFeature(feature);
         if (items.isEmpty()) {
@@ -3208,66 +3276,54 @@ public class XmppConnection implements Runnable {
             return HttpUrl.parse(address);
         }
 
-        public boolean httpUpload(long filesize) {
+        public boolean httpUpload(long fileSize) {
             if (Config.DISABLE_HTTP_UPLOAD) {
                 return false;
+            }
+            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+            if (result == null) {
+                return false;
+            }
+            final long maxSize;
+            try {
+                maxSize =
+                        Long.parseLong(
+                                result.getValue()
+                                        .getExtendedDiscoInformation(
+                                                Namespace.HTTP_UPLOAD, "max-file-size"));
+            } catch (final Exception e) {
+                return true;
+            }
+            if (fileSize <= maxSize) {
+                return true;
             } else {
-                for (String namespace :
-                        new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
-                    List<Entry<Jid, ServiceDiscoveryResult>> items =
-                            findDiscoItemsByFeature(namespace);
-                    if (!items.isEmpty()) {
-                        try {
-                            long maxsize =
-                                    Long.parseLong(
-                                            items.get(0)
-                                                    .getValue()
-                                                    .getExtendedDiscoInformation(
-                                                            namespace, "max-file-size"));
-                            if (filesize <= maxsize) {
-                                return true;
-                            } else {
-                                Log.d(
-                                        Config.LOGTAG,
-                                        account.getJid().asBareJid()
-                                                + ": http upload is not available for files with"
-                                                + " size "
-                                                + filesize
-                                                + " (max is "
-                                                + maxsize
-                                                + ")");
-                                return false;
-                            }
-                        } catch (Exception e) {
-                            return true;
-                        }
-                    }
-                }
+                Log.d(
+                        Config.LOGTAG,
+                        account.getJid().asBareJid()
+                                + ": http upload is not available for files with"
+                                + " size "
+                                + fileSize
+                                + " (max is "
+                                + maxSize
+                                + ")");
                 return false;
             }
         }
 
-        public boolean useLegacyHttpUpload() {
-            return findDiscoItemByFeature(Namespace.HTTP_UPLOAD) == null
-                    && findDiscoItemByFeature(Namespace.HTTP_UPLOAD_LEGACY) != null;
-        }
-
         public long getMaxHttpUploadSize() {
-            for (String namespace :
-                    new String[] {Namespace.HTTP_UPLOAD, Namespace.HTTP_UPLOAD_LEGACY}) {
-                List<Entry<Jid, ServiceDiscoveryResult>> items = findDiscoItemsByFeature(namespace);
-                if (!items.isEmpty()) {
-                    try {
-                        return Long.parseLong(
-                                items.get(0)
-                                        .getValue()
-                                        .getExtendedDiscoInformation(namespace, "max-file-size"));
-                    } catch (Exception e) {
-                        // ignored
-                    }
-                }
+            final var result = getServiceDiscoveryResultByFeature(Namespace.HTTP_UPLOAD);
+            if (result == null) {
+                return -1;
+            }
+            try {
+                return Long.parseLong(
+                        result.getValue()
+                                .getExtendedDiscoInformation(
+                                        Namespace.HTTP_UPLOAD, "max-file-size"));
+            } catch (final Exception e) {
+                return -1;
+                // ignored
             }
-            return -1;
         }
 
         public boolean stanzaIds() {

src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java 🔗

@@ -73,8 +73,7 @@ public class Avatar {
 
     @Override
     public boolean equals(Object object) {
-        if (object != null && object instanceof Avatar) {
-            Avatar other = (Avatar) object;
+        if (object != null && object instanceof Avatar other) {
             return other.getFilename().equals(this.getFilename());
         } else {
             return false;

src/main/java/im/conversations/android/xmpp/model/stanza/Stanza.java 🔗

@@ -42,10 +42,7 @@ public abstract class Stanza extends StreamElement {
     public boolean isInvalid() {
         final var to = getTo();
         final var from = getFrom();
-        if (to instanceof Jid.Invalid || from instanceof Jid.Invalid) {
-            return true;
-        }
-        return false;
+        return to instanceof Jid.Invalid || from instanceof Jid.Invalid;
     }
 
     public boolean fromServer(final Account account) {

src/main/java/im/conversations/android/xmpp/model/upload/Slot.java 🔗

@@ -2,6 +2,7 @@ package im.conversations.android.xmpp.model.upload;
 
 import im.conversations.android.annotation.XmlElement;
 import im.conversations.android.xmpp.model.Extension;
+import okhttp3.HttpUrl;
 
 @XmlElement
 public class Slot extends Extension {
@@ -9,4 +10,13 @@ public class Slot extends Extension {
     public Slot() {
         super(Slot.class);
     }
+
+    public HttpUrl getGetUrl() {
+        final var get = getExtension(Get.class);
+        return get == null ? null : get.getUrl();
+    }
+
+    public Put getPut() {
+        return getExtension(Put.class);
+    }
 }

src/main/res/drawable/ic_help_center_48dp.xml 🔗

@@ -1,5 +1,13 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="48dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
-      
-    <path android:fillColor="@android:color/white" android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM12.01,18c-0.7,0 -1.26,-0.56 -1.26,-1.26c0,-0.71 0.56,-1.25 1.26,-1.25c0.71,0 1.25,0.54 1.25,1.25C13.25,17.43 12.72,18 12.01,18zM15.02,10.6c-0.76,1.11 -1.48,1.46 -1.87,2.17c-0.16,0.29 -0.22,0.48 -0.22,1.41h-1.82c0,-0.49 -0.08,-1.29 0.31,-1.98c0.49,-0.87 1.42,-1.39 1.96,-2.16c0.57,-0.81 0.25,-2.33 -1.37,-2.33c-1.06,0 -1.58,0.8 -1.8,1.48L8.56,8.49C9.01,7.15 10.22,6 11.99,6c1.48,0 2.49,0.67 3.01,1.52C15.44,8.24 15.7,9.59 15.02,10.6z"/>
-    
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:autoMirrored="true"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM12.01,18c-0.7,0 -1.26,-0.56 -1.26,-1.26c0,-0.71 0.56,-1.25 1.26,-1.25c0.71,0 1.25,0.54 1.25,1.25C13.25,17.43 12.72,18 12.01,18zM15.02,10.6c-0.76,1.11 -1.48,1.46 -1.87,2.17c-0.16,0.29 -0.22,0.48 -0.22,1.41h-1.82c0,-0.49 -0.08,-1.29 0.31,-1.98c0.49,-0.87 1.42,-1.39 1.96,-2.16c0.57,-0.81 0.25,-2.33 -1.37,-2.33c-1.06,0 -1.58,0.8 -1.8,1.48L8.56,8.49C9.01,7.15 10.22,6 11.99,6c1.48,0 2.49,0.67 3.01,1.52C15.44,8.24 15.7,9.59 15.02,10.6z" />
+
 </vector>

src/main/res/drawable/ic_lan_24dp.xml 🔗

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:tint="?colorControlNormal"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M13,22h8v-7h-3v-4h-5V9h3V2H8v7h3v2H6v4H3v7h8v-7H8v-2h8v2h-3V22zM10,7V4h4v3H10zM9,17v3H5v-3H9zM19,17v3h-4v-3H19z" />
+
+</vector>

src/main/res/layout/activity_edit_account.xml 🔗

@@ -100,6 +100,7 @@
                                 android:layout_width="fill_parent"
                                 android:layout_height="wrap_content"
                                 android:layout_marginTop="8sp"
+                                android:baselineAligned="false"
                                 android:orientation="horizontal"
                                 android:weightSum="1">
 

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

@@ -1135,4 +1135,7 @@
     <string name="copy_telephone_number">Copia numero di telefono</string>
     <string name="copy_URI">Copia URI</string>
     <string name="uri_copied_to_clipboard">URI copiato negli appunti</string>
+    <string name="account_status_service_outage_scheduled">Manutenzione programmata</string>
+    <string name="account_status_service_outage_known">Servizio in manutenzione (problema noto)</string>
+    <string name="sos_scheduled_return">Il servizio è programmato per tornare il %s</string>
 </resources>

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

@@ -1139,4 +1139,7 @@
     <string name="copied_email_address">Endereço de e-mail copiado para a área de transferência</string>
     <string name="uri_copied_to_clipboard">URI copiada para a área de transferência</string>
     <string name="copied_phone_number">Número de telefone copiado para a área de transferência</string>
+    <string name="account_status_service_outage_scheduled">Tempo de inatividade planejado</string>
+    <string name="account_status_service_outage_known">Serviço caído (problema conhecido)</string>
+    <string name="sos_scheduled_return">Se espera que o serviço volte às %s</string>
 </resources>

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

@@ -184,7 +184,7 @@
 \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>
@@ -1011,7 +1011,7 @@
     <string name="could_not_delete_account_from_server">Невозможно удалить аккаунт на сервере</string>
     <string name="delete_from_server">Удалить аккаунт на сервере</string>
     <string name="pref_autojoin">Синхронизировать закладки</string>
-    <string name="pref_autojoin_summary">Устанавливать флаг \"автоприсоединение\" при входе и выходе из MUC, и реагировать на изменения от других клиентов</string>
+    <string name="pref_autojoin_summary">Устанавливать флаг \"автоприсоединение\" при входе и выходе из группового чата, и реагировать на изменения от других клиентов</string>
     <string name="search_group_chats">Поиск конференций</string>
     <string name="download_failed_invalid_file">Загрузка не выполнена: файл испорчен</string>
     <string name="rtp_state_content_add_video">Перейти на видеовызов\?</string>

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

@@ -1155,4 +1155,7 @@
     <string name="copy_telephone_number">Копирај број телефона</string>
     <string name="copied_phone_number">Копиран број телефона у клипборд</string>
     <string name="copy_URI">Копирај URI</string>
+    <string name="account_status_service_outage_scheduled">Планирана недоступност</string>
+    <string name="account_status_service_outage_known">Сервис недоступан (познат проблем)</string>
+    <string name="sos_scheduled_return">Опоравак сервиса предвиђен у %s</string>
 </resources>

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

@@ -174,7 +174,7 @@
     <string name="account_status_stream_error">Stream error</string>
     <string name="account_status_stream_opening_error">Stream opening error</string>
     <string name="encryption_choice_unencrypted">TLS</string>
-    <string name="account_status_channel_binding">Channel binding unavailable</string>
+    <string name="account_status_channel_binding">No channel binding</string>
     <string name="encryption_choice_otr">OTR</string>
     <string name="encryption_choice_pgp">OpenPGP</string>
     <string name="encryption_choice_omemo">OMEMO</string>

src/quicksy/fastlane/metadata/android/ru-RU/full_description.txt 🔗

@@ -1,14 +1,14 @@
 Quicksy — это ответвление популярного Jabber/XMPP клиента Conversations с автоматическим нахождением контактов.
 
-Вы регистрируетесь используя свой номер телефона и Quicksy автоматически на основе телефонных номеров в вашей адресной книге предложит вам возможные контакты.
+Вы регистрируетесь, используя свой номер телефона, и Quicksy автоматически на основе телефонных номеров в вашей адресной книге предложит вам возможные контакты.
 
-По сути Quicksy — это полноценный XMPP-клиент, который позволяет вам общаться с любым пользователем на любом общедоступном федеративном сервере. Аналогично, с пользователями Quicksy можно связаться извне просто добавив +phonenumber@quicksy.im в свой список контактов.
+По сути Quicksy — это полноценный XMPP-клиент, который позволяет вам общаться с любым пользователем на любом общедоступном федеративном сервере. Аналогично, с пользователями Quicksy можно связаться извне, просто добавив +phonenumber@quicksy.im в свой список контактов.
 
 Помимо синхронизации контактов, пользовательский интерфейс намеренно максимально приближен к Conversations. Это позволяет пользователям в конечном итоге перейти с Quicksy на Conversations без необходимости заново изучать принципы работы приложения.
 
-Предлагаемые контакты включают других пользователей Quicksy и обычных пользователей XMPP которые ввели свой идентификатор XMPP в каталог Quicksy (https://quicksy.im/#get-listed).
+Предлагаемые контакты включают других пользователей Quicksy и обычных пользователей XMPP, которые внесли свой идентификатор XMPP в каталог Quicksy (https://quicksy.im/#get-listed).
 
-ПРИМЕЧАНИЕ: Чтобы ввести (https://quicksy.im/enter/) свой идентификатор XMPP в каталог Quicksy
+ПРИМЕЧАНИЕ: чтобы ввести (https://quicksy.im/enter/) свой идентификатор XMPP в каталог Quicksy,
  необходимо внести единовременный регистрационный взнос.
 
 Ознакомьтесь с Политикой конфиденциальности (https://quicksy.im/#privacy) для дополнительной информации.