pass omemo decrypt up to higher layers to count as download error. decrypt all encrypted files

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java | 965 
1 file changed, 481 insertions(+), 484 deletions(-)

Detailed changes

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

@@ -31,493 +31,490 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.FileWriterException;
 import eu.siacs.conversations.utils.WakeLockHelper;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import rocks.xmpp.addr.Jid;
 
 public class HttpDownloadConnection implements Transferable {
 
-	private HttpConnectionManager mHttpConnectionManager;
-	private XmppConnectionService mXmppConnectionService;
-
-	private URL mUrl;
-	private final Message message;
-	private DownloadableFile file;
-	private int mStatus = Transferable.STATUS_UNKNOWN;
-	private boolean acceptedAutomatically = false;
-	private int mProgress = 0;
-	private final boolean mUseTor;
-	private boolean canceled = false;
-	private Method method = Method.HTTP_UPLOAD;
-
-	HttpDownloadConnection(Message message, HttpConnectionManager manager) {
-		this.message = message;
-		this.mHttpConnectionManager = manager;
-		this.mXmppConnectionService = manager.getXmppConnectionService();
-		this.mUseTor = mXmppConnectionService.useTorToConnect();
-	}
-
-	@Override
-	public boolean start() {
-		if (mXmppConnectionService.hasInternetConnection()) {
-			if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
-				checkFileSize(true);
-			} else {
-				download(true);
-			}
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-	public void init(boolean interactive) {
-		if (message.isDeleted()) {
-			if (message.getType() == Message.TYPE_PRIVATE_FILE) {
-				message.setType(Message.TYPE_PRIVATE);
-			} else if (message.isFileOrImage()) {
-				message.setType(Message.TYPE_TEXT);
-			}
-			message.setDeleted(false);
-			mXmppConnectionService.updateMessage(message);
-		}
-		this.message.setTransferable(this);
-		try {
-			final Message.FileParams fileParams = message.getFileParams();
-			if (message.hasFileOnRemoteHost()) {
-				mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
-			} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
-				mUrl = fileParams.url;
-			} else {
-				mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0]));
-			}
-			final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
-			if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
-				this.message.setEncryption(Message.ENCRYPTION_PGP);
-			} else if (message.getEncryption() != Message.ENCRYPTION_OTR
-					&& message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
-				this.message.setEncryption(Message.ENCRYPTION_NONE);
-			}
-			final String ext;
-			if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
-				ext = extension.secondary;
-			} else {
-				ext = extension.main;
-			}
-			message.setRelativeFilePath(message.getUuid() + (ext != null ? ("." + ext) : ""));
-			if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
-				this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
-				Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
-			} else {
-				this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
-			}
-			final String reference = mUrl.getRef();
-			if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
-				this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
-			}
-
-			if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
-				this.message.setEncryption(Message.ENCRYPTION_NONE);
-			}
-			method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
-			long knownFileSize = message.getFileParams().size;
-			if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
-				this.file.setExpectedSize(knownFileSize);
-				download(true);
-			} else {
-				checkFileSize(interactive);
-			}
-		} catch (MalformedURLException e) {
-			this.cancel();
-		}
-	}
-
-	private void download(boolean interactive) {
-		new Thread(new FileDownloader(interactive)).start();
-	}
-
-	private void checkFileSize(boolean interactive) {
-		new Thread(new FileSizeChecker(interactive)).start();
-	}
-
-	@Override
-	public void cancel() {
-		this.canceled = true;
-		mHttpConnectionManager.finishConnection(this);
-		message.setTransferable(null);
-		if (message.isFileOrImage()) {
-			message.setDeleted(true);
-		}
-		mHttpConnectionManager.updateConversationUi(true);
-	}
-
-	private void decryptOmemoFile() {
-		final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
-
-		if (outputFile.getParentFile().mkdirs()) {
-			Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
-		}
-
-		try {
-			outputFile.createNewFile();
-			final InputStream is = new FileInputStream(this.file);
-
-			outputFile.setKey(this.file.getKey());
-			outputFile.setIv(this.file.getIv());
-			final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true);
-
-			ByteStreams.copy(is, os);
-
-			FileBackend.close(is);
-			FileBackend.close(os);
-
-			if (!file.delete()) {
-				Log.w(Config.LOGTAG,"unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
-			}
-
-			message.setRelativeFilePath(outputFile.getPath());
-		} catch (IOException e) {
-			message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
-			mXmppConnectionService.updateMessage(message);
-		}
-	}
-
-	private void finish() {
-		message.setTransferable(null);
-		mHttpConnectionManager.finishConnection(this);
-		boolean notify = acceptedAutomatically && !message.isRead();
-		if (message.getEncryption() == Message.ENCRYPTION_PGP) {
-			notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
-		}
-		mHttpConnectionManager.updateConversationUi(true);
-		final boolean notifyAfterScan = notify;
-		final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true);
-		mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> {
-			if (notifyAfterScan) {
-				mXmppConnectionService.getNotificationService().push(message);
-			}
-		});
-	}
-
-	private void decryptIfNeeded() {
-		if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL)	{
-			decryptOmemoFile();
-		}
-	}
-
-	private void changeStatus(int status) {
-		this.mStatus = status;
-		mHttpConnectionManager.updateConversationUi(true);
-	}
-
-	private void showToastForException(Exception e) {
-		if (e instanceof java.net.UnknownHostException) {
-			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
-		} else if (e instanceof java.net.ConnectException) {
-			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
-		} else if (e instanceof FileWriterException) {
-			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
-		} else if (!(e instanceof CancellationException)) {
-			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
-		}
-	}
-
-	private void updateProgress(long i) {
-		this.mProgress = (int) i;
-		mHttpConnectionManager.updateConversationUi(false);
-	}
-
-	@Override
-	public int getStatus() {
-		return this.mStatus;
-	}
-
-	@Override
-	public long getFileSize() {
-		if (this.file != null) {
-			return this.file.getExpectedSize();
-		} else {
-			return 0;
-		}
-	}
-
-	@Override
-	public int getProgress() {
-		return this.mProgress;
-	}
-
-	public Message getMessage() {
-		return message;
-	}
-
-	private class FileSizeChecker implements Runnable {
-
-		private final boolean interactive;
-
-		FileSizeChecker(boolean interactive) {
-			this.interactive = interactive;
-		}
-
-
-		@Override
-		public void run() {
-			if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
-				retrieveUrl();
-			} else {
-				check();
-			}
-		}
-
-		private void retrieveUrl() {
-			changeStatus(STATUS_CHECKING);
-			final Account account = message.getConversation().getAccount();
-			IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(Jid.of(account.getJid().getDomain()), mUrl.getHost());
-			mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> {
-				if (packet.getType() == IqPacket.TYPE.RESULT) {
-					String download = packet.query().getAttribute("download");
-					if (download != null) {
-						try {
-							mUrl = new URL(download);
-							check();
-							return;
-						} catch (MalformedURLException e) {
-							//fallthrough
-						}
-					}
-				}
-				Log.d(Config.LOGTAG,"unable to retrieve actual download url");
-				retrieveFailed(null);
-			});
-		}
-
-		private void retrieveFailed(@Nullable Exception e) {
-			changeStatus(STATUS_OFFER_CHECK_FILESIZE);
-			if (interactive) {
-				if (e != null) {
-					showToastForException(e);
-				}
-			} else {
-				HttpDownloadConnection.this.acceptedAutomatically = false;
-				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
-			}
-			cancel();
-		}
-
-		private void check() {
-			long size;
-			try {
-				size = retrieveFileSize();
-			} catch (Exception e) {
-				Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
-				retrieveFailed(e);
-				return;
-			}
-			final Message.FileParams fileParams = message.getFileParams();
-			FileBackend.updateFileParams(message, fileParams.url, size);
-			message.setOob(true);
-			mXmppConnectionService.databaseBackend.updateMessage(message, true);
-			file.setExpectedSize(size);
-			message.resetFileParams();
-			if (mHttpConnectionManager.hasStoragePermission()
-					&& size <= mHttpConnectionManager.getAutoAcceptFileSize()
-					&& mXmppConnectionService.isDataSaverDisabled()) {
-				HttpDownloadConnection.this.acceptedAutomatically = true;
-				download(interactive);
-			} else {
-				changeStatus(STATUS_OFFER);
-				HttpDownloadConnection.this.acceptedAutomatically = false;
-				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
-			}
-		}
-
-		private long retrieveFileSize() throws IOException {
-			try {
-				Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
-				changeStatus(STATUS_CHECKING);
-				HttpURLConnection connection;
-				final String hostname = mUrl.getHost();
-				final boolean onion = hostname != null && hostname.endsWith(".onion");
-				if (mUseTor || message.getConversation().getAccount().isOnion() || onion) {
-					connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
-				} else {
-					connection = (HttpURLConnection) mUrl.openConnection();
-				}
-				if (method == Method.P1_S3) {
-					connection.setRequestMethod("GET");
-					connection.addRequestProperty("Range","bytes=0-0");
-				} else {
-					connection.setRequestMethod("HEAD");
-				}
-				connection.setUseCaches(false);
-				Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
-				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
-				if (connection instanceof HttpsURLConnection) {
-					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
-				}
-				connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
-				connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
-				connection.connect();
-				String contentLength;
-				if (method == Method.P1_S3) {
-					String contentRange = connection.getHeaderField("Content-Range");
-					String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/");
-					if (contentRangeParts.length != 2) {
-						contentLength = null;
-					} else {
-						contentLength = contentRangeParts[1];
-					}
-				} else {
-					contentLength = connection.getHeaderField("Content-Length");
-				}
-				connection.disconnect();
-				if (contentLength == null) {
-					throw new IOException("no content-length found in HEAD response");
-				}
-				return Long.parseLong(contentLength, 10);
-			} catch (IOException e) {
-				Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
-				throw e;
-			} catch (NumberFormatException e) {
-				throw new IOException();
-			}
-		}
-
-	}
-
-	private class FileDownloader implements Runnable {
-
-		private final boolean interactive;
-
-		private OutputStream os;
-
-		public FileDownloader(boolean interactive) {
-			this.interactive = interactive;
-		}
-
-		@Override
-		public void run() {
-			try {
-				changeStatus(STATUS_DOWNLOADING);
-				download();
-				decryptIfNeeded();
-				updateImageBounds();
-				finish();
-			} catch (SSLHandshakeException e) {
-				changeStatus(STATUS_OFFER);
-			} catch (Exception e) {
-				if (interactive) {
-					showToastForException(e);
-				} else {
-					HttpDownloadConnection.this.acceptedAutomatically = false;
-					HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
-				}
-				cancel();
-			}
-		}
-
-		private void download() throws Exception {
-			InputStream is = null;
-			HttpURLConnection connection = null;
-			PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_" + message.getUuid());
-			try {
-				wakeLock.acquire();
-				if (mUseTor || message.getConversation().getAccount().isOnion()) {
-					connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
-				} else {
-					connection = (HttpURLConnection) mUrl.openConnection();
-				}
-				if (connection instanceof HttpsURLConnection) {
-					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
-				}
-				connection.setUseCaches(false);
-				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
-				final long expected = file.getExpectedSize();
-				final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
-				long resumeSize = 0;
-
-				if (tryResume) {
-					resumeSize = file.getSize();
-					Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
-					connection.setRequestProperty("Range", "bytes=" + resumeSize + "-");
-				}
-				connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
-				connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
-				connection.connect();
-				is = new BufferedInputStream(connection.getInputStream());
-				final String contentRange = connection.getHeaderField("Content-Range");
-				boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
-				long transmitted = 0;
-				if (tryResume && serverResumed) {
-					Log.d(Config.LOGTAG, "server resumed");
-					transmitted = file.getSize();
-					updateProgress(Math.round(((double) transmitted / expected) * 100));
-					os = AbstractConnectionManager.createOutputStream(file, true, false);
-					if (os == null) {
-						throw new FileWriterException();
-					}
-				} else {
-					long reportedContentLengthOnGet;
-					try {
-						reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
-					} catch (NumberFormatException | NullPointerException e) {
-						reportedContentLengthOnGet = 0;
-					}
-					if (expected != reportedContentLengthOnGet) {
-						Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
-					}
-					file.getParentFile().mkdirs();
-					if (!file.exists() && !file.createNewFile()) {
-						throw new FileWriterException();
-					}
-					os = AbstractConnectionManager.createOutputStream(file, false, false);
-				}
-				int count;
-				byte[] buffer = new byte[4096];
-				while ((count = is.read(buffer)) != -1) {
-					transmitted += count;
-					try {
-						os.write(buffer, 0, count);
-					} catch (IOException e) {
-						throw new FileWriterException();
-					}
-					updateProgress(Math.round(((double) transmitted / expected) * 100));
-					if (canceled) {
-						throw new CancellationException();
-					}
-				}
-				try {
-					os.flush();
-				} catch (IOException e) {
-					throw new FileWriterException();
-				}
-			} catch (CancellationException | IOException e) {
-				Log.d(Config.LOGTAG, "http download failed " + e.getMessage());
-				throw e;
-			} finally {
-				FileBackend.close(os);
-				FileBackend.close(is);
-				if (connection != null) {
-					connection.disconnect();
-				}
-				WakeLockHelper.release(wakeLock);
-			}
-		}
-
-		private void updateImageBounds() {
-			final boolean privateMessage = message.isPrivateMessage();
-			message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
-			final URL url;
-			final String ref = mUrl.getRef();
-			if (method == Method.P1_S3) {
-				url = message.getFileParams().url;
-			} else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
-				url = CryptoHelper.toAesGcmUrl(mUrl);
-			} else {
-				url = mUrl;
-			}
-			mXmppConnectionService.getFileBackend().updateFileParams(message, url);
-			mXmppConnectionService.updateMessage(message);
-		}
-
-	}
+    private final Message message;
+    private final boolean mUseTor;
+    private HttpConnectionManager mHttpConnectionManager;
+    private XmppConnectionService mXmppConnectionService;
+    private URL mUrl;
+    private DownloadableFile file;
+    private int mStatus = Transferable.STATUS_UNKNOWN;
+    private boolean acceptedAutomatically = false;
+    private int mProgress = 0;
+    private boolean canceled = false;
+    private Method method = Method.HTTP_UPLOAD;
+
+    HttpDownloadConnection(Message message, HttpConnectionManager manager) {
+        this.message = message;
+        this.mHttpConnectionManager = manager;
+        this.mXmppConnectionService = manager.getXmppConnectionService();
+        this.mUseTor = mXmppConnectionService.useTorToConnect();
+    }
+
+    @Override
+    public boolean start() {
+        if (mXmppConnectionService.hasInternetConnection()) {
+            if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
+                checkFileSize(true);
+            } else {
+                download(true);
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void init(boolean interactive) {
+        if (message.isDeleted()) {
+            if (message.getType() == Message.TYPE_PRIVATE_FILE) {
+                message.setType(Message.TYPE_PRIVATE);
+            } else if (message.isFileOrImage()) {
+                message.setType(Message.TYPE_TEXT);
+            }
+            message.setDeleted(false);
+            mXmppConnectionService.updateMessage(message);
+        }
+        this.message.setTransferable(this);
+        try {
+            final Message.FileParams fileParams = message.getFileParams();
+            if (message.hasFileOnRemoteHost()) {
+                mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
+            } else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
+                mUrl = fileParams.url;
+            } else {
+                mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0]));
+            }
+            final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
+            if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
+                this.message.setEncryption(Message.ENCRYPTION_PGP);
+            } else if (message.getEncryption() != Message.ENCRYPTION_OTR
+                    && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
+                this.message.setEncryption(Message.ENCRYPTION_NONE);
+            }
+            final String ext;
+            if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
+                ext = extension.secondary;
+            } else {
+                ext = extension.main;
+            }
+            message.setRelativeFilePath(message.getUuid() + (ext != null ? ("." + ext) : ""));
+            final String reference = mUrl.getRef();
+            if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
+                this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
+                this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
+                Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
+            } else {
+                this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
+            }
+
+
+            if (this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL && this.file.getKey() == null) {
+                this.message.setEncryption(Message.ENCRYPTION_NONE);
+            }
+            method = mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME) ? Method.P1_S3 : Method.HTTP_UPLOAD;
+            long knownFileSize = message.getFileParams().size;
+            if (knownFileSize > 0 && interactive && method != Method.P1_S3) {
+                this.file.setExpectedSize(knownFileSize);
+                download(true);
+            } else {
+                checkFileSize(interactive);
+            }
+        } catch (MalformedURLException e) {
+            this.cancel();
+        }
+    }
+
+    private void download(boolean interactive) {
+        new Thread(new FileDownloader(interactive)).start();
+    }
+
+    private void checkFileSize(boolean interactive) {
+        new Thread(new FileSizeChecker(interactive)).start();
+    }
+
+    @Override
+    public void cancel() {
+        this.canceled = true;
+        mHttpConnectionManager.finishConnection(this);
+        message.setTransferable(null);
+        if (message.isFileOrImage()) {
+            message.setDeleted(true);
+        }
+        mHttpConnectionManager.updateConversationUi(true);
+    }
+
+    private void decryptFile() throws IOException {
+        final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
+
+        if (outputFile.getParentFile().mkdirs()) {
+            Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
+        }
+
+        if (!outputFile.createNewFile()) {
+            Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath());
+        }
+
+        final InputStream is = new FileInputStream(this.file);
+
+        outputFile.setKey(this.file.getKey());
+        outputFile.setIv(this.file.getIv());
+        final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true);
+
+        ByteStreams.copy(is, os);
+
+        FileBackend.close(is);
+        FileBackend.close(os);
+
+        if (!file.delete()) {
+            Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
+        }
+
+        //TODO can this be skipped?
+        //message.setRelativeFilePath(outputFile.getPath());
+
+    }
+
+    private void finish() {
+        message.setTransferable(null);
+        mHttpConnectionManager.finishConnection(this);
+        boolean notify = acceptedAutomatically && !message.isRead();
+        if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+            notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
+        }
+        mHttpConnectionManager.updateConversationUi(true);
+        final boolean notifyAfterScan = notify;
+        final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true);
+        mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> {
+            if (notifyAfterScan) {
+                mXmppConnectionService.getNotificationService().push(message);
+            }
+        });
+    }
+
+    private void decryptIfNeeded() throws IOException {
+        if (file.getKey() != null && file.getIv() != null) {
+            decryptFile();
+        }
+    }
+
+    private void changeStatus(int status) {
+        this.mStatus = status;
+        mHttpConnectionManager.updateConversationUi(true);
+    }
+
+    private void showToastForException(Exception e) {
+        if (e instanceof java.net.UnknownHostException) {
+            mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
+        } else if (e instanceof java.net.ConnectException) {
+            mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
+        } else if (e instanceof FileWriterException) {
+            mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
+        } else if (!(e instanceof CancellationException)) {
+            mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
+        }
+    }
+
+    private void updateProgress(long i) {
+        this.mProgress = (int) i;
+        mHttpConnectionManager.updateConversationUi(false);
+    }
+
+    @Override
+    public int getStatus() {
+        return this.mStatus;
+    }
+
+    @Override
+    public long getFileSize() {
+        if (this.file != null) {
+            return this.file.getExpectedSize();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public int getProgress() {
+        return this.mProgress;
+    }
+
+    public Message getMessage() {
+        return message;
+    }
+
+    private class FileSizeChecker implements Runnable {
+
+        private final boolean interactive;
+
+        FileSizeChecker(boolean interactive) {
+            this.interactive = interactive;
+        }
+
+
+        @Override
+        public void run() {
+            if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
+                retrieveUrl();
+            } else {
+                check();
+            }
+        }
+
+        private void retrieveUrl() {
+            changeStatus(STATUS_CHECKING);
+            final Account account = message.getConversation().getAccount();
+            IqPacket request = mXmppConnectionService.getIqGenerator().requestP1S3Url(Jid.of(account.getJid().getDomain()), mUrl.getHost());
+            mXmppConnectionService.sendIqPacket(message.getConversation().getAccount(), request, (a, packet) -> {
+                if (packet.getType() == IqPacket.TYPE.RESULT) {
+                    String download = packet.query().getAttribute("download");
+                    if (download != null) {
+                        try {
+                            mUrl = new URL(download);
+                            check();
+                            return;
+                        } catch (MalformedURLException e) {
+                            //fallthrough
+                        }
+                    }
+                }
+                Log.d(Config.LOGTAG, "unable to retrieve actual download url");
+                retrieveFailed(null);
+            });
+        }
+
+        private void retrieveFailed(@Nullable Exception e) {
+            changeStatus(STATUS_OFFER_CHECK_FILESIZE);
+            if (interactive) {
+                if (e != null) {
+                    showToastForException(e);
+                }
+            } else {
+                HttpDownloadConnection.this.acceptedAutomatically = false;
+                HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+            }
+            cancel();
+        }
+
+        private void check() {
+            long size;
+            try {
+                size = retrieveFileSize();
+            } catch (Exception e) {
+                Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
+                retrieveFailed(e);
+                return;
+            }
+            final Message.FileParams fileParams = message.getFileParams();
+            FileBackend.updateFileParams(message, fileParams.url, size);
+            message.setOob(true);
+            mXmppConnectionService.databaseBackend.updateMessage(message, true);
+            file.setExpectedSize(size);
+            message.resetFileParams();
+            if (mHttpConnectionManager.hasStoragePermission()
+                    && size <= mHttpConnectionManager.getAutoAcceptFileSize()
+                    && mXmppConnectionService.isDataSaverDisabled()) {
+                HttpDownloadConnection.this.acceptedAutomatically = true;
+                download(interactive);
+            } else {
+                changeStatus(STATUS_OFFER);
+                HttpDownloadConnection.this.acceptedAutomatically = false;
+                HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+            }
+        }
+
+        private long retrieveFileSize() throws IOException {
+            try {
+                Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
+                changeStatus(STATUS_CHECKING);
+                HttpURLConnection connection;
+                final String hostname = mUrl.getHost();
+                final boolean onion = hostname != null && hostname.endsWith(".onion");
+                if (mUseTor || message.getConversation().getAccount().isOnion() || onion) {
+                    connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
+                } else {
+                    connection = (HttpURLConnection) mUrl.openConnection();
+                }
+                if (method == Method.P1_S3) {
+                    connection.setRequestMethod("GET");
+                    connection.addRequestProperty("Range", "bytes=0-0");
+                } else {
+                    connection.setRequestMethod("HEAD");
+                }
+                connection.setUseCaches(false);
+                Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
+                connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
+                if (connection instanceof HttpsURLConnection) {
+                    mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+                }
+                connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
+                connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
+                connection.connect();
+                String contentLength;
+                if (method == Method.P1_S3) {
+                    String contentRange = connection.getHeaderField("Content-Range");
+                    String[] contentRangeParts = contentRange == null ? new String[0] : contentRange.split("/");
+                    if (contentRangeParts.length != 2) {
+                        contentLength = null;
+                    } else {
+                        contentLength = contentRangeParts[1];
+                    }
+                } else {
+                    contentLength = connection.getHeaderField("Content-Length");
+                }
+                connection.disconnect();
+                if (contentLength == null) {
+                    throw new IOException("no content-length found in HEAD response");
+                }
+                return Long.parseLong(contentLength, 10);
+            } catch (IOException e) {
+                Log.d(Config.LOGTAG, "io exception during HEAD " + e.getMessage());
+                throw e;
+            } catch (NumberFormatException e) {
+                throw new IOException();
+            }
+        }
+
+    }
+
+    private class FileDownloader implements Runnable {
+
+        private final boolean interactive;
+
+        private OutputStream os;
+
+        public FileDownloader(boolean interactive) {
+            this.interactive = interactive;
+        }
+
+        @Override
+        public void run() {
+            try {
+                changeStatus(STATUS_DOWNLOADING);
+                download();
+                decryptIfNeeded();
+                updateImageBounds();
+                finish();
+            } catch (SSLHandshakeException e) {
+                changeStatus(STATUS_OFFER);
+            } catch (Exception e) {
+                if (interactive) {
+                    showToastForException(e);
+                } else {
+                    HttpDownloadConnection.this.acceptedAutomatically = false;
+                    HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
+                }
+                cancel();
+            }
+        }
+
+        private void download() throws Exception {
+            InputStream is = null;
+            HttpURLConnection connection = null;
+            PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_" + message.getUuid());
+            try {
+                wakeLock.acquire();
+                if (mUseTor || message.getConversation().getAccount().isOnion()) {
+                    connection = (HttpURLConnection) mUrl.openConnection(HttpConnectionManager.getProxy());
+                } else {
+                    connection = (HttpURLConnection) mUrl.openConnection();
+                }
+                if (connection instanceof HttpsURLConnection) {
+                    mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
+                }
+                connection.setUseCaches(false);
+                connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
+                final long expected = file.getExpectedSize();
+                final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
+                long resumeSize = 0;
+
+                if (tryResume) {
+                    resumeSize = file.getSize();
+                    Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
+                    connection.setRequestProperty("Range", "bytes=" + resumeSize + "-");
+                }
+                connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
+                connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
+                connection.connect();
+                is = new BufferedInputStream(connection.getInputStream());
+                final String contentRange = connection.getHeaderField("Content-Range");
+                boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes " + resumeSize + "-");
+                long transmitted = 0;
+                if (tryResume && serverResumed) {
+                    Log.d(Config.LOGTAG, "server resumed");
+                    transmitted = file.getSize();
+                    updateProgress(Math.round(((double) transmitted / expected) * 100));
+                    os = AbstractConnectionManager.createOutputStream(file, true, false);
+                    if (os == null) {
+                        throw new FileWriterException();
+                    }
+                } else {
+                    long reportedContentLengthOnGet;
+                    try {
+                        reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
+                    } catch (NumberFormatException | NullPointerException e) {
+                        reportedContentLengthOnGet = 0;
+                    }
+                    if (expected != reportedContentLengthOnGet) {
+                        Log.d(Config.LOGTAG, "content-length reported on GET (" + reportedContentLengthOnGet + ") did not match Content-Length reported on HEAD (" + expected + ")");
+                    }
+                    file.getParentFile().mkdirs();
+                    if (!file.exists() && !file.createNewFile()) {
+                        throw new FileWriterException();
+                    }
+                    os = AbstractConnectionManager.createOutputStream(file, false, false);
+                }
+                int count;
+                byte[] buffer = new byte[4096];
+                while ((count = is.read(buffer)) != -1) {
+                    transmitted += count;
+                    try {
+                        os.write(buffer, 0, count);
+                    } catch (IOException e) {
+                        throw new FileWriterException();
+                    }
+                    updateProgress(Math.round(((double) transmitted / expected) * 100));
+                    if (canceled) {
+                        throw new CancellationException();
+                    }
+                }
+                try {
+                    os.flush();
+                } catch (IOException e) {
+                    throw new FileWriterException();
+                }
+            } catch (CancellationException | IOException e) {
+                Log.d(Config.LOGTAG, "http download failed " + e.getMessage());
+                throw e;
+            } finally {
+                FileBackend.close(os);
+                FileBackend.close(is);
+                if (connection != null) {
+                    connection.disconnect();
+                }
+                WakeLockHelper.release(wakeLock);
+            }
+        }
+
+        private void updateImageBounds() {
+            final boolean privateMessage = message.isPrivateMessage();
+            message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : Message.TYPE_FILE);
+            final URL url;
+            final String ref = mUrl.getRef();
+            if (method == Method.P1_S3) {
+                url = message.getFileParams().url;
+            } else if (ref != null && AesGcmURLStreamHandler.IV_KEY.matcher(ref).matches()) {
+                url = CryptoHelper.toAesGcmUrl(mUrl);
+            } else {
+                url = mUrl;
+            }
+            mXmppConnectionService.getFileBackend().updateFileParams(message, url);
+            mXmppConnectionService.updateMessage(message);
+        }
+
+    }
 }