refactor HTTP slot requester

Daniel Gultsch created

make use of future api; get rid of http upload legacy

Change summary

src/main/java/eu/siacs/conversations/generator/IqGenerator.java          |  21 
src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java     |   6 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java      | 171 
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/services/XmppConnectionService.java |  10 
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            | 120 
src/main/java/im/conversations/android/xmpp/model/upload/Slot.java       |  10 
11 files changed, 269 insertions(+), 290 deletions(-)

Detailed changes

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

@@ -18,6 +18,7 @@ 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;
@@ -438,23 +439,13 @@ public class IqGenerator extends AbstractGenerator {
         return packet;
     }
 
-    public Iq requestHttpUploadSlot(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);
-        request.setAttribute("filename", convertFilename(file.getName()));
-        request.setAttribute("size", file.getExpectedSize());
-        request.setAttribute("content-type", mime);
-        return packet;
-    }
-
-    public Iq requestHttpUploadLegacySlot(Jid host, DownloadableFile file, 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 🔗

@@ -114,11 +114,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
                     return;
                 }
             }
-            HttpUploadConnection connection =
-                    new HttpUploadConnection(
-                            message,
-                            Method.determine(message.getConversation().getAccount()),
-                            this);
+            HttpUploadConnection connection = new HttpUploadConnection(message, this);
             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;
@@ -53,9 +46,9 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
     private Call mostRecentCall;
     private ListenableFuture<SlotRequester.Slot> slotFuture;
 
-    public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
+    public HttpUploadConnection(
+            final Message message, final HttpConnectionManager httpConnectionManager) {
         this.message = message;
-        this.method = method;
         this.mHttpConnectionManager = httpConnectionManager;
         this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
     }
@@ -88,13 +81,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");
         }
     }
 
@@ -102,8 +95,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);
     }
 
     private void finish() {
@@ -115,7 +113,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();
@@ -130,75 +129,85 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
         }
         this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
         message.resetFileParams();
-        this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, 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());
-                }
-            }
+        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);
+                        } 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);
-                } 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() {
@@ -210,4 +219,4 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
         this.transmitted = progress;
         mHttpConnectionManager.updateConversationUi(false);
     }
-}
+}

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 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, 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;
-                    }
-                }
-            }
-            future.setException(new IqResponseException(IqParser.extractErrorMessage(packet)));
-        });
-        return future;
-    }
-
-    private ListenableFuture<Slot> requestHttpUpload(Account account, Jid host, DownloadableFile file, String mime) {
-        final SettableFuture<Slot> future = SettableFuture.create();
+    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);
-        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 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.toString());
+                        throw new IllegalStateException("Slot not found in IQ response");
+                    }
+                    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/services/XmppConnectionService.java 🔗

@@ -60,6 +60,7 @@ import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 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;
@@ -169,6 +170,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;
@@ -5958,6 +5960,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) {
         final XmppConnection connection = account.getXmppConnection();
         if (connection != null) {

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 🔗

@@ -21,6 +21,8 @@ import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.primitives.Ints;
+import 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;
@@ -128,6 +130,7 @@ 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;
@@ -2533,6 +2536,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) {
         packet.setFrom(account.getJid());
         return this.sendUnmodifiedIqPacket(packet, callback, false);
@@ -2720,6 +2738,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()) {
@@ -3152,66 +3182,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/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);
+    }
 }