package de.thedevstack.conversationsplus.http; import android.os.PowerManager; import android.util.Log; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.CancellationException; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.enums.FileStatus; import de.thedevstack.conversationsplus.exceptions.RemoteFileNotFoundException; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.entities.TransferablePlaceholder; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.FileUtils; import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; public class HttpDownloadConnection implements Transferable { private HttpConnectionManager mHttpConnectionManager; private XmppConnectionService mXmppConnectionService; private URL mUrl; private Message message; private DownloadableFile file; private int mStatus = Transferable.STATUS_UNKNOWN; private boolean acceptedAutomatically = false; private int mProgress = 0; private boolean canceled = false; public HttpDownloadConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService; } @Override public boolean start() { if (mXmppConnectionService.hasInternetConnection()) { if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) { checkFileSize(true); } else { new Thread(new FileDownloader(true)).start(); } return true; } else { return false; } } public void init(Message message) { init(message, false); } public void init(Message message, boolean interactive) { this.message = message; this.message.setTransferable(this); try { String url = (null != message && null != message.getFileParams()) ? message.getFileParams().getUrl() : null; if (null == url) { /* * If this code is reached and the URL is null something went wrong. * Try again to extract the file parameters from the message. */ MessageUtil.extractFileParamsFromBody(message); url = (null != message.getFileParams()) ? message.getFileParams().getUrl() : null; if (null == url) { message.setTreatAsDownloadable(Message.Decision.NEVER); // TODO find sth better this.cancel(); return; } } mUrl = new URL(url); final String sUrlFilename = mUrl.getPath().substring(mUrl.getPath().lastIndexOf('/') + 1).toLowerCase(); final String lastPart = FileUtils.getLastExtension(sUrlFilename); if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) { this.message.setEncryption(Message.ENCRYPTION_PGP); } else if (message.getEncryption() != Message.ENCRYPTION_OTR && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; String originalFilename; if (!lastPart.isEmpty() && VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { extension = FileUtils.getSecondToLastExtension(sUrlFilename); originalFilename = sUrlFilename.replace("." + lastPart, ""); } else { extension = lastPart; originalFilename = sUrlFilename; } message.setRelativeFilePath(message.getUuid() + "." + extension); this.file = FileBackend.getFile(message, false); FileParams fileParams = message.getFileParams(); if (null == fileParams) { fileParams = new FileParams(); message.setFileParams(fileParams); } fileParams.setOriginalFilename(originalFilename); if ((this.message.getEncryption() == Message.ENCRYPTION_OTR || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } checkFileSize(interactive); } catch (MalformedURLException e) { this.cancel(); } } private void checkFileSize(boolean interactive) { new Thread(new FileSizeChecker(interactive)).start(); } @Override public void cancel() { this.canceled = true; mHttpConnectionManager.finishConnection(this); if (message.isFileOrImage()) { message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED)); } else { message.setTransferable(null); } mXmppConnectionService.updateConversationUi(); } private void finish() { FileBackend.updateMediaScanner(file, mXmppConnectionService); message.setTransferable(null); MessageUtil.setAndSaveFileStatus(this.message, FileStatus.DOWNLOADED); mHttpConnectionManager.finishConnection(this); if (message.getEncryption() == Message.ENCRYPTION_PGP) { message.getConversation().getAccount().getPgpDecryptionService().add(message); } mXmppConnectionService.updateConversationUi(); if (acceptedAutomatically) { mXmppConnectionService.getNotificationService().push(message); } } private void changeStatus(int status) { this.mStatus = status; mXmppConnectionService.updateConversationUi(); } private void showToastForException(Exception e) { e.printStackTrace(); 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 CancellationException)) { mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found); } } private class FileSizeChecker implements Runnable { private boolean interactive = false; public FileSizeChecker(boolean interactive) { this.interactive = interactive; } @Override public void run() { long size; try { size = retrieveFileSize(); } catch (SSLHandshakeException e) { changeStatus(STATUS_OFFER_CHECK_FILESIZE); HttpDownloadConnection.this.acceptedAutomatically = false; HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); return; } catch (RemoteFileNotFoundException e) { message.setNoDownloadable(); // TODO Set remote file status to not-available cancel(); return; } catch (IOException e) { Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage()); if (interactive) { showToastForException(e); } cancel(); return; } file.setExpectedSize(size); if (mHttpConnectionManager.hasStoragePermission() && size != -1 && size <= ConversationsPlusPreferences.autoAcceptFileSize() && mXmppConnectionService.isDownloadAllowedInConnection()) { HttpDownloadConnection.this.acceptedAutomatically = true; new Thread(new FileDownloader(interactive)).start(); } else { changeStatus(STATUS_OFFER); HttpDownloadConnection.this.acceptedAutomatically = false; HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message); } } private long retrieveFileSize() throws IOException { try { Logging.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive)); changeStatus(STATUS_CHECKING); HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); connection.setRequestMethod("HEAD"); Logging.d(Config.LOGTAG, "url: " + connection.getURL().toString()); Logging.d(Config.LOGTAG, "connection: " + connection.toString()); connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); // https://code.google.com/p/android/issues/detail?id=24672 connection.setRequestProperty("Accept-Encoding", ""); if (connection instanceof HttpsURLConnection) { mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); } connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { Logging.d(Config.LOGTAG, "remote file not found"); throw new RemoteFileNotFoundException(); } String contentLength = connection.getHeaderField("Content-Length"); connection.disconnect(); if (contentLength == null) { return -1; } return Long.parseLong(contentLength, 10); } catch (RemoteFileNotFoundException e) { throw e; } catch (IOException e) { return -1; } catch (NumberFormatException e) { return -1; } } } private class FileDownloader implements Runnable { private boolean interactive = false; private OutputStream os; public FileDownloader(boolean interactive) { this.interactive = interactive; } @Override public void run() { try { changeStatus(STATUS_DOWNLOADING); download(); updateImageBounds(); finish(); } catch (SSLHandshakeException e) { changeStatus(STATUS_OFFER); } catch (Exception e) { if (interactive) { showToastForException(e); } cancel(); } } private void download() throws SSLHandshakeException, IOException { InputStream is = null; PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_download_"+message.getUuid()); try { wakeLock.acquire(); HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection(); if (connection instanceof HttpsURLConnection) { mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive); } connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion()); final boolean tryResume = file.exists() && file.getKey() == null; if (tryResume) { Logging.d(Config.LOGTAG, "http download trying resume"); long size = file.getSize(); connection.setRequestProperty("Range", "bytes="+size+"-"); } connection.connect(); is = new BufferedInputStream(connection.getInputStream()); boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges")); long transmitted = 0; long expected = file.getExpectedSize(); if (tryResume && serverResumed) { Logging.d(Config.LOGTAG, "server resumed"); transmitted = file.getSize(); updateProgress((int) ((((double) transmitted) / expected) * 100)); os = AbstractConnectionManager.createAppendedOutputStream(file); } else { file.getParentFile().mkdirs(); file.createNewFile(); os = AbstractConnectionManager.createOutputStream(file, true); } int count = -1; byte[] buffer = new byte[1024]; while ((count = is.read(buffer)) != -1) { transmitted += count; os.write(buffer, 0, count); updateProgress((int) ((((double) transmitted) / expected) * 100)); if (canceled) { throw new CancellationException(); } } } catch (CancellationException | IOException e) { throw e; } finally { if (os != null) { try { os.flush(); } catch (final IOException ignored) { } } StreamUtil.close(os); StreamUtil.close(is); wakeLock.release(); } } private void updateImageBounds() { message.setType(Message.TYPE_FILE); MessageUtil.updateFileParams(message, mUrl); mXmppConnectionService.updateMessage(message); } } public void updateProgress(int i) { this.mProgress = i; mXmppConnectionService.updateConversationUi(); } @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; } }