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		if (message.getEncryption() == Message.ENCRYPTION_PGP) {
130			message.getConversation().getAccount().getPgpDecryptionService().add(message);
131		}
132		mXmppConnectionService.updateConversationUi();
133		if (acceptedAutomatically) {
134			mXmppConnectionService.getNotificationService().push(message);
135		}
136	}
137
138	private void changeStatus(int status) {
139		this.mStatus = status;
140		mXmppConnectionService.updateConversationUi();
141	}
142
143	private void showToastForException(Exception e) {
144		e.printStackTrace();
145		if (e instanceof java.net.UnknownHostException) {
146			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
147		} else if (e instanceof java.net.ConnectException) {
148			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
149		} else if (!(e instanceof  CancellationException)) {
150			mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
151		}
152	}
153
154	private class FileSizeChecker implements Runnable {
155
156		private boolean interactive = false;
157
158		public FileSizeChecker(boolean interactive) {
159			this.interactive = interactive;
160		}
161
162		@Override
163		public void run() {
164			long size;
165			try {
166				size = retrieveFileSize();
167			} catch (SSLHandshakeException e) {
168				changeStatus(STATUS_OFFER_CHECK_FILESIZE);
169				HttpDownloadConnection.this.acceptedAutomatically = false;
170				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
171				return;
172			} catch (IOException e) {
173				Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
174				if (interactive) {
175					showToastForException(e);
176				}
177				cancel();
178				return;
179			}
180			file.setExpectedSize(size);
181			if (mHttpConnectionManager.hasStoragePermission() && size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
182				HttpDownloadConnection.this.acceptedAutomatically = true;
183				new Thread(new FileDownloader(interactive)).start();
184			} else {
185				changeStatus(STATUS_OFFER);
186				HttpDownloadConnection.this.acceptedAutomatically = false;
187				HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
188			}
189		}
190
191		private long retrieveFileSize() throws IOException {
192			try {
193				Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
194				changeStatus(STATUS_CHECKING);
195				HttpURLConnection connection;
196				if (mUseTor) {
197					connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy());
198				} else {
199					connection = (HttpURLConnection) mUrl.openConnection();
200				}
201				connection.setRequestMethod("HEAD");
202				Log.d(Config.LOGTAG,"url: "+connection.getURL().toString());
203				Log.d(Config.LOGTAG,"connection: "+connection.toString());
204				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
205				if (connection instanceof HttpsURLConnection) {
206					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
207				}
208				connection.connect();
209				String contentLength = connection.getHeaderField("Content-Length");
210				connection.disconnect();
211				if (contentLength == null) {
212					throw new IOException();
213				}
214				return Long.parseLong(contentLength, 10);
215			} catch (IOException e) {
216				throw e;
217			} catch (NumberFormatException e) {
218				throw new IOException();
219			}
220		}
221
222	}
223
224	private class FileDownloader implements Runnable {
225
226		private boolean interactive = false;
227
228		private OutputStream os;
229
230		public FileDownloader(boolean interactive) {
231			this.interactive = interactive;
232		}
233
234		@Override
235		public void run() {
236			try {
237				changeStatus(STATUS_DOWNLOADING);
238				download();
239				updateImageBounds();
240				finish();
241			} catch (SSLHandshakeException e) {
242				changeStatus(STATUS_OFFER);
243			} catch (Exception e) {
244				if (interactive) {
245					showToastForException(e);
246				}
247				cancel();
248			}
249		}
250
251		private void download()  throws Exception {
252			InputStream is = null;
253			PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
254			try {
255				wakeLock.acquire();
256				HttpURLConnection connection;
257				if (mUseTor) {
258					connection = (HttpURLConnection) mUrl.openConnection(mHttpConnectionManager.getProxy());
259				} else {
260					connection = (HttpURLConnection) mUrl.openConnection();
261				}
262				if (connection instanceof HttpsURLConnection) {
263					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
264				}
265				connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
266				final boolean tryResume = file.exists() && file.getKey() == null;
267				if (tryResume) {
268					Log.d(Config.LOGTAG,"http download trying resume");
269					long size = file.getSize();
270					connection.setRequestProperty("Range", "bytes="+size+"-");
271				}
272				connection.connect();
273				is = new BufferedInputStream(connection.getInputStream());
274				boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges"));
275				long transmitted = 0;
276				long expected = file.getExpectedSize();
277				if (tryResume && serverResumed) {
278					Log.d(Config.LOGTAG,"server resumed");
279					transmitted = file.getSize();
280					updateProgress((int) ((((double) transmitted) / expected) * 100));
281					os = AbstractConnectionManager.createAppendedOutputStream(file);
282				} else {
283					file.getParentFile().mkdirs();
284					file.createNewFile();
285					os = AbstractConnectionManager.createOutputStream(file, true);
286				}
287				int count = -1;
288				byte[] buffer = new byte[1024];
289				while ((count = is.read(buffer)) != -1) {
290					transmitted += count;
291					os.write(buffer, 0, count);
292					updateProgress((int) ((((double) transmitted) / expected) * 100));
293					if (canceled) {
294						throw new CancellationException();
295					}
296				}
297			} catch (CancellationException | IOException e) {
298				throw e;
299			} finally {
300				if (os != null) {
301					try {
302						os.flush();
303					} catch (final IOException ignored) {
304
305					}
306				}
307				FileBackend.close(os);
308				FileBackend.close(is);
309				wakeLock.release();
310			}
311		}
312
313		private void updateImageBounds() {
314			message.setType(Message.TYPE_FILE);
315			mXmppConnectionService.getFileBackend().updateFileParams(message, mUrl);
316			mXmppConnectionService.updateMessage(message);
317		}
318
319	}
320
321	public void updateProgress(int i) {
322		this.mProgress = i;
323		mXmppConnectionService.updateConversationUi();
324	}
325
326	@Override
327	public int getStatus() {
328		return this.mStatus;
329	}
330
331	@Override
332	public long getFileSize() {
333		if (this.file != null) {
334			return this.file.getExpectedSize();
335		} else {
336			return 0;
337		}
338	}
339
340	@Override
341	public int getProgress() {
342		return this.mProgress;
343	}
344}