HttpDownloadConnection.java

  1package eu.siacs.conversations.http;
  2
  3import android.os.PowerManager;
  4import android.util.Log;
  5
  6import java.io.BufferedInputStream;
  7import java.io.IOException;
  8import java.io.InputStream;
  9import java.io.OutputStream;
 10import java.net.HttpURLConnection;
 11import java.net.MalformedURLException;
 12import java.net.URL;
 13import java.util.concurrent.CancellationException;
 14
 15import javax.net.ssl.HttpsURLConnection;
 16import javax.net.ssl.SSLHandshakeException;
 17
 18import eu.siacs.conversations.Config;
 19import eu.siacs.conversations.R;
 20import eu.siacs.conversations.entities.DownloadableFile;
 21import eu.siacs.conversations.entities.Message;
 22import eu.siacs.conversations.entities.Transferable;
 23import eu.siacs.conversations.entities.TransferablePlaceholder;
 24import eu.siacs.conversations.persistance.FileBackend;
 25import eu.siacs.conversations.services.AbstractConnectionManager;
 26import eu.siacs.conversations.services.XmppConnectionService;
 27import eu.siacs.conversations.utils.CryptoHelper;
 28import eu.siacs.conversations.utils.FileWriterException;
 29
 30public class HttpDownloadConnection implements Transferable {
 31
 32	private HttpConnectionManager mHttpConnectionManager;
 33	private XmppConnectionService mXmppConnectionService;
 34
 35	private URL mUrl;
 36	private Message message;
 37	private DownloadableFile file;
 38	private int mStatus = Transferable.STATUS_UNKNOWN;
 39	private boolean acceptedAutomatically = false;
 40	private int mProgress = 0;
 41	private boolean mUseTor = false;
 42	private boolean canceled = false;
 43
 44	public HttpDownloadConnection(HttpConnectionManager manager) {
 45		this.mHttpConnectionManager = manager;
 46		this.mXmppConnectionService = manager.getXmppConnectionService();
 47		this.mUseTor = mXmppConnectionService.useTorToConnect();
 48	}
 49
 50	@Override
 51	public boolean start() {
 52		if (mXmppConnectionService.hasInternetConnection()) {
 53			if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
 54				checkFileSize(true);
 55			} else {
 56				new Thread(new FileDownloader(true)).start();
 57			}
 58			return true;
 59		} else {
 60			return false;
 61		}
 62	}
 63
 64	public void init(Message message) {
 65		init(message, false);
 66	}
 67
 68	public void init(Message message, boolean interactive) {
 69		this.message = message;
 70		this.message.setTransferable(this);
 71		try {
 72			if (message.hasFileOnRemoteHost()) {
 73				mUrl = CryptoHelper.toHttpsUrl(message.getFileParams().url);
 74			} else {
 75				mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody()));
 76			}
 77			String[] parts = mUrl.getPath().toLowerCase().split("\\.");
 78			String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
 79			String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
 80			if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
 81				this.message.setEncryption(Message.ENCRYPTION_PGP);
 82			} else if (message.getEncryption() != Message.ENCRYPTION_OTR
 83					&& message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
 84				this.message.setEncryption(Message.ENCRYPTION_NONE);
 85			}
 86			String extension;
 87			if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
 88				extension = secondToLast;
 89			} else {
 90				extension = lastPart;
 91			}
 92			message.setRelativeFilePath(message.getUuid() + "." + extension);
 93			this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
 94			final String reference = mUrl.getRef();
 95			if (reference != null && reference.matches("([A-Fa-f0-9]{2}){48}")) {
 96				this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
 97			}
 98
 99			if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
