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