HttpUploadConnection.java

  1package eu.siacs.conversations.http;
  2
  3import android.util.Log;
  4
  5import com.google.common.util.concurrent.FutureCallback;
  6import com.google.common.util.concurrent.Futures;
  7import com.google.common.util.concurrent.ListenableFuture;
  8import com.google.common.util.concurrent.MoreExecutors;
  9
 10import org.checkerframework.checker.nullness.compatqual.NullableDecl;
 11import org.jetbrains.annotations.NotNull;
 12
 13import java.io.IOException;
 14import java.util.Arrays;
 15import java.util.List;
 16import java.util.concurrent.Future;
 17
 18import eu.siacs.conversations.Config;
 19import eu.siacs.conversations.entities.Account;
 20import eu.siacs.conversations.entities.DownloadableFile;
 21import eu.siacs.conversations.entities.Message;
 22import eu.siacs.conversations.entities.Transferable;
 23import eu.siacs.conversations.services.AbstractConnectionManager;
 24import eu.siacs.conversations.services.XmppConnectionService;
 25import eu.siacs.conversations.utils.CryptoHelper;
 26import okhttp3.Call;
 27import okhttp3.Callback;
 28import okhttp3.OkHttpClient;
 29import okhttp3.Request;
 30import okhttp3.RequestBody;
 31import okhttp3.Response;
 32
 33public class HttpUploadConnection implements Transferable, AbstractConnectionManager.ProgressListener {
 34
 35    static final List<String> WHITE_LISTED_HEADERS = Arrays.asList(
 36            "Authorization",
 37            "Cookie",
 38            "Expires"
 39    );
 40
 41    private final HttpConnectionManager mHttpConnectionManager;
 42    private final XmppConnectionService mXmppConnectionService;
 43    private final Method method;
 44    private boolean delayed = false;
 45    private DownloadableFile file;
 46    private final Message message;
 47    private SlotRequester.Slot slot;
 48    private byte[] key = null;
 49
 50    private long transmitted = 0;
 51    private Call mostRecentCall;
 52    private ListenableFuture<SlotRequester.Slot> slotFuture;
 53
 54    public HttpUploadConnection(Message message, Method method, HttpConnectionManager httpConnectionManager) {
 55        this.message = message;
 56        this.method = method;
 57        this.mHttpConnectionManager = httpConnectionManager;
 58        this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
 59    }
 60
 61    @Override
 62    public boolean start() {
 63        return false;
 64    }
 65
 66    @Override
 67    public int getStatus() {
 68        return STATUS_UPLOADING;
 69    }
 70
 71    @Override
 72    public Long getFileSize() {
 73        return file == null ? null : file.getExpectedSize();
 74    }
 75
 76    @Override
 77    public int getProgress() {
 78        if (file == null) {
 79            return 0;
 80        }
 81        return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
 82    }
 83
 84    @Override
 85    public void cancel() {
 86        final ListenableFuture<SlotRequester.Slot> slotFuture = this.slotFuture;
 87        if (slotFuture != null && !slotFuture.isDone()) {
 88            if (slotFuture.cancel(true)) {
 89                Log.d(Config.LOGTAG,"cancelled slot requester");
 90            }
 91        }
 92        final Call call = this.mostRecentCall;
 93        if (call != null && !call.isCanceled()) {
 94            call.cancel();
 95            Log.d(Config.LOGTAG,"cancelled HTTP request");
 96        }
 97    }
 98
 99    private void fail(String errorMessage) {
100        finish();
101        final Call call = this.mostRecentCall;
102        final Future<SlotRequester.Slot> slotFuture = this.slotFuture;
103        final boolean cancelled = (call != null && call.isCanceled()) || (slotFuture != null && slotFuture.isCancelled());
104        mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, cancelled ? Message.ERROR_MESSAGE_CANCELLED : errorMessage);
105    }
106
107    private void finish() {
108        mHttpConnectionManager.finishUploadConnection(this);
109        message.setTransferable(null);
110    }
111
112    public void init(boolean delay) {
113        final Account account = message.getConversation().getAccount();
114        this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
115        final String mime;
116        if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
117            mime = "application/pgp-encrypted";
118        } else {
119            mime = this.file.getMimeType();
120        }
121        final long originalFileSize = file.getSize();
122        this.delayed = delay;
123        if (Config.ENCRYPT_ON_HTTP_UPLOADED
124                || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
125                || message.getEncryption() == Message.ENCRYPTION_OTR) {
126            this.key = new byte[44];
127            mXmppConnectionService.getRNG().nextBytes(this.key);
128            this.file.setKeyAndIv(this.key);
129        }
130        this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
131        message.resetFileParams();
132        this.slotFuture = new SlotRequester(mXmppConnectionService).request(method, account, file, mime);
133        Futures.addCallback(this.slotFuture, new FutureCallback<SlotRequester.Slot>() {
134            @Override
135            public void onSuccess(@NullableDecl SlotRequester.Slot result) {
136                HttpUploadConnection.this.slot = result;
137                try {
138                    HttpUploadConnection.this.upload();
139                } catch (final Exception e) {
140                    fail(e.getMessage());
141                }
142            }
143
144            @Override
145            public void onFailure(@NotNull final Throwable throwable) {
146                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to request slot", throwable);
147                fail(throwable.getMessage());
148            }
149        }, MoreExecutors.directExecutor());
150        message.setTransferable(this);
151        mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
152    }
153
154    private void upload() {
155        final OkHttpClient client = mHttpConnectionManager.buildHttpClient(
156                slot.put,
157                message.getConversation().getAccount(),
158                0,
159                true
160        );
161        final RequestBody requestBody = AbstractConnectionManager.requestBody(file, this);
162        final Request request = new Request.Builder()
163                .url(slot.put)
164                .put(requestBody)
165                .headers(slot.headers)
166                .build();
167        Log.d(Config.LOGTAG, "uploading file to " + slot.put);
168        this.mostRecentCall = client.newCall(request);
169        this.mostRecentCall.enqueue(new Callback() {
170            @Override
171            public void onFailure(@NotNull Call call, IOException e) {
172                Log.d(Config.LOGTAG, "http upload failed", e);
173                fail(e.getMessage());
174            }
175
176            @Override
177            public void onResponse(@NotNull Call call, @NotNull Response response)  {
178                final int code = response.code();
179                if (code == 200 || code == 201) {
180                    Log.d(Config.LOGTAG, "finished uploading file");
181                    final String get;
182                    if (key != null) {
183                        get = AesGcmURL.toAesGcmUrl(slot.get.newBuilder().fragment(CryptoHelper.bytesToHex(key)).build());
184                    } else {
185                        get = slot.get.toString();
186                    }
187                    mXmppConnectionService.getFileBackend().updateFileParams(message, get);
188                    mXmppConnectionService.getFileBackend().updateMediaScanner(file);
189                    finish();
190                    if (!message.isPrivateMessage()) {
191                        message.setCounterpart(message.getConversation().getJid().asBareJid());
192                    }
193                    mXmppConnectionService.resendMessage(message, delayed);
194                } else {
195                    Log.d(Config.LOGTAG, "http upload failed because response code was " + code);
196                    fail("http upload failed because response code was " + code);
197                }
198            }
199        });
200    }
201
202    public Message getMessage() {
203        return message;
204    }
205
206    @Override
207    public void onProgress(final long progress) {
208        this.transmitted = progress;
209        mHttpConnectionManager.updateConversationUi(false);
210    }
211}