100					|| this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
101					&& this.file.getKey() == null) {
102				this.message.setEncryption(Message.ENCRYPTION_NONE);
103			}
104			checkFileSize(interactive);
105		} catch (MalformedURLException e) {
106			this.cancel();
107		}
108	}
109
110	private void checkFileSize(boolean interactive) {
111		new Thread(new FileSizeChecker(interactive)).start();
112	}
113
114	@Override
115	public void cancel() {
116		this.canceled = true;
117		mHttpConnectionManager.finishConnection(this);
118		if (message.isFileOrImage()) {
119			message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
120		} else {
121			message.setTransferable(null);
122		}
123		mHttpConnectionManager.updateConversationUi(true);
124	}
125
126	private void finish() {
127		mXmppConnectionService.getFileBackend().updateMediaScanner(file);
128		message.setTransferable(null);
129		mHttpConnectionManager.finishConnection(this);
130		boolean notify = acceptedAutomatically && !message.isRead();
131		if (message.getEncryption() == Message.ENCRYPTION_PGP) {
132			notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
133		}
134		mHttpConnectionManager.updateConversationUi(true);
135		if (notify) {
136			mXmppConnectionService.getNotificationService().push(message);
137		}
138	}
139
140	private void changeStatus(int status) {
141		this.mStatus = status;
142		mHttpConnectionManager.updateConversationUi(true);
143	}
144
145	private void showToastForException(Exception e) {
146		if (e instanceof java.net.UnknownHostException) {
147			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
148		} else if (e instanceof java.net.ConnectException) {
149			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
150		} else if (e instanceof FileWriterException) {
151			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
152		} else if (!(e instanceof  CancellationException)) {
153			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
154		}
155	}
156
157	private class FileSizeChecker implements Runnable {
158
159		private boolean interactive = false;
160
161		public FileSizeChecker(boolean interactive) {
162			this.interactive = interactive;
163		}
164
165		@Override
166		public void run() {
167			long size;
168			try {
169				size = retrieveFileSize();
170			} catch (Exception e) {
171				changeStatus(STATUS_OFFER_CHECK_FILESIZE);
172				Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
173				if (interactive) {
174					showToastForException(e);
175				} else {
176					HttpDownloadConnection.this.acceptedAutomatically = false;
177					HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
178				}
179				cancel();
180				return;
181			}
182			file.setExpectedSize(size);
183			if (mHttpConnectionManager.hasStoragePermission()
184					&& size <= mHttpConnectionManager.getAutoAcceptFileSize()
185					&& mXmppConnectionService.isDataSaverDisabled()) {
186				HttpDownloadConnection.this.acceptedAutomatically = true;
187				new Thread(new FileDownloader(interactive)).start();
188			} else {
189				changeStatus(STATUS_OFFER);
190				HttpDownloadConnection.this.acceptedAutomatically = false;
191				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
192			}
193		}
194
195		private long retrieveFileSize() throws IOException {
196			try {
197				Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
198				changeStatus(STATUS_CHECKING);
199				HttpURLConnection connection;
200				if (mUseTor) {
201					connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy());
202				} else {
203					connection = (HttpURLConnection) mUrl.openConnection();
204				}
205				connection.setRequestMethod("HEAD");
206				Log.d(Config.LOGTAG,"url: "+connection.getURL().toString());
207				Log.d(Config.LOGTAG,"connection: "+connection.toString());
208				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
209				if (connection instanceof HttpsURLConnection) {
210					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
211				}
212				connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
213				connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
214				connection.connect();
215				String contentLength = connection.getHeaderField("Content-Length");
216				connection.disconnect();
217				if (contentLength == null) {
218					throw new IOException("no content-length found in HEAD response");
219				}
220				return Long.parseLong(contentLength, 10);
221			} catch (IOException e) {
222				Log.d(Config.LOGTAG,"io exception during HEAD "+e.getMessage());
223				throw e;
224			} catch (NumberFormatException e) {
225				throw new IOException();
226			}
227		}
228
229	}
230
231	private class FileDownloader implements Runnable {
232
233		private boolean interactive = false;
234
235		private OutputStream os;
236
237		public FileDownloader(boolean interactive) {
238			this.interactive = interactive;
239		}
240
241		@Override
242		public void run() {
243			try {
244				changeStatus(STATUS_DOWNLOADING);
245				download();
246				updateImageBounds();
247				finish();
248			} catch (SSLHandshakeException e) {
249				changeStatus(STATUS_OFFER);
250			} catch (Exception e) {
251				if (interactive) {
252					showToastForException(e);
253				} else {
254					HttpDownloadConnection.this.acceptedAutomatically = false;
255					HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
256				}
257				cancel();
258			}
259		}
260
261		private void download()  throws Exception {
262			InputStream is = null;
263			HttpURLConnection connection = null;
264			PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
265			try {
266				wakeLock.acquire();
267				if (mUseTor) {
268					connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy());
269				} else {
270					connection = (HttpURLConnection) mUrl.openConnection();
271				}
272				if (connection instanceof HttpsURLConnection) {
273					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
274				}
275				connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
276				final boolean tryResume = file.exists() && file.getKey() == null;
277				long resumeSize = 0;
278				if (tryResume) {
279					Log.d(Config.LOGTAG,"http download trying resume");
280					resumeSize = file.getSize();
281					connection.setRequestProperty("Range", "bytes="+resumeSize+"-");
282				}
283				connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
284				connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
285				connection.connect();
286				is = new BufferedInputStream(connection.getInputStream());
287				final String contentRange = connection.getHeaderField("Content-Range");
288				boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes "+resumeSize+"-");
289				long transmitted = 0;
290				long expected = file.getExpectedSize();
291				if (tryResume && serverResumed) {
292					Log.d(Config.LOGTAG,"server resumed");
293					transmitted = file.getSize();
294					updateProgress((int) ((((double) transmitted) / expected) * 100));
295					os = AbstractConnectionManager.createAppendedOutputStream(file);
296					if (os == null) {
297						throw new FileWriterException();
298					}
299				} else {
300					file.getParentFile().mkdirs();
301					if (!file.exists() && !file.createNewFile()) {
302						throw new FileWriterException();
303					}
304					os = AbstractConnectionManager.createOutputStream(file, true);
305				}
306				int count;
307				byte[] buffer = new byte[1024];
308				while ((count = is.read(buffer)) != -1) {
309					transmitted += count;
310					try {
311						os.write(buffer, 0, count);
312					} catch (IOException e) {
313						throw new FileWriterException();
314					}
315					updateProgress((int) ((((double) transmitted) / expected) * 100));
316					if (canceled) {
317						throw new CancellationException();
318					}
319				}
320				try {
321					os.flush();
322				} catch (IOException e) {
323					throw new FileWriterException();
324				}
325			} catch (CancellationException | IOException e) {
326				Log.d(Config.LOGTAG,"http download failed "+e.getMessage());
327				throw e;
328			} finally {
329				FileBackend.close(os);
330				FileBackend.close(is);
331				if (connection != null) {
332					connection.disconnect();
333				}
334				wakeLock.release();
335			}
336		}
337
338		private void updateImageBounds() {
339			message.setType(Message.TYPE_FILE);
340			final URL url;
341			final String ref = mUrl.getRef();
342			if (ref != null && ref.matches("([A-Fa-f0-9]{2}){48}")) {
343				url = CryptoHelper.toAesGcmUrl(mUrl);
344			} else {
345				url = mUrl;
346			}
347			mXmppConnectionService.getFileBackend().updateFileParams(message, url);
348			mXmppConnectionService.updateMessage(message);
349		}
350
351	}
352
353	public void updateProgress(int i) {
354		this.mProgress = i;
355		mHttpConnectionManager.updateConversationUi(false);
356	}
357
358	@Override
359	public int getStatus() {
360		return this.mStatus;
361	}
362
363	@Override
364	public long getFileSize() {
365		if (this.file != null) {
366			return this.file.getExpectedSize();
367		} else {
368			return 0;
369		}
370	}
371
372	@Override
373	public int getProgress() {
374		return this.mProgress;
375	}
376}