HttpDownloadConnection.java

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