diff options
author | steckbrief <steckbrief@chefmail.de> | 2017-02-06 10:01:13 +0100 |
---|---|---|
committer | steckbrief <steckbrief@chefmail.de> | 2017-02-06 10:01:13 +0100 |
commit | 754de6bb0449a577d2bb9c28cca6adf0ef9554f6 (patch) | |
tree | 279b405d94e0d86d10ed94bd34d919457944ead6 /src | |
parent | cd633f13b8d7327e47994bb5a000f0c0b7089e7f (diff) |
relates FS#241: Implementation of http download based on okhttp
Diffstat (limited to 'src')
45 files changed, 1053 insertions, 807 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java index d4e3ebc2..2c056867 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java @@ -1,8 +1,10 @@ package de.thedevstack.conversationsplus; +import android.Manifest; import android.app.Application; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Build; import android.os.PowerManager; import android.preference.PreferenceManager; @@ -11,15 +13,14 @@ import java.security.SecureRandom; import de.duenndns.ssl.MemorizingTrustManager; import de.thedevstack.conversationsplus.http.HttpClient; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; +import de.thedevstack.conversationsplus.services.filetransfer.http.download.HttpDownloadFileTransferService; import de.thedevstack.conversationsplus.services.filetransfer.http.upload.HttpUploadFileTransferService; import de.thedevstack.conversationsplus.services.filetransfer.jingle.JingleFileTransferService; import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.PRNGFixes; import de.thedevstack.conversationsplus.utils.SerialSingleThreadExecutor; -import okhttp3.OkHttpClient; /** * This class is used to provide static access to the applicationcontext. @@ -46,8 +47,7 @@ public class ConversationsPlusApplication extends Application { this.initializeSecurity(); ImageUtil.initBitmapCache(); FileBackend.init(); - FileTransferManager.init(new HttpUploadFileTransferService(), new JingleFileTransferService()); - HttpConnectionManager.init(); + FileTransferManager.init(new HttpUploadFileTransferService(), new JingleFileTransferService(), new HttpDownloadFileTransferService()); HttpClient.init(); } @@ -132,6 +132,14 @@ public class ConversationsPlusApplication extends Application { return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name); } + public static boolean hasStoragePermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return ConversationsPlusApplication.getAppContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } else { + return true; + } + } + public static MemorizingTrustManager getMemorizingTrustManager() { return getInstance().memorizingTrustManager; } @@ -150,11 +158,6 @@ public class ConversationsPlusApplication extends Application { getInstance().setMemorizingTrustManager(tm); } - private static void initHttpClient() { - OkHttpClient client = new OkHttpClient.Builder() - .build(); - } - public static SecureRandom getSecureRandom() { return getInstance().secureRandom; } diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java index 352e86e3..db9c560f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java +++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusColors.java @@ -147,7 +147,7 @@ public final class ConversationsPlusColors { * @see {@link android.content.res.Resources#getColor(int)} * @return the color identified by id */ - private static int byId(int id) { + public static int byId(int id) { return ConversationsPlusApplication.getAppContext().getResources().getColor(id); } diff --git a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java index c97b8395..440ca6de 100644 --- a/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java +++ b/src/main/java/de/thedevstack/conversationsplus/crypto/PgpEngine.java @@ -16,6 +16,7 @@ import java.io.InputStream; import java.io.OutputStream; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; @@ -23,7 +24,6 @@ import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.ui.UiCallback; @@ -67,7 +67,10 @@ public class PgpEngine { && message.treatAsDownloadable() != Message.Decision.NEVER && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink()) && ConversationsPlusPreferences.autoAcceptFileSize() > 0) { - HttpConnectionManager.createNewDownloadConnection(message); + FileTransferManager ftm = FileTransferManager.getInstance(); + if (ftm.accept(message)) { + ftm.transferFile(message); + } } mXmppConnectionService.updateMessage(message); callback.success(message); diff --git a/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java index 58b64b28..3439460b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java +++ b/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java @@ -5,7 +5,7 @@ import android.support.annotation.NonNull; import java.io.Serializable; /** - * Created by steckbrief on 22.08.2016. + * */ public class RemoteFile implements Serializable { private static final long serialVersionUID = 34564871234564L; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java b/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java index 7d5741d9..b21472ba 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java @@ -1,6 +1,7 @@ package de.thedevstack.conversationsplus.entities; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.utils.FileUtils; import de.thedevstack.conversationsplus.utils.MimeUtils; /** @@ -12,7 +13,7 @@ public class FileParams { private String path; private String url; private String mimeType; - private long size = 0; + private long size = -1; private int width = 0; private int height = 0; private FileStatus fileStatus; @@ -136,15 +137,12 @@ public class FileParams { path = (!path.endsWith(this.name)) ? path + "/" + this.name : path; } else { if (!path.endsWith("/")) { - this.setName(path.substring(path.lastIndexOf('/') + 1)); + this.setName(FileUtils.getFilenameFromPath(path)); } } if (null == this.mimeType) { - int start = path.lastIndexOf('.') + 1; - if (start < path.length()) { - String extension = path.substring(start); - this.mimeType = MimeUtils.guessMimeTypeFromExtension(extension); - } + String extension = FileUtils.getLastExtension(path); + this.mimeType = MimeUtils.guessMimeTypeFromExtension(extension); } } @@ -152,7 +150,7 @@ public class FileParams { } public boolean isRemoteAvailable() { - return null != this.url || FileStatus.UPLOADED == this.fileStatus || FileStatus.DELETE_FAILED == this.fileStatus; + return (null != this.url || FileStatus.UPLOADED == this.fileStatus || FileStatus.DELETE_FAILED == this.fileStatus) && FileStatus.NOT_FOUND != this.fileStatus; } public void setFileStatus(FileStatus fileStatus) { diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java index a553a3e6..4d7697f2 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -18,8 +18,6 @@ public class Message extends AbstractEntity { public static final String TABLENAME = "messages"; - public static final String MERGE_SEPARATOR = " \u200B\n\n"; - public static final int STATUS_RECEIVED = 0; public static final int STATUS_UNSEND = 1; public static final int STATUS_SEND = 2; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java index 8e2ca20e..10bc8bcf 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java @@ -1,6 +1,8 @@ package de.thedevstack.conversationsplus.entities; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; public interface Transferable { @@ -23,8 +25,6 @@ public interface Transferable { int getStatus(); - long getFileSize(); - int getProgress(); void cancel(); diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/TransferablePlaceholder.java b/src/main/java/de/thedevstack/conversationsplus/entities/TransferablePlaceholder.java index c51320d1..40292e1f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/TransferablePlaceholder.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/TransferablePlaceholder.java @@ -18,11 +18,6 @@ public class TransferablePlaceholder implements Transferable { } @Override - public long getFileSize() { - return 0; - } - - @Override public int getProgress() { return 0; } diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java b/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java index b6a4ef9a..e4254aeb 100644 --- a/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java +++ b/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java @@ -13,5 +13,8 @@ public enum FileStatus { NEEDS_UPLOAD, UNDEFINED, NEEDS_DOWNLOAD, - DELETING; + DELETING, + NOT_FOUND, + DOWNLOADING, + CHECKING_FILE_SIZE; } diff --git a/src/main/java/de/thedevstack/conversationsplus/http/Http.java b/src/main/java/de/thedevstack/conversationsplus/http/Http.java new file mode 100644 index 00000000..98b4dc82 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/http/Http.java @@ -0,0 +1,18 @@ +package de.thedevstack.conversationsplus.http; + +import java.net.HttpURLConnection; + +/** + * + */ + +public interface Http { + // HTTP Response Codes + int HTTP_NOT_FOUND = HttpURLConnection.HTTP_NOT_FOUND; + + // Header Field Names + String MIME_REQUEST_PROPERTY_NAME = "Content-Type"; + String HEADER_NAME_CONTENT_LENGTH = "Content-Length"; + String HEADER_NAME_ACCEPT_ENCODING = "Accept-Encoding"; + String USER_AGENT_REQUEST_PROPERTY_NAME = "User-Agent"; +} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java index 7e12a890..e62be056 100644 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java @@ -1,52 +1,101 @@ package de.thedevstack.conversationsplus.http; +import android.support.annotation.NonNull; + import org.apache.http.conn.ssl.StrictHostnameVerifier; +import java.io.IOException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.SSLSocketHelper; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Interceptor; +import okhttp3.MediaType; import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; /** - * Created by steckbrief on 22.08.2016. + * */ -public final class HttpClient { +public final class HttpClient implements Http { private static HttpClient INSTANCE; - private boolean interactive = false; - private OkHttpClient client; + private static final String LOGTAG = "http-client"; + + private final OkHttpClient client; - public static void init() { + public static synchronized void init() { INSTANCE = new HttpClient(); } - public static synchronized OkHttpClient getClient(boolean interactive) { - if (INSTANCE.interactive != interactive) { - INSTANCE.interactive = interactive; - INSTANCE.buildClient(); + public static synchronized HttpClient getClient() { + if (null == INSTANCE) { + init(); } - return INSTANCE.client; + return INSTANCE; } - private HttpClient() { - this.buildClient(); + private static OkHttpClient.Builder getBuilder(boolean interactive) { + OkHttpClient.Builder builder = INSTANCE.client.newBuilder(); + INSTANCE.initTrustManager(builder, interactive); + + return builder; + } + + public static synchronized OkHttpClient getOkHttpClient(boolean interactive) { + return getBuilder(interactive).build(); + } + + public static synchronized Call openCancelableAndProgressListenedCall(String url, final ProgressListener progressListener, boolean interactive) { + OkHttpClient.Builder builder = getBuilder(interactive); + OkHttpClient client = builder.addNetworkInterceptor(new Interceptor() { + @Override public Response intercept(Chain chain) throws IOException { + Response originalResponse = chain.proceed(chain.request()); + return originalResponse.newBuilder() + .body(new ProgressResponseBody(originalResponse.body(), progressListener)) + .build(); + } + }) + .build(); + + return client.newCall(new Request.Builder().url(url).build()); } - private void buildClient() { + public static void retrieveHead(String url, @NonNull Callback callback) throws IOException { + OkHttpClient client = HttpClient.getOkHttpClient(true); + Request request = new Request.Builder() + .url(url) + //.addHeader(HEADER_NAME_ACCEPT_ENCODING, "") + .head() + .build(); + client.newCall(request).enqueue(callback); + } + + private HttpClient() { OkHttpClient.Builder builder = new OkHttpClient.Builder(); - this.initTrustManager(builder); + builder.addInterceptor(new UserAgentInterceptor()); + builder.addInterceptor(new LoggingInterceptor()); this.client = builder.build(); } - public void initTrustManager(final OkHttpClient.Builder builder) { + private static void initTrustManager(final OkHttpClient.Builder builder, final boolean interactive) { final X509TrustManager trustManager; final HostnameVerifier hostnameVerifier; if (interactive) { @@ -78,4 +127,75 @@ public final class HttpClient { } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { } } + + private static class UserAgentInterceptor implements Interceptor, Http { + + @Override + public Response intercept(Chain chain) throws IOException { + Request originalRequest = chain.request(); + Request requestWithUserAgent = originalRequest.newBuilder() + .header(USER_AGENT_REQUEST_PROPERTY_NAME, ConversationsPlusApplication.getNameAndVersion()) + .build(); + return chain.proceed(requestWithUserAgent); + } + } + + private static class LoggingInterceptor implements Interceptor { + @Override public Response intercept(Interceptor.Chain chain) throws IOException { + Request request = chain.request(); + + long t1 = System.nanoTime(); + Logging.d(LOGTAG, String.format(Locale.getDefault(), "Sending %s request %s on %s%n%s", + request.method(), request.url(), chain.connection(), request.headers())); + + Response response = chain.proceed(request); + + long t2 = System.nanoTime(); + Logging.d(LOGTAG, String.format(Locale.getDefault(), "Received response for %s request %s in %.1fms%n%s", + response.request().method(), response.request().url(), (t2 - t1) / 1e6d, response.headers())); + + return response; + } + } + + private static class ProgressResponseBody extends ResponseBody { + + private final ResponseBody responseBody; + private final ProgressListener progressListener; + private BufferedSource bufferedSource; + + public ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override public MediaType contentType() { + return responseBody.contentType(); + } + + @Override public long contentLength() { + return responseBody.contentLength(); + } + + @Override public BufferedSource source() { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + + @Override public long read(Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + // read() returns the number of bytes read, or -1 if this source is exhausted. + boolean done = bytesRead == -1; + long currentBytesRead = !done ? bytesRead : 0; + progressListener.update(currentBytesRead, responseBody.contentLength(), done); + return bytesRead; + } + }; + } + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java deleted file mode 100644 index 011e2529..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpConnectionManager.java +++ /dev/null @@ -1,90 +0,0 @@ -package de.thedevstack.conversationsplus.http; - -import org.apache.http.conn.ssl.StrictHostnameVerifier; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -import de.thedevstack.conversationsplus.ConversationsPlusApplication; -import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.services.AbstractConnectionManager; -import de.thedevstack.conversationsplus.utils.CryptoHelper; -import de.thedevstack.conversationsplus.utils.MessageUtil; -import de.thedevstack.conversationsplus.utils.SSLSocketHelper; - -public class HttpConnectionManager extends AbstractConnectionManager { - private static HttpConnectionManager INSTANCE; - - public static void init() { - INSTANCE = new HttpConnectionManager(); - } - - private List<HttpDownloadConnection> downloadConnections = new CopyOnWriteArrayList<>(); - - public static HttpDownloadConnection createNewDownloadConnection(Message message) { - return createNewDownloadConnection(message, false); - } - - public static HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) { - if (MessageUtil.needsDownload(message)) { - HttpDownloadConnection connection = new HttpDownloadConnection(INSTANCE); - connection.init(message, interactive); - INSTANCE.downloadConnections.add(connection); - return connection; - } - return null; - } - - public void finishConnection(HttpDownloadConnection connection) { - this.downloadConnections.remove(connection); - } - - public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { - final X509TrustManager trustManager; - final HostnameVerifier hostnameVerifier; - if (interactive) { - trustManager = ConversationsPlusApplication.getMemorizingTrustManager(); - hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier( - new StrictHostnameVerifier()); - } else { - trustManager = ConversationsPlusApplication.getMemorizingTrustManager() - .getNonInteractive(); - hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager() - .wrapHostnameVerifierNonInteractive( - new StrictHostnameVerifier()); - } - try { - final SSLContext sc = SSLSocketHelper.getSSLContext(); - sc.init(null, new X509TrustManager[]{trustManager}, - ConversationsPlusApplication.getSecureRandom()); - - final SSLSocketFactory sf = sc.getSocketFactory(); - final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( - sf.getSupportedCipherSuites()); - if (cipherSuites.length > 0) { - sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); - - } - - connection.setSSLSocketFactory(sf); - connection.setHostnameVerifier(hostnameVerifier); - } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { - } - } - - public Proxy getProxy() throws IOException { - return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118)); - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java deleted file mode 100644 index 07f308fe..00000000 --- a/src/main/java/de/thedevstack/conversationsplus/http/HttpDownloadConnection.java +++ /dev/null @@ -1,375 +0,0 @@ -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; - } -} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpHeadRetrievedListener.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpHeadRetrievedListener.java new file mode 100644 index 00000000..0db4f71b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpHeadRetrievedListener.java @@ -0,0 +1,10 @@ +package de.thedevstack.conversationsplus.http; + +import de.thedevstack.conversationsplus.entities.Message; + +/** + * + */ +public interface HttpHeadRetrievedListener { + void onFileSizeRetrieved(long size, Message message); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/http/ProgressListener.java b/src/main/java/de/thedevstack/conversationsplus/http/ProgressListener.java new file mode 100644 index 00000000..b834eae1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/http/ProgressListener.java @@ -0,0 +1,14 @@ +package de.thedevstack.conversationsplus.http; + +/** + * + */ +public interface ProgressListener { + /** + * React on transferred bytes. + * @param bytesRead the number of bytes transferred in the current chunk + * @param contentLength the total number of bytes to be transferred + * @param done whether there are bytes left or not + */ + void update(long bytesRead, long contentLength, boolean done); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java index 53fbb107..4b6ed240 100644 --- a/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java +++ b/src/main/java/de/thedevstack/conversationsplus/parser/MessageParser.java @@ -5,6 +5,8 @@ import android.util.Pair; import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.services.filetransfer.http.download.AutomaticFileDownload; +import de.thedevstack.conversationsplus.services.filetransfer.http.download.HttpRetrieveHead; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint; import de.tzur.conversations.Settings; @@ -29,7 +31,6 @@ import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.MucOptions; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.services.AvatarService; import de.thedevstack.conversationsplus.services.MessageArchiveService; import de.thedevstack.conversationsplus.services.XmppConnectionService; @@ -466,21 +467,23 @@ public class MessageParser extends AbstractParser implements conversation.endOtrIfNeeded(); } + MessageUtil.extractFileParamsFromBody(message); + FileParams fileParams = message.getFileParams(); + if (message.treatAsDownloadable() != Message.Decision.NEVER && message.treatAsDownloadable() != Message.Decision.NOT_DECIDED) { + if (null != fileParams) { + fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD); + } + } if (message.getEncryption() == Message.ENCRYPTION_NONE || !ConversationsPlusPreferences.dontSaveEncrypted()) { mXmppConnectionService.databaseBackend.createMessage(message); } - MessageUtil.extractFileParamsFromBody(message); - FileParams fileParams = message.getFileParams(); - if (message.treatAsDownloadable() != Message.Decision.NEVER && message.treatAsDownloadable() != Message.Decision.NOT_DECIDED) { - if (null != fileParams) { - fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD); - } - } if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && ConversationsPlusPreferences.autoAcceptFileSize() > 0 && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) { - HttpConnectionManager.createNewDownloadConnection(message); + HttpRetrieveHead hrh = new HttpRetrieveHead(message); + hrh.setListener(new AutomaticFileDownload(true)); + hrh.retrieveAndSetContentTypeAndLength(); } else { if (query == null) { mXmppConnectionService.getNotificationService().push(message); diff --git a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java index ee0ee14a..6a9b5c13 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/XmppConnectionService.java @@ -2387,7 +2387,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void sendReadMarker(final Conversation conversation) { final Message markable = conversation.getLatestMarkableMessage(); - Logging.d("markRead", "XmppConnectionService.sendReadMarker (" + conversation.getName() + ")"); + //Logging.d("markRead", "XmppConnectionService.sendReadMarker (" + conversation.getName() + ")"); if (this.markRead(conversation)) { updateConversationUi(); } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java index e1b40fa6..06e697db 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java @@ -49,10 +49,16 @@ public class FileTransferEntity implements Transferable { * This initialization includes loading the file and associating this transferable to the message. * @param message the message in which the file to transfer is contained. */ - public FileTransferEntity(Message message) { + public FileTransferEntity(Message message, boolean initFile) { this.message = message; this.message.setTransferable(this); - this.file = FileBackend.getFile(message, false); + if (initFile) { + this.initFile(); + } + } + + protected final void initFile() { + this.file = FileBackend.getFile(this.message, false); } /** @@ -68,11 +74,11 @@ public class FileTransferEntity implements Transferable { /** * Returns the global transferable status. * - * @return {@value STATUS_FAILED} if #isFailed returns <code>true</code>, {@value STATUS_UPLOADING} otherwise + * @return {@value STATUS_FAILED} if #isFailed returns <code>true</code>, {@value STATUS_UNKNOWN} otherwise */ @Override public int getStatus() { - int status = (isFailed()) ? STATUS_FAILED : STATUS_UPLOADING; + int status = (isFailed()) ? STATUS_FAILED : STATUS_UNKNOWN; return status; } @@ -80,7 +86,6 @@ public class FileTransferEntity implements Transferable { * Returns the expected file size of the underlying file. * @return the expected file size or 0 if no file is associated. */ - @Override public long getFileSize() { return file == null ? 0 : file.getExpectedSize(); } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java index 2f9a819b..8d971bb2 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java @@ -9,7 +9,7 @@ import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.services.FileTransferService; -import de.thedevstack.conversationsplus.services.filetransfer.http.upload.HttpFileTransferEntity; +import de.thedevstack.conversationsplus.services.filetransfer.http.upload.HttpUploadFileTransferEntity; import de.thedevstack.conversationsplus.utils.MessageUtil; /** @@ -73,7 +73,7 @@ public class FileTransferManager implements FileTransferStatusListener { * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise */ public boolean transferFile(Message message, boolean delay) { - Logging.d(Config.LOGTAG, "send file message"); + Logging.d(Config.LOGTAG, "transfer file message"); boolean transferSuccessfullyStarted = false; for (WeightedTransferService wts : this.transferServices) { try { @@ -91,13 +91,13 @@ public class FileTransferManager implements FileTransferStatusListener { } /** - * Checks whether a message can be sent using this service or not. + * Checks whether a message can be sent or received using this service or not. * * @param message the message to be checked * @return <code>true</code> if the message can be processed, <code>false</code> otherwise */ public boolean accept(Message message) { - return message.needsUploading(); + return message.needsUploading() || MessageUtil.needsDownload(message); } @Override @@ -106,7 +106,7 @@ public class FileTransferManager implements FileTransferStatusListener { if (null == wts) { return; } - boolean delayed = (entity instanceof HttpFileTransferEntity) && ((HttpFileTransferEntity) entity).isDelayed(); + boolean delayed = (entity instanceof HttpUploadFileTransferEntity) && ((HttpUploadFileTransferEntity) entity).isDelayed(); if (failureReason.isRecoverable()) { wts.fileTransferService.transferFile(entity.getMessage(), delayed); } else { diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java index 83a250de..d61a5c6a 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java @@ -42,7 +42,7 @@ public class DeleteTokenReceived implements OnIqPacketReceived { String url = this.remoteFile.getPath(); String deleteToken = DeleteSlotPacketParser.parseDeleteToken(packet); Logging.d("filetransfer.http.delete", "Got delete token '" + deleteToken + "' for remote file '" + remoteFile.getPath() + "'"); - OkHttpClient client = HttpClient.getClient(true); + OkHttpClient client = HttpClient.getOkHttpClient(true); Request request = new Request.Builder() .url(url) .addHeader(HEADER_NAME_DELETE_TOKEN, deleteToken) diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java new file mode 100644 index 00000000..5885e2e1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/AutomaticFileDownload.java @@ -0,0 +1,61 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.download; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.http.HttpHeadRetrievedListener; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; + +/** + * + */ +public class AutomaticFileDownload implements HttpHeadRetrievedListener { + private boolean notify; + + public AutomaticFileDownload(boolean notify) { + this.notify = notify; + } + + @Override + public void onFileSizeRetrieved(long size, Message message) { + if (!this.transferFile(message)) { + + } + if (this.notify) { + XmppConnectionServiceAccessor.xmppConnectionService.getNotificationService().push(message); + } + } + + /** + * Transfers a file for the corresponding message. + * + * @param message the message containing the file to transfer + * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise + */ + public boolean transferFile(Message message) { + if (this.accept(message)) { + Logging.d("http-download", "Starting automatic download"); + FileTransferManager ftm = FileTransferManager.getInstance(); + if (ftm.accept(message)) { + return ftm.transferFile(message); + } + } + return false; + } + + /** + * Checks whether a message can be sent using this service or not. + * + * @param message the message to be checked + * @return <code>true</code> if the message can be processed, <code>false</code> otherwise + */ + public boolean accept(Message message) { + long size = message.getFileParams().getSize(); + return ConversationsPlusApplication.hasStoragePermission() + && size > -1 + && size <= ConversationsPlusPreferences.autoAcceptFileSize() + && XmppConnectionServiceAccessor.xmppConnectionService.isDownloadAllowedInConnection(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpDownloadFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpDownloadFileTransferEntity.java new file mode 100644 index 00000000..fcd3904b --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpDownloadFileTransferEntity.java @@ -0,0 +1,53 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.download; + +import android.os.PowerManager; + +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.http.ProgressListener; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferEntity; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; + +/** + * + */ +public class HttpDownloadFileTransferEntity extends FileTransferEntity implements ProgressListener { + public PowerManager.WakeLock wakeLock; + /** + * Initializes the FileTransferEntity based on the associated message. + * This initialization includes loading the file and associating this transferable to the message. + * + * @param message the message in which the file to transfer is contained. + */ + public HttpDownloadFileTransferEntity(Message message) { + super(message, false); + FileParams fileParams = message.getFileParams(); + this.initFile(); + this.getFile().setExpectedSize(fileParams.getSize()); + } + + /** + * Returns the global transferable status. + * + * @return {@value STATUS_FAILED} if #isFailed returns <code>true</code>, {@value STATUS_DOWNLOADING} otherwise + */ + @Override + public int getStatus() { + int status = (isFailed()) ? STATUS_FAILED : STATUS_DOWNLOADING; + return status; + } + + @Override + public void update(long bytesRead, long contentLength, boolean done) { + if (0 >= getFile().getExpectedSize()) { + getFile().setExpectedSize(contentLength); + getMessage().getFileParams().setSize(contentLength); + } + if (done) { + this.transferred(); + } else { + this.updateProgress(bytesRead); + } + UiUpdateHelper.updateConversationUi(); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpDownloadFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpDownloadFileTransferService.java new file mode 100644 index 00000000..c7fcc430 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpDownloadFileTransferService.java @@ -0,0 +1,65 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.download; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.http.HttpClient; +import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; +import okhttp3.Call; + +/** + * + */ +public class HttpDownloadFileTransferService extends AbstractFileTransferService { + /** + * Transfers a file for the corresponding message. + * + * @param message the message containing the file to transfer + * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise + */ + @Override + public boolean transferFile(Message message) { + return this.transferFile(message, false); + } + + /** + * Transfers a file for the corresponding message. + * + * @param message the message containing the file to transfer + * @param delay whether the message is delayed or not + * @return <code>true</code> if the file transfer was successful, <code>false</code> otherwise + */ + @Override + public boolean transferFile(Message message, boolean delay) { + Logging.d("http-download", "Get file from remote host"); + final HttpDownloadFileTransferEntity entity = new HttpDownloadFileTransferEntity(message); + if (message.getFileParams().getFileStatus() == FileStatus.NEEDS_DOWNLOAD + && ConversationsPlusApplication.hasStoragePermission() + && XmppConnectionServiceAccessor.xmppConnectionService.isDownloadAllowedInConnection()) { + FileParams fileParams = message.getFileParams(); + MessageUtil.setAndSaveFileStatus(message, FileStatus.DOWNLOADING); + entity.wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_download_" + entity.getMessage().getUuid()); + entity.wakeLock.acquire(); + Call call = HttpClient.openCancelableAndProgressListenedCall(fileParams.getUrl(), entity, false); + call.enqueue(new HttpFileDownloadCallback(entity)); + } + + return true; + } + + /** + * Checks whether a message can be sent using this service or not. + * + * @param message the message to be checked + * @return <code>true</code> if the message can be processed, <code>false</code> otherwise + */ + @Override + public boolean accept(Message message) { + return MessageUtil.needsDownload(message) + && message.hasFileAttached() && (null == message.getFileParams() || message.getFileParams().isRemoteAvailable()); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpFileDownloadCallback.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpFileDownloadCallback.java new file mode 100644 index 00000000..2c31754c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpFileDownloadCallback.java @@ -0,0 +1,57 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.download; + +import java.io.IOException; +import java.io.OutputStream; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.DownloadableFile; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.services.AbstractConnectionManager; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.StreamUtil; +import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Response; + +/** + * + */ +public class HttpFileDownloadCallback implements Callback { + private HttpDownloadFileTransferEntity entity; + + public HttpFileDownloadCallback(HttpDownloadFileTransferEntity entity) { + this.entity = entity; + } + + + @Override + public void onFailure(Call call, IOException e) { + changeStatus(FileStatus.DOWNLOAD_FAILED); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (response.isSuccessful()) { + Logging.d("http-download", "Receiving file from remote host"); + DownloadableFile file = this.entity.getFile(); + OutputStream os = AbstractConnectionManager.createOutputStream(file, true); + os.write(response.body().bytes()); + StreamUtil.close(os); + FileBackend.updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService); + this.entity.transferred(); + changeStatus(FileStatus.DOWNLOADED); + } else { + Logging.e("http-download", "Failed to retrieve file from remote host. HTTP response: " + response.code() + ", " + response.body().string()); + changeStatus(FileStatus.DOWNLOAD_FAILED); + } + if (entity.wakeLock.isHeld()) { + entity.wakeLock.release(); + } + } + + private void changeStatus(FileStatus status) { + MessageUtil.setAndSaveFileStatus(this.entity.getMessage(), status); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java new file mode 100644 index 00000000..8d23d9c0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/download/HttpRetrieveHead.java @@ -0,0 +1,112 @@ +package de.thedevstack.conversationsplus.services.filetransfer.http.download; + +import java.io.IOException; + +import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.entities.FileParams; +import de.thedevstack.conversationsplus.entities.Message; +import de.thedevstack.conversationsplus.enums.FileStatus; +import de.thedevstack.conversationsplus.http.Http; +import de.thedevstack.conversationsplus.http.HttpClient; +import de.thedevstack.conversationsplus.http.HttpHeadRetrievedListener; +import de.thedevstack.conversationsplus.persistance.DatabaseBackend; +import de.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.utils.UiUpdateHelper; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.MediaType; +import okhttp3.Response; + +/** + * + */ +public class HttpRetrieveHead implements Http, Callback { + private static final String LOGTAG = "http-retrieve-head"; + + private Message message; + private String url; + + private HttpHeadRetrievedListener listener; + + public HttpRetrieveHead(Message message) { + this.message = message; + this.url = (null != message && null != message.getFileParams()) ? message.getFileParams().getUrl() : null; + if (null == this.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); + this.url = (null != message.getFileParams()) ? message.getFileParams().getUrl() : null; + if (null == this.url) { + message.setTreatAsDownloadable(Message.Decision.NEVER); // TODO find sth better + if (null != message.getFileParams()) { + MessageUtil.setAndSaveFileStatus(message, FileStatus.NOT_FOUND); + } + } + } + } + + public void retrieveAndSetContentTypeAndLength() { + if (null != this.url) { + Logging.d(LOGTAG, "retrieve file size and mime type."); + + try { + MessageUtil.setAndSaveFileStatus(message, FileStatus.CHECKING_FILE_SIZE); + HttpClient.retrieveHead(this.url, this); + } catch (IOException e) { + Logging.e(LOGTAG, "Error while trying to call '" + url + "'.", e); + } + } + } + + private static long parseContentLength(String contentLength) { + long length = -1; + if (null != contentLength) { + try { + length = Long.parseLong(contentLength, 10); + } catch (NumberFormatException e) { + } + } + return length; + } + + @Override + public void onFailure(Call call, IOException e) { + Logging.e(LOGTAG, "Error while trying to call '" + call.request().url() + "'.", e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + Logging.d(LOGTAG, "Response for retrieving file size and mime type received."); + FileParams fileParams = message.getFileParams(); + if (response.isSuccessful()) { + MediaType mediaType = response.body().contentType(); + String contentType = null != mediaType ? (mediaType.type() + "/" + mediaType.subtype()) : response.header(MIME_REQUEST_PROPERTY_NAME); + String contentLength = response.header(HEADER_NAME_CONTENT_LENGTH); + + long size = parseContentLength(contentLength); + fileParams.setSize(size); + fileParams.setMimeType(contentType); + if (0 < size) { + fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD); + } + DatabaseBackend.getInstance().updateMessage(message); + UiUpdateHelper.updateConversationUi(); + if (null != this.listener) { + this.listener.onFileSizeRetrieved(size, this.message); + } + } else { + if (response.code() == HTTP_NOT_FOUND) { + Logging.d(LOGTAG, "remote file '" + response.request().url() + "' not found."); + MessageUtil.setAndSaveFileStatus(message, FileStatus.NOT_FOUND); + } else { + Logging.d(LOGTAG, "remote file '" + response.request().url() + "' not loaded - response code: " + response.code() + "."); + } + } + } + + public void setListener(HttpHeadRetrievedListener listener) { + this.listener = listener; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java index 8cae599c..73c18f15 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java @@ -2,23 +2,32 @@ package de.thedevstack.conversationsplus.services.filetransfer.http.upload; import android.os.PowerManager; +import org.apache.http.conn.ssl.StrictHostnameVerifier; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; import java.util.Scanner; +import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason; +import de.thedevstack.conversationsplus.utils.CryptoHelper; +import de.thedevstack.conversationsplus.utils.SSLSocketHelper; import de.thedevstack.conversationsplus.utils.StreamUtil; import de.thedevstack.conversationsplus.utils.UiUpdateHelper; import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; @@ -29,9 +38,9 @@ public class HttpFileUploader implements Runnable { private static final String MIME_REQUEST_PROPERTY_NAME = "Content-Type"; private static final String USER_AGENT_REQUEST_PROPERTY_NAME = "User-Agent"; private static final int BUFFER_LENGTH = 4096; - private final HttpFileTransferEntity entity; + private final HttpUploadFileTransferEntity entity; - public HttpFileUploader(HttpFileTransferEntity entity) { + public HttpFileUploader(HttpUploadFileTransferEntity entity) { this.entity = entity; } @@ -55,7 +64,7 @@ public class HttpFileUploader implements Runnable { connection = (HttpURLConnection) putUrl.openConnection(); if (connection instanceof HttpsURLConnection) { - HttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true); + setupTrustManager((HttpsURLConnection) connection, true); } connection.setRequestMethod(HTTP_METHOD); connection.setFixedLengthStreamingMode((int) file.getExpectedSize()); @@ -147,4 +156,37 @@ public class HttpFileUploader implements Runnable { } return httpResponseMessage; } + + public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) { + final X509TrustManager trustManager; + final HostnameVerifier hostnameVerifier; + if (interactive) { + trustManager = ConversationsPlusApplication.getMemorizingTrustManager(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier( + new StrictHostnameVerifier()); + } else { + trustManager = ConversationsPlusApplication.getMemorizingTrustManager() + .getNonInteractive(); + hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager() + .wrapHostnameVerifierNonInteractive( + new StrictHostnameVerifier()); + } + try { + final SSLContext sc = SSLSocketHelper.getSSLContext(); + sc.init(null, new X509TrustManager[]{trustManager}, + ConversationsPlusApplication.getSecureRandom()); + + final SSLSocketFactory sf = sc.getSocketFactory(); + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( + sf.getSupportedCipherSuites()); + if (cipherSuites.length > 0) { + sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); + + } + + connection.setSSLSocketFactory(sf); + connection.setHostnameVerifier(hostnameVerifier); + } catch (final KeyManagementException | NoSuchAlgorithmException ignored) { + } + } }
\ No newline at end of file diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferEntity.java index 985dafcb..54186983 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferEntity.java @@ -18,15 +18,15 @@ import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUpload /** * */ -public class HttpFileTransferEntity extends FileTransferEntity { +public class HttpUploadFileTransferEntity extends FileTransferEntity { private HttpUploadSlot slot; private final byte[] key; private final boolean delayed; - public HttpFileTransferEntity(Message message, boolean delayed) { - super(message); + public HttpUploadFileTransferEntity(Message message, boolean delayed) { + super(message, true); this.getMessage().setHttpUploaded(true); - this.getMessage().setNoDownloadable(); // TODO Set rmeote file status to uploaded + this.getMessage().setNoDownloadable(); FileParams fileParams = this.getMessage().getFileParams(); if (null == fileParams) { fileParams = new FileParams(); @@ -45,6 +45,17 @@ public class HttpFileTransferEntity extends FileTransferEntity { this.delayed = delayed; } + /** + * Returns the global transferable status. + * + * @return {@value STATUS_FAILED} if #isFailed returns <code>true</code>, {@value STATUS_UPLOADING} otherwise + */ + @Override + public int getStatus() { + int status = (isFailed()) ? STATUS_FAILED : STATUS_UPLOADING; + return status; + } + public void setSlot(HttpUploadSlot slot) { this.slot = slot; } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java index f0bb438d..c2e92a9e 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java @@ -9,7 +9,6 @@ import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.entities.Account; import de.thedevstack.conversationsplus.entities.DownloadableFile; import de.thedevstack.conversationsplus.entities.Message; -import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.AbstractConnectionManager; import de.thedevstack.conversationsplus.services.FileTransferService; import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService; @@ -53,7 +52,7 @@ public class HttpUploadFileTransferService extends AbstractFileTransferService i Logging.d("httpupload", "Starting to upload file"); boolean started = false; try { - final HttpFileTransferEntity entity = new HttpFileTransferEntity(message, delay); + final HttpUploadFileTransferEntity entity = new HttpUploadFileTransferEntity(message, delay); this.addStatusListenerToEntity(entity); entity.startAttempt(); Account account = message.getConversation().getAccount(); @@ -99,6 +98,7 @@ public class HttpUploadFileTransferService extends AbstractFileTransferService i && null != message.getConversation() && null != message.getConversation().getAccount() && null != message.getFileParams() + && message.needsUploading() && AccountUtil.isHttpUploadAvailable(message.getConversation().getAccount(), message.getFileParams().getSize()); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java index 2687878d..022aedcf 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java @@ -13,9 +13,9 @@ import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; * */ public class HttpUploadSlotRequestReceived implements OnIqPacketReceived { - private final HttpFileTransferEntity entity; + private final HttpUploadFileTransferEntity entity; - public HttpUploadSlotRequestReceived(HttpFileTransferEntity entity) { + public HttpUploadSlotRequestReceived(HttpUploadFileTransferEntity entity) { this.entity = entity; } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java index f363a675..dc36851f 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java @@ -11,9 +11,9 @@ import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor; * */ public class HttpUploadedFileEncryptionUiCallback implements UiCallback<Message> { - private final HttpFileTransferEntity entity; + private final HttpUploadFileTransferEntity entity; - public HttpUploadedFileEncryptionUiCallback(HttpFileTransferEntity entity) { + public HttpUploadedFileEncryptionUiCallback(HttpUploadFileTransferEntity entity) { this.entity = entity; } diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java index 5d8ddd4e..ca708fe6 100644 --- a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java +++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java @@ -47,6 +47,7 @@ public class JingleFileTransferService extends AbstractFileTransferService imple */ @Override public boolean accept(Message message) { - return message.fixCounterpart(); // No clue why - but this seems to be the check for jingle file transfer + return message.needsUploading() + && message.fixCounterpart(); // No clue why - but this seems to be the check for jingle file transfer } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java index bca0851a..ce1eb439 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationActivity.java @@ -44,6 +44,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; import de.thedevstack.conversationsplus.utils.AccountUtil; import de.timroes.android.listview.EnhancedListView; @@ -59,8 +60,6 @@ import de.thedevstack.conversationsplus.entities.Contact; import de.thedevstack.conversationsplus.entities.Conversation; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; -import de.thedevstack.conversationsplus.http.HttpDownloadConnection; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.services.XmppConnectionService.OnAccountUpdate; @@ -624,8 +623,12 @@ public class ConversationActivity extends XmppActivity Toast.makeText(this, R.string.not_connected_try_again, Toast.LENGTH_SHORT).show(); } } else if (message.treatAsDownloadable() != Message.Decision.NEVER) { - HttpDownloadConnection downloadConnection = HttpConnectionManager.createNewDownloadConnection(message, true); - if (null == downloadConnection) { + FileTransferManager ftm = FileTransferManager.getInstance(); + boolean created = false; + if (ftm.accept(message)) { + created = ftm.transferFile(message); + } + if (!created) { Toast.makeText(this, R.string.file_not_on_remote_host, Toast.LENGTH_LONG).show(); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java index 157e6dc3..0f32e762 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/ConversationFragment.java @@ -43,8 +43,7 @@ import java.util.Collections; import java.util.List; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; -import de.thedevstack.conversationsplus.http.HttpDownloadConnection; +import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager; import de.thedevstack.conversationsplus.services.filetransfer.http.delete.DeleteRemoteFileService; import de.thedevstack.conversationsplus.ui.dialogs.SimpleConfirmDialog; import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog; @@ -559,8 +558,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } if (m.hasFileOnRemoteHost() || GeoHelper.isGeoUri(m.getBody()) - || m.treatAsDownloadable() == Message.Decision.MUST - || (t != null && t instanceof HttpDownloadConnection)) { + || m.treatAsDownloadable() == Message.Decision.MUST) { copyUrl.setVisible(true); } if ((m.getType() == Message.TYPE_TEXT && t == null && m.treatAsDownloadable() != Message.Decision.NEVER) @@ -687,8 +685,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } private void downloadFile(Message message) { - HttpDownloadConnection downloadConnection = HttpConnectionManager.createNewDownloadConnection(message, true); - if (null == downloadConnection) { + FileTransferManager ftm = FileTransferManager.getInstance(); + boolean created = false; + if (ftm.accept(message)) { + created = ftm.transferFile(message); + } + if (!created) { Toast.makeText(activity, R.string.file_not_on_remote_host, Toast.LENGTH_LONG).show(); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java b/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java index 726facac..6785dd31 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/XmppActivity.java @@ -17,28 +17,22 @@ import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentSender.SendIntentException; import android.content.ServiceConnection; -import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Point; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.nfc.NfcEvent; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.os.SystemClock; -import android.preference.PreferenceManager; import android.text.InputType; import android.util.DisplayMetrics; import android.view.Menu; @@ -62,17 +56,13 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import net.java.otr4j.session.SessionID; -import java.io.FileNotFoundException; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; -import java.util.concurrent.RejectedExecutionException; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusColors; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; -import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; @@ -104,7 +94,6 @@ public abstract class XmppActivity extends Activity { public boolean xmppConnectionServiceBound = false; protected boolean registeredListeners = false; - private DisplayMetrics metrics; protected int mTheme; private long mLastUiRefresh = 0; @@ -346,7 +335,6 @@ public abstract class XmppActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - metrics = getResources().getDisplayMetrics(); ExceptionHelper.init(getApplicationContext()); this.mTheme = findTheme(); setTheme(this.mTheme); @@ -1062,112 +1050,4 @@ public abstract class XmppActivity extends Activity { } } } - - class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { - private final WeakReference<ImageView> imageViewReference; - private final boolean setSize; - private Message message = null; - - public BitmapWorkerTask(ImageView imageView, boolean setSize) { - imageViewReference = new WeakReference<>(imageView); - this.setSize = setSize; - } - - @Override - protected Bitmap doInBackground(Message... params) { - message = params[0]; - try { - return ImageUtil.getThumbnail(message, (int) (metrics.density * 288), false); - } catch (FileNotFoundException e) { - return null; - } - } - - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap != null) { - final ImageView imageView = imageViewReference.get(); - if (imageView != null) { - imageView.setImageBitmap(bitmap); - imageView.setBackgroundColor(0x00000000); - if (setSize) { - imageView.setLayoutParams(new LinearLayout.LayoutParams( - bitmap.getWidth(), bitmap.getHeight())); - } - } - } - } - } - - public void loadBitmap(Message message, ImageView imageView, boolean setSize) { - Bitmap bm; - try { - bm = ImageUtil.getThumbnail(message,(int) (metrics.density * 288), true); - } catch (FileNotFoundException e) { - bm = null; - } - - if (bm != null) { - imageView.setImageBitmap(bm); - imageView.setBackgroundColor(0x00000000); - if (setSize) { - imageView.setLayoutParams(new LinearLayout.LayoutParams( - bm.getWidth(), bm.getHeight())); - } - } else { - if (cancelPotentialWork(message, imageView)) { - imageView.setBackgroundColor(0xff333333); - imageView.setImageDrawable(null); - final BitmapWorkerTask task = new BitmapWorkerTask(imageView, setSize); - final AsyncDrawable asyncDrawable = new AsyncDrawable( - getResources(), null, task); - imageView.setImageDrawable(asyncDrawable); - try { - task.execute(message); - } catch (final RejectedExecutionException ignored) { - } - } - } - } - - public static boolean cancelPotentialWork(Message message, - ImageView imageView) { - final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); - - if (bitmapWorkerTask != null) { - final Message oldMessage = bitmapWorkerTask.message; - if (oldMessage == null || message != oldMessage) { - bitmapWorkerTask.cancel(true); - } else { - return false; - } - } - return true; - } - - private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { - if (imageView != null) { - final Drawable drawable = imageView.getDrawable(); - if (drawable instanceof AsyncDrawable) { - final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; - return asyncDrawable.getBitmapWorkerTask(); - } - } - return null; - } - - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; - - public AsyncDrawable(Resources res, Bitmap bitmap, - BitmapWorkerTask bitmapWorkerTask) { - super(res, bitmap); - bitmapWorkerTaskReference = new WeakReference<>( - bitmapWorkerTask); - } - - public BitmapWorkerTask getBitmapWorkerTask() { - return bitmapWorkerTaskReference.get(); - } - } } diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java index e0e672e3..df7fc922 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/ConversationAdapter.java @@ -23,6 +23,7 @@ import java.util.concurrent.RejectedExecutionException; import de.thedevstack.conversationsplus.ConversationsPlusColors; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.ui.listeners.ShowResourcesListDialogListener; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.tzur.conversations.Settings; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Account; @@ -100,7 +101,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { || message.getTransferable().getStatus() != Transferable.STATUS_DELETED)) { mLastMessage.setVisibility(View.GONE); imagePreview.setVisibility(View.VISIBLE); - activity.loadBitmap(message, imagePreview, false); + ImageUtil.loadBitmap(message, imagePreview, mLastMessage, false); } else { Pair<String,Boolean> preview = UIHelper.getMessagePreview(activity,message); mLastMessage.setVisibility(View.VISIBLE); diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java index 79ab3bc3..e49575f6 100644 --- a/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/thedevstack/conversationsplus/ui/adapter/MessageAdapter.java @@ -13,9 +13,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.text.Spannable; import android.text.SpannableString; -import android.text.Spanned; import android.text.style.ForegroundColorSpan; -import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.text.util.Linkify; import android.util.Patterns; @@ -48,13 +46,14 @@ import de.thedevstack.conversationsplus.entities.FileParams; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.entities.Transferable; import de.thedevstack.conversationsplus.enums.FileStatus; -import de.thedevstack.conversationsplus.http.HttpConnectionManager; import de.thedevstack.conversationsplus.persistance.FileBackend; import de.thedevstack.conversationsplus.providers.ConversationsPlusFileProvider; import de.thedevstack.conversationsplus.services.AvatarService; +import de.thedevstack.conversationsplus.services.filetransfer.http.download.AutomaticFileDownload; import de.thedevstack.conversationsplus.ui.ConversationActivity; import de.thedevstack.conversationsplus.utils.CryptoHelper; import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.utils.ImageUtil; import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.utils.ui.TextViewUtil; @@ -119,7 +118,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) { + private void displayStatus(ViewHolder viewHolder, Message message, int type) { String filesize = null; String info = null; boolean error = false; @@ -177,12 +176,12 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (error && type == SENT) { viewHolder.time.setTextColor(ConversationsPlusColors.warning()); } else { - viewHolder.time.setTextColor(this.getMessageTextColor(darkBackground,false)); + viewHolder.time.setTextColor(this.getMessageTextColor(viewHolder.darkBackground,false)); } if (message.getEncryption() == Message.ENCRYPTION_NONE) { viewHolder.indicator.setVisibility(View.GONE); } else { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); + viewHolder.indicator.setImageResource(viewHolder.darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); viewHolder.indicator.setVisibility(View.VISIBLE); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { XmppAxolotlSession.Trust trust = message.getConversation() @@ -194,7 +193,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.indicator.setAlpha(1.0f); } else { viewHolder.indicator.clearColorFilter(); - if (darkBackground) { + if (viewHolder.darkBackground) { viewHolder.indicator.setAlpha(0.7f); } else { viewHolder.indicator.setAlpha(0.57f); @@ -202,7 +201,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } else { viewHolder.indicator.clearColorFilter(); - if (darkBackground) { + if (viewHolder.darkBackground) { viewHolder.indicator.setAlpha(0.7f); } else { viewHolder.indicator.setAlpha(0.57f); @@ -236,7 +235,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (message.hasFileAttached() && null != message.getFileParams() && null != viewHolder.remoteFileStatus) { FileStatus fileStatus = message.getFileParams().getFileStatus(); - if (fileStatus == FileStatus.DELETE_FAILED || fileStatus == FileStatus.DELETED || fileStatus == FileStatus.DELETING) { + if (fileStatus == FileStatus.DELETE_FAILED || fileStatus == FileStatus.DELETED || fileStatus == FileStatus.DELETING || fileStatus == FileStatus.NOT_FOUND) { viewHolder.remoteFileStatus.setVisibility(View.VISIBLE); switch (fileStatus) { case DELETE_FAILED: @@ -249,24 +248,28 @@ public class MessageAdapter extends ArrayAdapter<Message> { case DELETING: viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_inprogress); break; + case NOT_FOUND: + TextViewUtil.setColor(viewHolder.remoteFileStatus, R.color.error); + viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_not_found); + break; } } } } - private void displayInfoMessage(ViewHolder viewHolder, String text, boolean darkBackground) { + private void displayInfoMessage(ViewHolder viewHolder, String text) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } viewHolder.image.setVisibility(View.GONE); viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(text); - viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false)); + viewHolder.messageBody.setTextColor(getMessageTextColor(viewHolder.darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); viewHolder.messageBody.setTextIsSelectable(false); } - private void displayDecryptionFailed(ViewHolder viewHolder, boolean darkBackground) { + private void displayDecryptionFailed(ViewHolder viewHolder) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } @@ -274,12 +277,12 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setVisibility(View.VISIBLE); viewHolder.messageBody.setText(getContext().getString( R.string.decryption_failed)); - viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false)); + viewHolder.messageBody.setTextColor(getMessageTextColor(viewHolder.darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); viewHolder.messageBody.setTextIsSelectable(false); } - private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground) { + private void displayTextMessage(final ViewHolder viewHolder, final Message message) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } @@ -295,12 +298,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { body = message.getBody(); } final SpannableString formattedBody = new SpannableString(body); - int i = body.indexOf(Message.MERGE_SEPARATOR); - while(i >= 0) { - final int end = i + Message.MERGE_SEPARATOR.length(); - formattedBody.setSpan(new RelativeSizeSpan(0.3f),i,end,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - i = body.indexOf(Message.MERGE_SEPARATOR,end); - } if (message.getType() != Message.TYPE_PRIVATE) { if (message.hasMeCommand()) { final Spannable span = new SpannableString(formattedBody); @@ -326,7 +323,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } final Spannable span = new SpannableString(privateMarker + " " + formattedBody); - span.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground,false)), 0, privateMarker + span.setSpan(new ForegroundColorSpan(getMessageTextColor(viewHolder.darkBackground,false)), 0, privateMarker .length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); span.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarker.length(), @@ -366,7 +363,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setText(""); viewHolder.messageBody.setTextIsSelectable(false); } - viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true)); + viewHolder.messageBody.setTextColor(this.getMessageTextColor(viewHolder.darkBackground, true)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); } @@ -403,10 +400,15 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setAutoLinkMask(oldAutoLinkMask); } - private void displayDownloadableMessage(ViewHolder viewHolder, final Message message, int resId) { + private void displayDownloadableMessage(ViewHolder viewHolder, final Message message) { viewHolder.image.setVisibility(View.GONE); FileParams fileParams = message.getFileParams(); String btnText; + int resId = R.string.download_x_file; + if ((message.getTransferable() != null && message.getTransferable().getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) + || (null != fileParams && -1 == fileParams.getSize())) { + resId = R.string.check_x_filesize; + } if (null != fileParams) { this.displayFileInfoForFileMessage(message, viewHolder); btnText = activity.getString(resId, ""); @@ -465,26 +467,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); } - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.image.setVisibility(View.VISIBLE); - //TODO: Check what value add the following lines have (compared with setting height/width in XmppActivity.loadBitmap from thumbnail after thumbnail is created) - /* - FileParams params = message.getFileParams(); - double target = metrics.density * 288; - int scalledW; - int scalledH; - if (params.width <= params.height) { - scalledW = (int) (params.width / ((double) params.height / target)); - scalledH = (int) target; - } else { - scalledW = (int) target; - scalledH = (int) (params.height / ((double) params.width / target)); - } - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scalledW, scalledH); - layoutParams.setMargins(0, (int) (metrics.density * 4), 0, (int) (metrics.density * 4)); - viewHolder.image.setLayoutParams(layoutParams);*/ - //TODO Why should this be calculated by hand??? - activity.loadBitmap(message, viewHolder.image, true); + + ImageUtil.loadBitmap(message, viewHolder.image, viewHolder.messageBody, true); viewHolder.image.setOnClickListener(new OnClickListener() { @Override @@ -511,19 +495,35 @@ public class MessageAdapter extends ArrayAdapter<Message> { } private void displayFileMessage(final Message message, ViewHolder viewHolder) { - final FileParams fileParams = message.getFileParams(); - String mimeType = (null != fileParams) ? fileParams.getMimeType() : null; - if ((message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) - || (fileParams != null && fileParams.getWidth() > 0) - || (null != mimeType && mimeType.startsWith("image/"))) { + Transferable transferable = message.getTransferable(); + if (null != transferable) { + switch (transferable.getStatus()) { + case Transferable.STATUS_OFFER: + case Transferable.STATUS_OFFER_CHECK_FILESIZE: + displayDownloadableMessage(viewHolder, message); + break; + case Transferable.STATUS_UPLOADING: + displayFileMessage(message, viewHolder); + break; + case Transferable.STATUS_DELETED: + case Transferable.STATUS_CHECKING: + case Transferable.STATUS_FAILED: + case Transferable.STATUS_DOWNLOADING: + case Transferable.STATUS_UNKNOWN: + displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first); + break; + } + } else if (FileStatus.CHECKING_FILE_SIZE == message.getFileParams().getFileStatus()) { + displayInfoMessage(viewHolder, activity.getString(R.string.checking_remote_filesize)); + } else if (MessageUtil.isAttachedFileAnImage(message) && FileStatus.DOWNLOADED == message.getFileParams().getFileStatus()) { displayImageMessage(viewHolder, message); - } else if ((message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) - && null != fileParams && (FileStatus.NEEDS_DOWNLOAD != fileParams.getFileStatus() || 0 == fileParams.getWidth())) { + } else if ((MessageUtil.isTypeFileAndDecrypted(message) || FileStatus.DOWNLOADED == message.getFileParams().getFileStatus()) + && !MessageUtil.needsDownload(message)) { displayOpenableMessage(viewHolder, message); - } else if (Message.Decision.NEVER == message.treatAsDownloadable()) { - displayTextMessage(viewHolder, message, getItemViewType(message) == RECEIVED && !message.isValidInSession()); + } else if (Message.Decision.NEVER == message.treatAsDownloadable() || !MessageUtil.mayFileRemoteAvailable(message)) { + displayTextMessage(viewHolder, message); } else { - displayDownloadableMessage(viewHolder, message, R.string.download_x_file); + displayDownloadableMessage(viewHolder, message); } } @@ -660,48 +660,28 @@ public class MessageAdapter extends ArrayAdapter<Message> { this.displayAvatar(message, type, viewHolder); - boolean darkBackground = (type == RECEIVED && !isInValidSession); - this.displayStatus(viewHolder, message, type, darkBackground); + viewHolder.darkBackground = (type == RECEIVED && !isInValidSession); + this.displayStatus(viewHolder, message, type); - final Transferable transferable = message.getTransferable(); - if (null != transferable) { - switch (transferable.getStatus()) { - case Transferable.STATUS_OFFER: - displayDownloadableMessage(viewHolder, message, R.string.download_x_file); - break; - case Transferable.STATUS_OFFER_CHECK_FILESIZE: - displayDownloadableMessage(viewHolder, message, R.string.check_x_filesize); - break; - case Transferable.STATUS_UPLOADING: - displayFileMessage(message, viewHolder); - break; - case Transferable.STATUS_DELETED: - case Transferable.STATUS_CHECKING: - case Transferable.STATUS_FAILED: - case Transferable.STATUS_UNKNOWN: - displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,darkBackground); - break; - } - } else if (message.hasFileAttached()) { - if (message.trusted() + if (null != message.getTransferable() || message.hasFileAttached()) { + if (!(message.trusted() && MessageUtil.needsDownload(message) && ConversationsPlusPreferences.autoAcceptFileSize() > 0 - && (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) { - HttpConnectionManager.createNewDownloadConnection(message); - } else { - displayFileMessage(message, viewHolder); + && message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) { + new AutomaticFileDownload(false).transferFile(message); } + displayFileMessage(message, viewHolder); } else if (GeoHelper.isGeoUri(message.getBody())) { displayLocationMessage(viewHolder, message); } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { if (activity.hasPgp()) { if (account.getPgpDecryptionService().isRunning()) { - displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting), darkBackground); + displayInfoMessage(viewHolder, activity.getString(R.string.message_decrypting)); } else { - displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message), darkBackground); + displayInfoMessage(viewHolder, activity.getString(R.string.pgp_message)); } } else { - displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),darkBackground); + displayInfoMessage(viewHolder, activity.getString(R.string.install_openkeychain)); if (viewHolder != null) { viewHolder.message_box .setOnClickListener(new OnClickListener() { @@ -714,9 +694,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { - displayDecryptionFailed(viewHolder,darkBackground); + displayDecryptionFailed(viewHolder); } else { - displayTextMessage(viewHolder, message, darkBackground); + displayTextMessage(viewHolder, message); } if (type == RECEIVED) { @@ -812,6 +792,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { protected TextView status_message; protected TextView encryption; public TextView remoteFileStatus; + protected boolean darkBackground; } class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java b/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java index 77c313f7..95bd60dc 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/FileUtils.java @@ -175,7 +175,7 @@ public final class FileUtils { return null; } - String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); + String filename = FileUtils.getFilenameFromPath(path).toLowerCase(); final String lastPart = FileUtils.getLastExtension(filename); @@ -254,6 +254,19 @@ public final class FileUtils { return true; } + public static boolean isImage(String mimeType) { + return null != mimeType && mimeType.startsWith("image/"); + } + + public static String getFilenameFromPath(String path) { + String filename = null; + if (null != path && !path.isEmpty()) { + filename = path.substring(path.lastIndexOf('/') + 1); + } + + return filename; + } + private FileUtils() { // Utility class - do not instantiate } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java index 0b8ace95..eee18cc9 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java @@ -1,25 +1,38 @@ package de.thedevstack.conversationsplus.utils; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.media.ExifInterface; import android.net.Uri; +import android.os.AsyncTask; +import android.util.DisplayMetrics; import android.util.LruCache; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.concurrent.RejectedExecutionException; import de.thedevstack.android.logcat.Logging; +import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.thedevstack.conversationsplus.exceptions.ImageResizeException; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.entities.Message; import de.thedevstack.conversationsplus.persistance.FileBackend; +import de.thedevstack.conversationsplus.utils.ui.ViewUtil; /** * This util provides @@ -373,6 +386,129 @@ public final class ImageUtil { return inSampleSize; } + static class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { + private final WeakReference<ImageView> imageViewReference; + private final WeakReference<TextView> alternativeViewReference; + private final boolean setSize; + private Message message = null; + + public BitmapWorkerTask(ImageView imageView, TextView alternativeView, boolean setSize) { + imageViewReference = new WeakReference<>(imageView); + alternativeViewReference = new WeakReference<>(alternativeView); + this.setSize = setSize; + } + + @Override + protected Bitmap doInBackground(Message... params) { + message = params[0]; + try { + DisplayMetrics metrics = ConversationsPlusApplication.getAppContext().getResources().getDisplayMetrics(); + return ImageUtil.getThumbnail(message, (int) (metrics.density * 288), false); + } catch (FileNotFoundException e) { + return null; + } + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + final ImageView imageView = imageViewReference.get(); + final TextView alternativeView = alternativeViewReference.get(); + if (bitmap != null) { + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + if (setSize) { + imageView.setLayoutParams(new LinearLayout.LayoutParams( + bitmap.getWidth(), bitmap.getHeight())); + } + ViewUtil.gone(alternativeView); + ViewUtil.visible(imageView); + } + } else { + ViewUtil.visible(alternativeView); + alternativeView.setText(message.getBody()); // TODO Should be the same as MessageAdapter.displayText... or display preview of ConversationAdapter + ViewUtil.gone(imageView); + } + } + } + + public static void loadBitmap(Message message, ImageView imageView, TextView alternativeView, boolean setSize) { + Bitmap bm; + try { + DisplayMetrics metrics = ConversationsPlusApplication.getAppContext().getResources().getDisplayMetrics(); + bm = ImageUtil.getThumbnail(message,(int) (metrics.density * 288), true); + } catch (FileNotFoundException e) { + bm = null; + } + + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + if (setSize) { + imageView.setLayoutParams(new LinearLayout.LayoutParams( + bm.getWidth(), bm.getHeight())); + } + + ViewUtil.gone(alternativeView); + ViewUtil.visible(imageView); + } else { + if (cancelPotentialWork(message, imageView)) { + imageView.setBackgroundColor(0xff333333); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView, alternativeView, setSize); + final AsyncDrawable asyncDrawable = new AsyncDrawable(ConversationsPlusApplication.getAppContext().getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + ViewUtil.gone(alternativeView); + ViewUtil.visible(imageView); + try { + task.execute(message); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + public static boolean cancelPotentialWork(Message message, + ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Message oldMessage = bitmapWorkerTask.message; + if (oldMessage == null || message != oldMessage) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, + BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>( + bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + private ImageUtil() { // Static helper class } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java index acd73575..e0d627b9 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java @@ -27,6 +27,14 @@ public final class MessageUtil { if (null == message) { return; } + + // Ensure that for every message the fileParams are set after calling this method + FileParams fileParams = message.getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); + message.setFileParams(fileParams); + } + String body = message.getBody(); /** * there are a few cases where spaces result in an unwanted behavior, e.g. @@ -37,24 +45,27 @@ public final class MessageUtil { return; } - FileParams fileParams = message.getFileParams(); - if (null == fileParams) { - fileParams = new FileParams(); - message.setFileParams(fileParams); - } - try { URL url = new URL(body); if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { message.setTreatAsDownloadable(Message.Decision.NEVER); return; } + String extension = FileUtils.getRelevantExtension(url); if (message.isHttpUploaded()) { fileParams.setUrl(url.toString()); - message.setTreatAsDownloadable(Message.Decision.MUST); + if (null != extension + && (Transferable.WELL_KNOWN_EXTENSIONS.contains(extension.toLowerCase()) || Transferable.VALID_IMAGE_EXTENSIONS.contains(extension.toLowerCase()))) { + message.setTreatAsDownloadable(Message.Decision.MUST); + } else { + message.setTreatAsDownloadable(Message.Decision.NEVER); + fileParams.setFileStatus(FileStatus.UNDEFINED); + } + + extractFilename(message, url.toString()); return; } - String extension = FileUtils.getRelevantExtension(url); + if (extension == null) { message.setTreatAsDownloadable(Message.Decision.NEVER); return; @@ -65,23 +76,100 @@ public final class MessageUtil { if (MimeUtils.guessMimeTypeFromExtension(extension) != null) { message.setTreatAsDownloadable(Message.Decision.MUST); fileParams.setKeyAndIv(ivAndKey); - fileParams.setUrl(url.toString()); } else { message.setTreatAsDownloadable(Message.Decision.NEVER); } } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension) || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) { message.setTreatAsDownloadable(Message.Decision.SHOULD); - fileParams.setUrl(url.toString()); } else { message.setTreatAsDownloadable(Message.Decision.NEVER); } + if (message.treatAsDownloadable() == Message.Decision.MUST + || message.treatAsDownloadable() == Message.Decision.SHOULD) { + fileParams.setUrl(url.toString()); + extractFilename(message, url.toString()); + } } catch (MalformedURLException e) { message.setTreatAsDownloadable(Message.Decision.NEVER); } } + private static void extractFilename(Message message, String url) { + String originalFilename = FileUtils.getFilenameFromPath(url); + final String lowerCaseFilename = originalFilename.toLowerCase(); + final String lastPart = FileUtils.getLastExtension(lowerCaseFilename); + + detectAndSetEncryption(lastPart, message); + + String filenameExtension; + if (!lastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) { + filenameExtension = FileUtils.getSecondToLastExtension(lowerCaseFilename); + originalFilename = originalFilename.replace("." + lastPart, ""); + } else { + filenameExtension = lastPart; + } + message.setRelativeFilePath(message.getUuid() + "." + filenameExtension); + + message.getFileParams().setOriginalFilename(originalFilename); + } + + private static void detectAndSetEncryption(String lastPart, Message message) { + if (!lastPart.isEmpty() && ("pgp".equals(lastPart) || "gpg".equals(lastPart))) { + message.setEncryption(Message.ENCRYPTION_PGP); + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { + message.setEncryption(Message.ENCRYPTION_NONE); + } else if ((message.getEncryption() == Message.ENCRYPTION_OTR + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL) + && message.getFileParams() != null && message.getFileParams().getKey() == null) { + // If an encryption is set for the message, but no key given -> decryption not possible + message.setEncryption(Message.ENCRYPTION_NONE); + } + } + + /** + * Checks if an attached file is an image or not. + * Prerequisite for calling this method: The check if a file is attached is done before. + * @param message + * @return + */ + public static boolean isAttachedFileAnImage(Message message) { + final FileParams fileParams = message.getFileParams(); + String mimeType = (null != fileParams) ? fileParams.getMimeType() : null; + return message.getType() == Message.TYPE_IMAGE + || (fileParams != null && fileParams.getWidth() > 0) + || (null != mimeType && mimeType.startsWith("image/")); + } + + public static boolean isTypeFileAndDecrypted(Message message) { + return message.getType() == Message.TYPE_FILE + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED; + } + + public static boolean isDecrypted(Message message) { + return message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED; + } + + /** + * Checks if the file status of an message is <b>NOT</b> one of: + * <ul> + * <li>FileStatus#NOT_FOUND</li> + * <li>FileStatus#DELETED</li> + * </ul> + * The file status does not guarantee that a file is really available or not. + * @param message + * @return + */ + public static boolean mayFileRemoteAvailable(Message message) { + FileStatus fileStatus = (null != message.getFileParams()) ? message.getFileParams().getFileStatus() : null; + return null != fileStatus + && FileStatus.NOT_FOUND != fileStatus + && FileStatus.DELETED != fileStatus; + } + public static boolean needsDownload(Message message) { FileStatus fileStatus = (null != message.getFileParams()) ? message.getFileParams().getFileStatus() : null; return (null == fileStatus diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java index a2ed6d81..e3680b81 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/UIHelper.java @@ -291,31 +291,28 @@ public class UIHelper { } public static String getHumanReadableFileSize(long size) { - String filesize; - if (size > (1.5 * 1024 * 1024)) { - filesize = size / (1024 * 1024)+ " MiB"; - } else if (size > 0) { - filesize = size / 1024 + " KiB"; - } else { - filesize = null; - } + return getHumanReadableDetailedFileSize(size, 1); + } - return filesize; + public static String getHumanReadableDetailedFileSize(long filesize) { + String size = getHumanReadableDetailedFileSize(filesize, 2); + return null == size ? "?" : size; } - public static String getHumanReadableDetailedFileSize(long filesize) { + public static String getHumanReadableDetailedFileSize(long filesize, int precision) { if (0 > filesize) { - return "?"; + return null; } double size = Double.valueOf(filesize); String[] sizes = {" bytes", " Kb", " Mb", " Gb", " Tb"}; int i = 0; - while (1023 < size) { + while (1023 < size && i < sizes.length - 1) { size /= 1024d; ++i; } + BigDecimal readableSize = new BigDecimal(size); - readableSize = readableSize.setScale(2, BigDecimal.ROUND_HALF_UP); - return readableSize.doubleValue() + sizes[i]; + readableSize = readableSize.setScale(precision, BigDecimal.ROUND_HALF_UP); + return ((0 == i) ? String.valueOf(readableSize.intValue()) : String.valueOf(readableSize.doubleValue())) + sizes[i]; } } diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java index 27a269f2..faa9a5ed 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ui/TextViewUtil.java @@ -4,6 +4,8 @@ import android.support.annotation.StringRes; import android.view.View; import android.widget.TextView; +import de.thedevstack.conversationsplus.ConversationsPlusColors; + /** * */ @@ -61,7 +63,7 @@ public final class TextViewUtil extends ViewUtil { public static void setColorEnabledAndTextResId(TextView tv, Integer color, Boolean enabled, @StringRes Integer resid) { if (null != color) { - tv.setTextColor(color); + tv.setTextColor(ConversationsPlusColors.byId(color)); } if (enabled != null) { diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ui/ViewUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ui/ViewUtil.java index 77422587..170a6401 100644 --- a/src/main/java/de/thedevstack/conversationsplus/utils/ui/ViewUtil.java +++ b/src/main/java/de/thedevstack/conversationsplus/utils/ui/ViewUtil.java @@ -10,30 +10,35 @@ import android.view.View; public class ViewUtil { public static <T extends View> T visible(View parentView, @IdRes int textViewId) { - T tv = (T) parentView.findViewById(textViewId); - if (null != tv) { - tv.setVisibility(View.VISIBLE); - } - - return tv; + return ViewUtil.visible((T) parentView.findViewById(textViewId)); } public static <T extends View> T invisible(View parentView, @IdRes int textViewId) { - T tv = (T) parentView.findViewById(textViewId); - if (null != tv) { - tv.setVisibility(View.INVISIBLE); - } - - return tv; + return ViewUtil.invisible((T) parentView.findViewById(textViewId)); } public static <T extends View> T gone(View parentView, @IdRes int textViewId) { - T tv = (T) parentView.findViewById(textViewId); - if (null != tv) { - tv.setVisibility(View.GONE); + return ViewUtil.gone((T) parentView.findViewById(textViewId)); + } + + public static <T extends View> T gone(T view) { + if (null != view) { + view.setVisibility(View.GONE); } + return view; + } - return tv; + public static <T extends View> T visible(T view) { + if (null != view) { + view.setVisibility(View.VISIBLE); + } + return view; } + public static <T extends View> T invisible(T view) { + if (null != view) { + view.setVisibility(View.INVISIBLE); + } + return view; + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java index ca823926..aa85430b 100644 --- a/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java +++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/jingle/JingleConnection.java @@ -1007,7 +1007,7 @@ public class JingleConnection implements Transferable { return this.mStatus; } - @Override + public long getFileSize() { if (this.file != null) { return this.file.getExpectedSize(); diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 251db18e..eb1fefb8 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -684,4 +684,6 @@ <string name="cplus_open">Open</string> <string name="cplus_remote_file_delete_service_unavailable">Remote File Deletion Service currently unavailable. Please try again later.</string> <string name="cplus_remote_file_delete_failed">Failed to delete remote file.</string> + <string name="remote_filestatus_not_found">Remote file not found.</string> + <string name="checking_remote_filesize">Checking file size on remote host.</string> </resources> diff --git a/src/test/java/de/thedevstack/conversationsplus/utils/UIHelperTest.java b/src/test/java/de/thedevstack/conversationsplus/utils/UIHelperTest.java new file mode 100644 index 00000000..2409413d --- /dev/null +++ b/src/test/java/de/thedevstack/conversationsplus/utils/UIHelperTest.java @@ -0,0 +1,29 @@ +package de.thedevstack.conversationsplus.utils; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +/** + * + */ +public class UIHelperTest { + @Test + public void testGetHumanReadableDetailedFileSizeBytes() { + String result = UIHelper.getHumanReadableDetailedFileSize(1); + assertEquals("1 bytes", result); + result = UIHelper.getHumanReadableDetailedFileSize(120); + assertEquals("120 bytes", result); + result = UIHelper.getHumanReadableDetailedFileSize(1023); + assertEquals("1023 bytes", result); + } + + @Test + public void testGetHumanReadableDetailedFileSizeKilobytes() { + String result = UIHelper.getHumanReadableDetailedFileSize(1024); + assertEquals("1 kb", result); + result = UIHelper.getHumanReadableDetailedFileSize(120); + assertEquals("120 bytes", result); + result = UIHelper.getHumanReadableDetailedFileSize(1023); + assertEquals("1023 bytes", result); + } +} |