aboutsummaryrefslogtreecommitdiffstats
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java58
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java37
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java130
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java17
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java82
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java60
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java21
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java (renamed from src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java)32
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java181
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java101
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java36
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java23
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java235
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java91
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java24
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java178
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java23
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java12
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java22
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java50
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java89
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java97
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java151
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java92
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java36
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java35
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java53
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java7
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java37
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java18
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java45
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java23
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java9
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java6
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java7
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java22
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java71
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java110
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java52
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java17
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java17
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java20
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java26
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java26
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java17
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java17
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java25
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java56
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java8
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java30
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java35
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java39
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java9
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java46
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java22
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java31
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java53
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java12
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java43
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java46
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java30
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java45
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java14
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java12
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java9
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Conversation.java7
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java169
-rw-r--r--src/main/java/eu/siacs/conversations/generator/IqGenerator.java14
-rw-r--r--src/main/java/eu/siacs/conversations/generator/MessageGenerator.java24
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java45
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java16
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java242
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java9
-rw-r--r--src/main/java/eu/siacs/conversations/parser/PresenceParser.java16
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java51
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java87
-rw-r--r--src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java18
-rw-r--r--src/main/java/eu/siacs/conversations/services/ExportLogsService.java2
-rw-r--r--src/main/java/eu/siacs/conversations/services/MessageArchiveService.java4
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java3
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java119
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java4
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java30
-rw-r--r--src/main/java/eu/siacs/conversations/ui/SettingsActivity.java5
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java68
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java19
-rw-r--r--src/main/java/eu/siacs/conversations/utils/FileUtils.java23
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java19
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java8
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java4
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java5
-rw-r--r--src/main/res/layout/message_sent.xml12
-rw-r--r--src/main/res/menu/message_context.xml4
-rw-r--r--src/main/res/values-de/strings.xml1
-rw-r--r--src/main/res/values/strings.xml5
96 files changed, 3269 insertions, 844 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java
index 4b11bb4a..fb1e9392 100644
--- a/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java
+++ b/src/main/java/de/thedevstack/conversationsplus/ConversationsPlusApplication.java
@@ -3,15 +3,25 @@ package de.thedevstack.conversationsplus;
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.os.PowerManager;
import android.preference.PreferenceManager;
import java.io.File;
+import java.security.SecureRandom;
+import de.duenndns.ssl.MemorizingTrustManager;
+import de.thedevstack.conversationsplus.http.HttpClient;
import de.thedevstack.conversationsplus.utils.ImageUtil;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager;
+import de.thedevstack.conversationsplus.services.filetransfer.http.upload.HttpUploadFileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.jingle.JingleFileTransferService;
import eu.siacs.conversations.R;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.utils.PRNGFixes;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
+import okhttp3.OkHttpClient;
/**
* This class is used to provide static access to the applicationcontext.
@@ -25,6 +35,9 @@ public class ConversationsPlusApplication extends Application {
private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
+ private MemorizingTrustManager memorizingTrustManager;
+ private SecureRandom secureRandom;
+
/**
* Initializes the application and saves its instance.
*/
@@ -32,8 +45,21 @@ public class ConversationsPlusApplication extends Application {
super.onCreate();
ConversationsPlusApplication.instance = this;
ConversationsPlusPreferences.init(PreferenceManager.getDefaultSharedPreferences(getAppContext()));
+ this.initializeSecurity();
ImageUtil.initBitmapCache();
FileBackend.init();
+ FileTransferManager.init(new HttpUploadFileTransferService(), new JingleFileTransferService());
+ HttpConnectionManager.init();
+ HttpClient.init();
+ }
+
+ /**
+ * Initializes security features.
+ */
+ private void initializeSecurity() {
+ PRNGFixes.apply();
+ this.secureRandom = new SecureRandom();
+ ConversationsPlusApplication.updateMemorizingTrustmanager();
}
/**
@@ -102,4 +128,36 @@ public class ConversationsPlusApplication extends Application {
public static String getNameAndVersion() {
return getName() + " " + getVersion();
}
+
+ public static PowerManager.WakeLock createPartialWakeLock(String name) {
+ PowerManager powerManager = (PowerManager) getAppContext().getSystemService(Context.POWER_SERVICE);
+ return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
+ }
+
+ public static MemorizingTrustManager getMemorizingTrustManager() {
+ return getInstance().memorizingTrustManager;
+ }
+
+ public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
+ this.memorizingTrustManager = trustManager;
+ }
+
+ public static void updateMemorizingTrustmanager() {
+ final MemorizingTrustManager tm;
+ if (ConversationsPlusPreferences.dontTrustSystemCAs()) {
+ tm = new MemorizingTrustManager(getAppContext(), null);
+ } else {
+ tm = new MemorizingTrustManager(getAppContext());
+ }
+ 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/dto/RemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java
new file mode 100644
index 00000000..58b64b28
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/dto/RemoteFile.java
@@ -0,0 +1,37 @@
+package de.thedevstack.conversationsplus.dto;
+
+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;
+ private final String path;
+
+ public RemoteFile(@NonNull String path) {
+ this.path = path;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RemoteFile that = (RemoteFile) o;
+
+ return path.equals(that.path);
+
+ }
+
+ @Override
+ public int hashCode() {
+ return path.hashCode();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java b/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java
new file mode 100644
index 00000000..1eef078f
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/entities/FileParams.java
@@ -0,0 +1,130 @@
+package de.thedevstack.conversationsplus.entities;
+
+import de.thedevstack.conversationsplus.enums.FileStatus;
+
+import eu.siacs.conversations.utils.MimeUtils;
+
+/**
+ *
+ */
+public class FileParams {
+ private String name;
+ private String path;
+ private String url;
+ private String mimeType;
+ private long size = 0;
+ private int width = 0;
+ private int height = 0;
+ private FileStatus fileStatus;
+
+ public FileParams() {
+ fileStatus = FileStatus.UNDEFINED;
+ }
+
+ public FileParams(String url) {
+ this();
+ this.url = url;
+ }
+
+ public FileParams(String url, long size) {
+ this(url);
+ this.size = size;
+ }
+
+ public FileParams(String url, long size, int width, int height) {
+ this(url, size);
+ this.width = width;
+ this.height = height;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ public void setMimeType(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * Sets the path to the file.
+ * If no file name is stored yet here - this method tries to extract the file name from the path.
+ * If the file name is stored here and the path does not end with the name - the name is appended to the path.
+ * @param path the path to be stored
+ */
+ public void setPath(String path) {
+ if (null != path) {
+ if (null != this.name) {
+ path = (!path.endsWith(this.name)) ? path + "/" + this.name : path;
+ } else {
+ if (!path.endsWith("/")) {
+ this.setName(path.substring(path.lastIndexOf('/') + 1));
+ }
+ }
+ if (null == this.mimeType) {
+ int start = path.lastIndexOf('.') + 1;
+ if (start < path.length()) {
+ String extension = path.substring(start);
+ this.mimeType = MimeUtils.guessMimeTypeFromExtension(extension);
+ }
+ }
+ }
+
+ this.path = path;
+ }
+
+ public boolean isRemoteAvailable() {
+ return null != this.url || FileStatus.UPLOADED == this.fileStatus || FileStatus.DELETE_FAILED == this.fileStatus;
+ }
+
+ public void setFileStatus(FileStatus fileStatus) {
+ this.fileStatus = fileStatus;
+ }
+
+ public FileStatus getFileStatus() {
+ return this.fileStatus;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java b/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java
new file mode 100644
index 00000000..b6a4ef9a
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/enums/FileStatus.java
@@ -0,0 +1,17 @@
+package de.thedevstack.conversationsplus.enums;
+
+/**
+ * Created by steckbrief on 23.08.2016.
+ */
+public enum FileStatus {
+ NOT_DOWNLOADED,
+ DOWNLOADED,
+ DOWNLOAD_FAILED,
+ DELETED,
+ DELETE_FAILED,
+ UPLOADED,
+ NEEDS_UPLOAD,
+ UNDEFINED,
+ NEEDS_DOWNLOAD,
+ DELETING;
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java b/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java
new file mode 100644
index 00000000..e1a38067
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/http/HttpClient.java
@@ -0,0 +1,82 @@
+package de.thedevstack.conversationsplus.http;
+
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
+import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.SSLSocketHelper;
+
+import okhttp3.OkHttpClient;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public final class HttpClient {
+ private static HttpClient INSTANCE;
+ private boolean interactive = false;
+ private OkHttpClient client;
+
+ public static void init() {
+ INSTANCE = new HttpClient();
+ }
+
+ public static synchronized OkHttpClient getClient(boolean interactive) {
+ if (INSTANCE.interactive != interactive) {
+ INSTANCE.interactive = interactive;
+ INSTANCE.buildClient();
+ }
+ return INSTANCE.client;
+ }
+
+ private HttpClient() {
+ this.buildClient();
+ }
+
+ private void buildClient() {
+ OkHttpClient.Builder builder = new OkHttpClient.Builder();
+ this.initTrustManager(builder);
+ this.client = builder.build();
+ }
+
+ public void initTrustManager(final OkHttpClient.Builder builder) {
+ 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);
+
+ }
+
+ builder.sslSocketFactory(sf, trustManager);
+ builder.hostnameVerifier(hostnameVerifier);
+ } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java
deleted file mode 100644
index ba5f4a2c..00000000
--- a/src/main/java/de/thedevstack/conversationsplus/persistance/MessageDatabaseAccess.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.thedevstack.conversationsplus.persistance;
-
-import android.content.ContentValues;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-
-import de.thedevstack.android.logcat.Logging;
-
-import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.xmpp.jid.InvalidJidException;
-import eu.siacs.conversations.xmpp.jid.Jid;
-
-/**
- * Created by steckbrief on 15.04.2016.
- */
-public class MessageDatabaseAccess {
- public static final String TABLE_NAME_ADDITIONAL_PARAMETERS = "message_parameters";
- public static final String COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD = "httpupload";
- public static final String COLUMN_NAME_MSG_PARAMS_MSGUUID = "message_uuid";
- public static final String COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION = "treatasdownloadable_decision";
-
- public static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V0 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " ("
- + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, "
- + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, "
- + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', "
- + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)";
-
- public static ContentValues getAdditionalParametersContentValues(Message message) {
- ContentValues additionalParameters = new ContentValues();
- additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID, message.getUuid());
- additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD, message.isHttpUploaded() ? 1 : 0);
- additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION, message.treatAsDownloadable().name());
-
- return additionalParameters;
- }
-
- public static void populateMessageParametersFromCursor(Cursor cursor, Message message) {
- boolean isHttpUploaded = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD) == 1;
- message.setHttpUploaded(isHttpUploaded);
- String downloadable = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION);
- Message.Decision treatAsDownloadable = Message.Decision.NOT_DECIDED;
- try {
- treatAsDownloadable = Message.Decision.valueOf(downloadable);
- } catch (IllegalArgumentException e) {
- // Should only happen if the database is corrupted, but to be on the save side catch it here
- Logging.e("db.msg", "Unknown Decision for treatAsDownloadable found: '" + downloadable + "'");
- }
- message.setTreatAsDownloadable(treatAsDownloadable);
- }
-
- public static void populateMessageParameters(SQLiteDatabase db, Message message) {
- Cursor paramsCursor = db.query(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS,
- null, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?",
- new String[] {message.getUuid()},
- null, null, null, null);
- paramsCursor.moveToNext();
- MessageDatabaseAccess.populateMessageParametersFromCursor(paramsCursor, message);
- paramsCursor.close();
- }
-}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java
new file mode 100644
index 00000000..407859c7
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/AbstractDatabaseAccess.java
@@ -0,0 +1,21 @@
+package de.thedevstack.conversationsplus.persistance.db.access;
+
+import android.database.sqlite.SQLiteDatabase;
+
+/**
+ * Created by steckbrief on 24.08.2016.
+ */
+public abstract class AbstractDatabaseAccess {
+ static void executeAlterStatements(SQLiteDatabase db, String... alterStatements) {
+ for (String alterStatement : alterStatements) {
+ db.execSQL(alterStatement);
+ }
+ }
+
+ static void addNewColumns(SQLiteDatabase db, String tableName, String... columnDefinitions) {
+ for (String columnDefinition : columnDefinitions) {
+ String alterStatement = "ALTER TABLE " + tableName + " ADD COLUMN " + columnDefinition;
+ db.execSQL(alterStatement);
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java
index 7e3fdab0..122e5160 100644
--- a/src/main/java/de/thedevstack/conversationsplus/persistance/CursorHelper.java
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/CursorHelper.java
@@ -1,4 +1,4 @@
-package de.thedevstack.conversationsplus.persistance;
+package de.thedevstack.conversationsplus.persistance.db.access;
import android.database.Cursor;
@@ -7,7 +7,7 @@ import android.database.Cursor;
*/
public abstract class CursorHelper {
- static double getDouble(Cursor cursor, String columnName) {
+ public static double getDouble(Cursor cursor, String columnName) {
if (null == cursor) {
return Double.MIN_VALUE;
}
@@ -18,7 +18,7 @@ public abstract class CursorHelper {
return getDouble(cursor, columnIndex);
}
- static String getString(Cursor cursor, String columnName) {
+ public static String getString(Cursor cursor, String columnName) {
if (null == cursor) {
return null;
}
@@ -29,7 +29,7 @@ public abstract class CursorHelper {
return getString(cursor, columnIndex);
}
- static float getFloat(Cursor cursor, String columnName) {
+ public static float getFloat(Cursor cursor, String columnName) {
if (null == cursor) {
return Float.MIN_VALUE;
}
@@ -40,7 +40,7 @@ public abstract class CursorHelper {
return getFloat(cursor, columnIndex);
}
- static int getInt(Cursor cursor, String columnName) {
+ public static int getInt(Cursor cursor, String columnName) {
if (null == cursor) {
return Integer.MIN_VALUE;
}
@@ -51,7 +51,7 @@ public abstract class CursorHelper {
return getInt(cursor, columnIndex);
}
- static long getLong(Cursor cursor, String columnName) {
+ public static long getLong(Cursor cursor, String columnName) {
if (null == cursor) {
return Long.MIN_VALUE;
}
@@ -62,7 +62,7 @@ public abstract class CursorHelper {
return getLong(cursor, columnIndex);
}
- static int getShort(Cursor cursor, String columnName) {
+ public static int getShort(Cursor cursor, String columnName) {
if (null == cursor) {
return Short.MIN_VALUE;
}
@@ -73,7 +73,7 @@ public abstract class CursorHelper {
return getShort(cursor, columnIndex);
}
- static byte[] getBlob(Cursor cursor, String columnName) {
+ public static byte[] getBlob(Cursor cursor, String columnName) {
if (null == cursor) {
return null;
}
@@ -94,7 +94,7 @@ public abstract class CursorHelper {
* the column name does not exist.
* @see Cursor#getColumnIndexOrThrow(String)
*/
- static int getColumnIndex(Cursor cursor, String columnName) {
+ public static int getColumnIndex(Cursor cursor, String columnName) {
return cursor.getColumnIndex(columnName);
}
@@ -108,7 +108,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as a byte array.
*/
- static byte[] getBlob(Cursor cursor, int columnIndex) {
+ public static byte[] getBlob(Cursor cursor, int columnIndex) {
return cursor.getBlob(columnIndex);
}
@@ -122,7 +122,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as a String.
*/
- static String getString(Cursor cursor, int columnIndex) {
+ public static String getString(Cursor cursor, int columnIndex) {
return cursor.getString(columnIndex);
}
@@ -137,7 +137,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as a short.
*/
- static short getShort(Cursor cursor, int columnIndex) {
+ public static short getShort(Cursor cursor, int columnIndex) {
return cursor.getShort(columnIndex);
}
@@ -152,7 +152,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as an int.
*/
- static int getInt(Cursor cursor, int columnIndex) {
+ public static int getInt(Cursor cursor, int columnIndex) {
return cursor.getInt(columnIndex);
}
@@ -167,7 +167,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as a long.
*/
- static long getLong(Cursor cursor, int columnIndex) {
+ public static long getLong(Cursor cursor, int columnIndex) {
return cursor.getLong(columnIndex);
}
@@ -182,7 +182,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as a float.
*/
- static float getFloat(Cursor cursor, int columnIndex) {
+ public static float getFloat(Cursor cursor, int columnIndex) {
return cursor.getFloat(columnIndex);
}
@@ -197,7 +197,7 @@ public abstract class CursorHelper {
* @param columnIndex the zero-based index of the target column.
* @return the value of that column as a double.
*/
- static double getDouble(Cursor cursor, int columnIndex) {
+ public static double getDouble(Cursor cursor, int columnIndex) {
return cursor.getDouble(columnIndex);
}
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java
new file mode 100644
index 00000000..139dc419
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/access/MessageDatabaseAccess.java
@@ -0,0 +1,181 @@
+package de.thedevstack.conversationsplus.persistance.db.access;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.persistance.db.migrations.FileParamsBodyToDatabaseFieldsMigration;
+
+import eu.siacs.conversations.entities.Message;
+
+/**
+ *
+ */
+public class MessageDatabaseAccess extends AbstractDatabaseAccess {
+ // since cplus db version 1
+ public static final String TABLE_NAME_ADDITIONAL_PARAMETERS = "message_parameters";
+ private static final String COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD = "httpupload";
+ private static final String COLUMN_NAME_MSG_PARAMS_MSGUUID = "message_uuid";
+ private static final String COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION = "treatasdownloadable_decision";
+ // since cplus db version 3
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_SIZE = "file_size";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_TYPE = "file_type";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_WIDTH = "file_width";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT = "file_height";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_STATUS = "file_status";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_NAME = "file_name";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_PATH = "file_path";
+ private static final String COLUMN_NAME_MSG_PARAMS_FILE_URL = "file_url";
+
+ private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE_V1 = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " ("
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', "
+ + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)";
+
+ private static final String TABLE_ADDITIONAL_PARAMETERS_CREATE = "CREATE TABLE " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + " ("
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + " TEXT, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD + " INTEGER DEFAULT 0, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION + " TEXT DEFAULT 'NOT_DECIDED', "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE + " TEXT DEFAULT NULL, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE + " INTEGER DEFAULT 0, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT + " INTEGER DEFAULT 0, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH + " INTEGER DEFAULT 0, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS + " TEXT DEFAULT 'UNDEFINED', "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_NAME + " TEXT DEFAULT NULL, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH + " TEXT DEFAULT NULL, "
+ + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_URL + " TEXT DEFAULT NULL, "
+ + "FOREIGN KEY(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") REFERENCES " + Message.TABLENAME + "(" + Message.UUID + ") ON DELETE CASCADE)";
+
+ public static void updateMessageParameters(SQLiteDatabase db, Message message, String uuid) {
+ String[] args = {uuid};
+ db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, MessageDatabaseAccess.getAdditionalParametersContentValues(message), MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", args);
+ }
+
+ public static ContentValues getAdditionalParametersContentValues(Message message) {
+ ContentValues additionalParameters = new ContentValues();
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID, message.getUuid());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD, message.isHttpUploaded() ? 1 : 0);
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION, message.treatAsDownloadable().name());
+ if (null != message.getFileParams()) {
+ FileParams fileParams = message.getFileParams();
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE, fileParams.getMimeType());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE, fileParams.getSize());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT, fileParams.getHeight());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH, fileParams.getWidth());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH, fileParams.getPath());
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_URL, fileParams.getUrl());
+ if (null != fileParams.getFileStatus()) {
+ additionalParameters.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS, fileParams.getFileStatus().name());
+ }
+ }
+
+ return additionalParameters;
+ }
+
+ private static void populateMessageParametersFromCursor(Cursor cursor, Message message) {
+ boolean isHttpUploaded = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_HTTPUPLOAD) == 1;
+ message.setHttpUploaded(isHttpUploaded);
+ String downloadable = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_TREATASDOWNLOADABLE_DECISION);
+ Message.Decision treatAsDownloadable = Message.Decision.NOT_DECIDED;
+ try {
+ treatAsDownloadable = Message.Decision.valueOf(downloadable);
+ } catch (IllegalArgumentException e) {
+ // Should only happen if the database is corrupted, but to be on the save side catch it here
+ Logging.e("db.msg", "Unknown Decision for treatAsDownloadable found: '" + downloadable + "'");
+ }
+
+ message.setTreatAsDownloadable(treatAsDownloadable);
+
+ if (message.hasFileAttached()) {
+ FileParams fileParams = new FileParams(message.getBody());
+ String fileType = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_TYPE);
+ fileParams.setMimeType(fileType);
+ String name = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_NAME);
+ fileParams.setName(name);
+ String path = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_PATH);
+ fileParams.setPath(path);
+ String url = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_URL);
+ fileParams.setUrl(url);
+ long fileSize = CursorHelper.getLong(cursor, COLUMN_NAME_MSG_PARAMS_FILE_SIZE);
+ fileParams.setSize(fileSize);
+ int imageHeight = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT);
+ fileParams.setHeight(imageHeight);
+ int imageWidth = CursorHelper.getInt(cursor, COLUMN_NAME_MSG_PARAMS_FILE_WIDTH);
+ fileParams.setWidth(imageWidth);
+ String status = CursorHelper.getString(cursor, COLUMN_NAME_MSG_PARAMS_FILE_STATUS);
+ if (null != status && !status.isEmpty()) {
+ FileStatus fileStatus = FileStatus.UNDEFINED;
+ try {
+ fileStatus = FileStatus.valueOf(status);
+ } catch (IllegalArgumentException e) {
+ // Should only happen if the database is corrupted, but to be on the save side catch it here
+ Logging.e("db.msg", "Unknown FileStatus for fileParams.fileStatus found: '" + status + "'");
+ }
+ fileParams.setFileStatus(fileStatus);
+ }
+ message.setFileParams(fileParams);
+ }
+ }
+
+ public static void populateMessageParameters(SQLiteDatabase db, Message message) {
+ Cursor paramsCursor = db.query(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS,
+ null, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?",
+ new String[] {message.getUuid()},
+ null, null, null, null);
+ paramsCursor.moveToNext();
+ MessageDatabaseAccess.populateMessageParametersFromCursor(paramsCursor, message);
+ paramsCursor.close();
+ }
+
+ public static void upgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 1 && newVersion >= 1) {
+ Logging.d("db.upgrade.cplus", "Creating additional parameters table for messages.");
+ db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V1);
+ db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") "
+ + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME);
+ }
+ if (oldVersion < 3 && newVersion >= 3) {
+ Logging.d("db.upgrade.cplus", "Upgrade " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS);
+ String[] columnDefinitions = {
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_TYPE + " TEXT DEFAULT NULL",
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE + " INTEGER DEFAULT 0",
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT + " INTEGER DEFAULT 0",
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH + " INTEGER DEFAULT 0",
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_STATUS + " TEXT DEFAULT 'UNDEFINED'",
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_NAME + " TEXT DEFAULT NULL",
+ MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_PATH + " TEXT DEFAULT NULL",
+ };
+
+ addNewColumns(db, MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, columnDefinitions);
+
+ Logging.d("db.upgrade.cplus", "Migrate file params from message body to database fields");
+ Cursor cursor = db.rawQuery("SELECT " + Message.UUID + ", " + Message.BODY + " FROM " + Message.TABLENAME + " WHERE " + Message.TYPE + "=? OR " + Message.TYPE + "=?", new String[] {String.valueOf(Message.TYPE_FILE), String.valueOf(Message.TYPE_IMAGE)});
+ while (cursor.moveToNext()) {
+ String uuid = CursorHelper.getString(cursor, Message.UUID);
+ String body = CursorHelper.getString(cursor, Message.BODY);
+ FileParams fileParams = FileParamsBodyToDatabaseFieldsMigration.getFileParams(body);
+ String newBody = fileParams.getUrl();
+ ContentValues values = new ContentValues();
+ values.put(Message.BODY, newBody);
+ db.update(Message.TABLENAME, values, Message.UUID + "=?", new String[] {uuid});
+
+ ContentValues parameterValues = new ContentValues();
+ parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_SIZE, fileParams.getSize());
+ parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_HEIGHT, fileParams.getHeight());
+ parameterValues.put(MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_FILE_WIDTH, fileParams.getWidth());
+
+ db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, parameterValues, MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", new String[] {uuid});
+ }
+ cursor.close();
+ }
+ }
+
+ public static void create(SQLiteDatabase db) {
+ // Create Conversations+ related tables
+ db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java b/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java
new file mode 100644
index 00000000..c0aa63c0
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/persistance/db/migrations/FileParamsBodyToDatabaseFieldsMigration.java
@@ -0,0 +1,101 @@
+package de.thedevstack.conversationsplus.persistance.db.migrations;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import de.thedevstack.conversationsplus.entities.FileParams;
+
+/**
+ * Created by steckbrief on 24.08.2016.
+ */
+public class FileParamsBodyToDatabaseFieldsMigration {
+
+ public static FileParams getFileParams(String body) {
+ FileParams params = getLegacyFileParams(body);
+ if (params != null) {
+ return params;
+ }
+ params = new FileParams();
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split("\\|");
+ switch (parts.length) {
+ case 1:
+ try {
+ params.setSize(Long.parseLong(parts[0]));
+ } catch (NumberFormatException e) {
+ try {
+ URL url = new URL(parts[0]);
+ params.setUrl(url.toString());
+ } catch (MalformedURLException e1) {
+ }
+ }
+ break;
+ case 2:
+ case 4:
+ try {
+ URL url = new URL(parts[0]);
+ params.setUrl(url.toString());
+ } catch (MalformedURLException e1) {
+ }
+ try {
+ params.setSize(Long.parseLong(parts[1]));
+ } catch (NumberFormatException e) {
+ }
+ try {
+ params.setWidth(Integer.parseInt(parts[2]));
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+ }
+ try {
+ params.setHeight(Integer.parseInt(parts[3]));
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+ }
+ break;
+ case 3:
+ try {
+ params.setSize(Long.parseLong(parts[0]));
+ } catch (NumberFormatException e) {
+ }
+ try {
+ params.setWidth(Integer.parseInt(parts[1]));
+ } catch (NumberFormatException e) {
+ }
+ try {
+ params.setHeight(Integer.parseInt(parts[2]));
+ } catch (NumberFormatException e) {
+ }
+ break;
+ }
+ return params;
+ }
+
+ private static FileParams getLegacyFileParams(String body) {
+ FileParams params = new FileParams();
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split(",");
+ if (parts.length == 3) {
+ try {
+ params.setSize(Long.parseLong(parts[0]));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ try {
+ params.setWidth(Integer.parseInt(parts[1]));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ try {
+ params.setHeight(Integer.parseInt(parts[2]));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return params;
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java
new file mode 100644
index 00000000..300d25e9
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/FileTransferService.java
@@ -0,0 +1,36 @@
+package de.thedevstack.conversationsplus.services;
+
+import eu.siacs.conversations.entities.Message;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferStatusListener;
+
+/**
+ * An implementation of this class transfers a file to another entity or server.
+ */
+public interface FileTransferService {
+ /**
+ * 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
+ */
+ boolean transferFile(Message message);
+ /**
+ * 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
+ */
+ boolean transferFile(Message message, boolean delay);
+
+ /**
+ * 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
+ */
+ boolean accept(Message message);
+
+ /**
+ * Adds one or more file transfer status listeners.
+ * @param listeners the listeners to add
+ */
+ void addFileTransferStatusListener(FileTransferStatusListener... listeners);
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java
new file mode 100644
index 00000000..c24603b7
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/AbstractFileTransferService.java
@@ -0,0 +1,23 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.services.FileTransferService;
+
+/**
+ *
+ */
+public abstract class AbstractFileTransferService implements FileTransferService {
+ private List<FileTransferStatusListener> statusListeners = new ArrayList<>();
+
+ @Override
+ public void addFileTransferStatusListener(FileTransferStatusListener... listeners) {
+ this.statusListeners.addAll(Arrays.asList(listeners));
+ }
+
+ protected void addStatusListenerToEntity(FileTransferEntity entity) {
+ entity.addFileTransferStatusListener(this.statusListeners.toArray(new FileTransferStatusListener[0]));
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java
new file mode 100644
index 00000000..7799fe8f
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferEntity.java
@@ -0,0 +1,235 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.persistance.FileBackend;
+
+/**
+ *
+ */
+public class FileTransferEntity implements Transferable {
+ /**
+ * Listeners to inform about a status change.
+ */
+ private final List<FileTransferStatusListener> statusListeners = new ArrayList<>();
+ /**
+ * The associated message.
+ */
+ private final Message message;
+ /**
+ * Number of attempts.
+ */
+ private int attempts = 0;
+ /**
+ * The status of the file transfer.
+ */
+ private FileTransferStatusEnum transferStatus;
+ /**
+ * Number of bytes transmitted.
+ */
+ private long transmitted = 0;
+ /**
+ * The associated file.
+ */
+ private DownloadableFile file;
+ /**
+ * The associated file as stream.
+ */
+ private InputStream fileInputStream;
+
+ /**
+ * 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 FileTransferEntity(Message message) {
+ this.message = message;
+ this.message.setTransferable(this);
+ this.file = FileBackend.getFile(message, false);
+ }
+
+ /**
+ * Start something.
+ * Empty implementation since documentation in interface is missing.
+ * @return <code>false</code>
+ */
+ @Override
+ public boolean start() {
+ return false;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Calculates the current progress in percent.
+ *
+ * @return the current progress in percent
+ */
+ @Override
+ public int getProgress() {
+ if (file == null) {
+ return 0;
+ }
+ return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
+ }
+
+ /**
+ * Cancels the file transfer and informs the listeners about cancellation.
+ */
+ @Override
+ public void cancel() {
+ this.transferStatus = FileTransferStatusEnum.CANCELED;
+
+ this.close();
+
+ for (FileTransferStatusListener listener : this.statusListeners) {
+ listener.onCancel(this);
+ }
+ }
+
+ /**
+ * Starts an transfer attempt.
+ */
+ public void startAttempt() {
+ this.attempts++;
+ this.transferStatus = FileTransferStatusEnum.TRANSFERRING;
+ }
+
+ /**
+ * Fails an file transfer and informs the listeners about failure.
+ *
+ * @param failureReason the reason of failure.
+ */
+ public void fail(FileTransferFailureReason failureReason) {
+ this.transferStatus = FileTransferStatusEnum.FAILED;
+
+ this.close();
+
+ failureReason.setAttempt(this.attempts);
+ for (FileTransferStatusListener listener : this.statusListeners) {
+ listener.onFailure(this, failureReason);
+ }
+ }
+
+ /**
+ * Updates the progress by adding parameter value to current progress value.
+ *
+ * @param progress the number of newly transferred bytes
+ */
+ public void updateProgress(long progress) {
+ if (0 == this.attempts) {
+ this.startAttempt();
+ }
+ this.transmitted += progress;
+ }
+
+ /**
+ * Set the status of the file transfer to FileTransferStatusEnum#TRANSFERRED and informs the listeners about success.
+ */
+ public void transferred() {
+ this.transferStatus = FileTransferStatusEnum.TRANSFERRED;
+
+ this.close();
+
+ for (FileTransferStatusListener listener : this.statusListeners) {
+ listener.onSuccess(this);
+ }
+ }
+
+ /**
+ * Closes the file input stream (if it is not yet closed) and removes association with message.
+ */
+ private void close() {
+ StreamUtil.close(this.fileInputStream);
+ this.getMessage().setTransferable(null);
+ }
+
+ /**
+ * Whether the file is transferred or not.
+ * @return <code>true</code> if the file is successfully transferred, <code>false</code> otherwise
+ */
+ public boolean isTransferred() {
+ return FileTransferStatusEnum.TRANSFERRED == this.transferStatus;
+ }
+
+ /**
+ * Whether the file transfer is canceled or not.
+ * @return <code>true</code> if the file transfer was canceled, <code>false</code> otherwise
+ */
+ public boolean isCanceled() {
+ return FileTransferStatusEnum.CANCELED == this.transferStatus;
+ }
+
+ /**
+ * Whether the file transfer failed or not.
+ * @return <code>true</code> if the file transfer failed, <code>false</code> otherwise
+ */
+ public boolean isFailed() {
+ return FileTransferStatusEnum.FAILED == this.transferStatus;
+ }
+
+ public void setFileInputStream(InputStream fileInputStream) {
+ this.fileInputStream = fileInputStream;
+ }
+
+ public InputStream getFileInputStream() {
+ return fileInputStream;
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+
+ public DownloadableFile getFile() {
+ return file;
+ }
+
+ public void addFileTransferStatusListener(FileTransferStatusListener... listeners) {
+ this.statusListeners.addAll(Arrays.asList(listeners));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FileTransferEntity that = (FileTransferEntity) o;
+
+ String uuid = message != null ? message.getUuid() : null;
+ String thatUuid = that.message != null ? that.message.getUuid() : null;
+
+ return uuid != null ? uuid.equals(thatUuid) : thatUuid == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return message != null && message.getUuid() != null ? message.getUuid().hashCode() : 0;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java
new file mode 100644
index 00000000..353c34d8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureReason.java
@@ -0,0 +1,91 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+/**
+ *
+ */
+public final class FileTransferFailureReason {
+ private final FileTransferFailureType type;
+ private int attempt;
+ private Exception causedByException;
+ private String failureMessage;
+
+ public static FileTransferFailureReason createRecoverableFailureReason(Exception e) {
+ return createFailureReason(FileTransferFailureType.RECOVERABLE, e.getMessage(), e);
+ }
+
+ public static FileTransferFailureReason createRecoverableFailureReason(String reason) {
+ return createFailureReason(FileTransferFailureType.RECOVERABLE, reason, null);
+ }
+
+ public static FileTransferFailureReason createRecoverableFailureReason(Exception e, String failureMessage) {
+ return createFailureReason(FileTransferFailureType.RECOVERABLE, failureMessage, e);
+ }
+
+ public static FileTransferFailureReason createLimitedRecoverableFailureReason(Exception e) {
+ return createFailureReason(FileTransferFailureType.LIMITEDRECOVERABLE, e.getMessage(), e);
+ }
+
+ public static FileTransferFailureReason createLimitedRecoverableFailureReason(String reason) {
+ return createFailureReason(FileTransferFailureType.LIMITEDRECOVERABLE, reason, null);
+ }
+
+ public static FileTransferFailureReason createLimitedRecoverableFailureReason(Exception e, String failureMessage) {
+ return createFailureReason(FileTransferFailureType.LIMITEDRECOVERABLE, failureMessage, e);
+ }
+
+ public static FileTransferFailureReason createNonRecoverableFailureReason(Exception e) {
+ return createFailureReason(FileTransferFailureType.NONRECOVERABLE, e.getMessage(), e);
+ }
+
+ public static FileTransferFailureReason createNonRecoverableFailureReason(String reason) {
+ return createFailureReason(FileTransferFailureType.NONRECOVERABLE, reason, null);
+ }
+
+ public static FileTransferFailureReason createNonRecoverableFailureReason(Exception e, String failureMessage) {
+ return createFailureReason(FileTransferFailureType.NONRECOVERABLE, failureMessage, e);
+ }
+
+ private static FileTransferFailureReason createFailureReason(FileTransferFailureType type, String message, Exception e) {
+ FileTransferFailureReason ftfr = new FileTransferFailureReason(type);
+ ftfr.setCausedByException(e);
+ if (null != e && (null == message || message.isEmpty())) {
+ message = e.getMessage();
+ }
+ ftfr.setFailureMessage(message);
+
+ return ftfr;
+
+ }
+
+ private FileTransferFailureReason(FileTransferFailureType type) {
+ this.type = type;
+ }
+
+ public void setFailureMessage(String failureMessage) {
+ this.failureMessage = failureMessage;
+ }
+
+ public String getFailureMessage() {
+ return failureMessage;
+ }
+
+ public FileTransferFailureType getType() {
+ return type;
+ }
+
+ public void setCausedByException(Exception causedByException) {
+ this.causedByException = causedByException;
+ }
+
+ public Exception getCausedByException() {
+ return causedByException;
+ }
+
+ public void setAttempt(int attempt) {
+ this.attempt = attempt;
+ }
+
+ public boolean isRecoverable() {
+ return this.type.isRetryPossible(this.attempt);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java
new file mode 100644
index 00000000..3daca38e
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferFailureType.java
@@ -0,0 +1,24 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+/**
+ *
+ */
+public enum FileTransferFailureType {
+ RECOVERABLE(Integer.MAX_VALUE),
+ LIMITEDRECOVERABLE(5),
+ NONRECOVERABLE(1);
+
+ int maxAttempts;
+
+ FileTransferFailureType(int maxAttempts) {
+ this.maxAttempts = maxAttempts;
+ }
+
+ public boolean isRetryPossible(int attempt) {
+ return attempt < this.maxAttempts;
+ }
+
+ public int getMaxAttempts() {
+ return this.maxAttempts;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java
new file mode 100644
index 00000000..112aafd1
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferManager.java
@@ -0,0 +1,178 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.services.FileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.http.upload.HttpFileTransferEntity;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Message;
+
+/**
+ *
+ */
+public class FileTransferManager implements FileTransferStatusListener {
+ private SortedSet<WeightedTransferService> transferServices;
+ private static final FileTransferManager INSTANCE = new FileTransferManager();
+ private final HashMap<String, WeightedTransferService> activeTransfers = new HashMap<>();
+
+ private FileTransferManager() {
+ this.transferServices = new TreeSet<>();
+ }
+
+ public static FileTransferManager getInstance() {
+ return INSTANCE;
+ }
+
+ public static void init(FileTransferService... fileTransferServices) {
+ if (null != fileTransferServices && fileTransferServices.length > 0) {
+ for (FileTransferService fts : fileTransferServices) {
+ addFileTransferService(fts);
+ }
+ }
+ }
+
+ public static void init(HashMap<Integer, FileTransferService> fileTransferServices) {
+ for (Map.Entry<Integer, FileTransferService> entry : fileTransferServices.entrySet()) {
+ addFileTransferService(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public static void addFileTransferService(int weight, FileTransferService fts) {
+ fts.addFileTransferStatusListener(INSTANCE);
+ INSTANCE.transferServices.add(new WeightedTransferService(weight, fts));
+ }
+
+ public static void addFileTransferService(FileTransferService fts) {
+ int weight = 1;
+ if (!INSTANCE.transferServices.isEmpty()) {
+ weight = INSTANCE.transferServices.last().weight + 1;
+ }
+ addFileTransferService(weight, fts);
+ }
+
+ /**
+ * 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) {
+ 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
+ */
+ public boolean transferFile(Message message, boolean delay) {
+ Logging.d(Config.LOGTAG, "send file message");
+ boolean transferSuccessfullyStarted = false;
+ for (WeightedTransferService wts : this.transferServices) {
+ try {
+ if (wts.fileTransferService.accept(message)) {
+ transferSuccessfullyStarted = this.startFileTransfer(message, delay, wts);
+ if (transferSuccessfullyStarted) {
+ break;
+ }
+ }
+ } catch (Exception e) {
+ //TODO Do real exception handling!!!!!
+ }
+ }
+ return transferSuccessfullyStarted;
+ }
+
+ /**
+ * 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) {
+ return message.needsUploading();
+ }
+
+ @Override
+ public void onFailure(FileTransferEntity entity, FileTransferFailureReason failureReason) {
+ WeightedTransferService wts = this.activeTransfers.get(entity.getMessage().getUuid());
+ if (null == wts) {
+ return;
+ }
+ boolean delayed = (entity instanceof HttpFileTransferEntity) && ((HttpFileTransferEntity) entity).isDelayed();
+ if (failureReason.isRecoverable()) {
+ wts.fileTransferService.transferFile(entity.getMessage(), delayed);
+ } else {
+ boolean retransferStarted = false;
+ this.activeTransfers.remove(entity.getMessage().getUuid());
+ for (WeightedTransferService newWts : this.transferServices.tailSet(wts)) {
+ if (newWts == wts) { // Same Reference
+ continue;
+ }
+ if (newWts.fileTransferService.accept(entity.getMessage())) {
+ retransferStarted = startFileTransfer(entity.getMessage(), delayed, newWts);
+ if (retransferStarted) {
+ break;
+ }
+ }
+ }
+ if (!retransferStarted) {
+ MessageUtil.markMessage(entity.getMessage(), Message.STATUS_SEND_FAILED);
+ }
+ }
+ }
+
+ @Override
+ public void onCancel(FileTransferEntity entity) {
+ this.activeTransfers.remove(entity.getMessage().getUuid());
+ MessageUtil.markMessage(entity.getMessage(), Message.STATUS_SEND_FAILED); // TODO New Status CANCELED!
+ }
+
+ @Override
+ public void onSuccess(FileTransferEntity entity) {
+ this.activeTransfers.remove(entity.getMessage().getUuid());
+ }
+
+ private boolean startFileTransfer(Message message, boolean delayed, WeightedTransferService wts) {
+ boolean transferSuccessfullyStarted = wts.fileTransferService.transferFile(message, delayed);
+ if (transferSuccessfullyStarted) {
+ this.activeTransfers.put(message.getUuid(), wts);
+ }
+ return transferSuccessfullyStarted;
+ }
+
+ static class WeightedTransferService implements Comparable<WeightedTransferService> {
+ int weight;
+ FileTransferService fileTransferService;
+
+ WeightedTransferService(int weight, FileTransferService service) {
+ this.weight = weight;
+ this.fileTransferService = service;
+ }
+
+ /**
+ * Compares this object to the specified object to determine their relative
+ * order.
+ *
+ * @param another the object to compare to this instance.
+ * @return a negative integer if this instance is less than {@code another};
+ * a positive integer if this instance is greater than
+ * {@code another}; 0 if this instance has the same order as
+ * {@code another}.
+ * @throws ClassCastException if {@code another} cannot be converted into something
+ * comparable to {@code this} instance.
+ */
+ @Override
+ public int compareTo(WeightedTransferService another) {
+ return this.weight - another.weight;
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java
new file mode 100644
index 00000000..e55d94d8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusEnum.java
@@ -0,0 +1,23 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+/**
+ * Status values of a file transfer.
+ */
+public enum FileTransferStatusEnum {
+ /**
+ * The file transfer is in progress.
+ */
+ TRANSFERRING,
+ /**
+ * The file transfer was finished successfully.
+ */
+ TRANSFERRED,
+ /**
+ * The file transfer failed.
+ */
+ FAILED,
+ /**
+ * The file transfer was canceled.
+ */
+ CANCELED;
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java
new file mode 100644
index 00000000..638a20cf
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/FileTransferStatusListener.java
@@ -0,0 +1,12 @@
+package de.thedevstack.conversationsplus.services.filetransfer;
+
+import eu.siacs.conversations.entities.Message;
+
+/**
+ *
+ */
+public interface FileTransferStatusListener {
+ void onFailure(FileTransferEntity entity, FileTransferFailureReason failureReason);
+ void onSuccess(FileTransferEntity entity);
+ void onCancel(FileTransferEntity entity);
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java
new file mode 100644
index 00000000..95b8450a
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFile.java
@@ -0,0 +1,22 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.delete;
+
+import android.support.annotation.NonNull;
+
+import eu.siacs.conversations.entities.Message;
+import de.thedevstack.conversationsplus.dto.RemoteFile;
+
+/**
+ *
+ */
+public class DeleteRemoteFile extends RemoteFile {
+ private final Message message;
+
+ public DeleteRemoteFile(@NonNull String path, @NonNull Message message) {
+ super(path);
+ this.message = message;
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java
new file mode 100644
index 00000000..f60efb56
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteRemoteFileService.java
@@ -0,0 +1,50 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.delete;
+
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.XmppSendUtil;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.FileTransferHttpDeleteSlotRequestPacketGenerator;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class DeleteRemoteFileService implements SimpleUserDecisionCallback {
+ private Message message;
+
+ public DeleteRemoteFileService(Message message) {
+ this.message = message;
+ }
+
+ public void deleteRemoteFile() {
+ if (this.message.isHttpUploaded()) {
+ String path = this.message.getBody();
+ if (this.message.hasFileOnRemoteHost()) {
+ path = this.message.getFileParams().getUrl();
+ }
+
+ DeleteRemoteFile remoteFile = new DeleteRemoteFile(path, this.message);
+ Account account = this.message.getConversation().getAccount();
+ Jid host = account.getXmppConnection().findDiscoItemByFeature(FileTransferHttp.NAMESPACE);
+ IqPacket request = FileTransferHttpDeleteSlotRequestPacketGenerator.generate(host, path);
+ MessageUtil.setAndSaveFileStatus(this.message, FileStatus.DELETING);
+ XmppSendUtil.sendIqPacket(account, request, new DeleteTokenReceived(remoteFile));
+ }
+ }
+
+ @Override
+ public void onYes() {
+ this.deleteRemoteFile();
+ }
+
+ @Override
+ public void onNo() {
+ // Nothing to do
+ }
+}
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
new file mode 100644
index 00000000..3151ca30
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/delete/DeleteTokenReceived.java
@@ -0,0 +1,89 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.delete;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.http.HttpClient;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete.DeleteSlotPacketParser;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+/**
+ *
+ */
+public class DeleteTokenReceived implements OnIqPacketReceived {
+ private static final String HEADER_NAME_DELETE_TOKEN = "X-FILETRANSFER-HTTP-DELETE-TOKEN";
+ private final DeleteRemoteFile remoteFile;
+
+ public DeleteTokenReceived(DeleteRemoteFile remoteFile) {
+ this.remoteFile = remoteFile;
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ try {
+ 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);
+ Request request = new Request.Builder()
+ .url(url)
+ .addHeader(HEADER_NAME_DELETE_TOKEN, deleteToken)
+ .delete()
+ .build();
+ client.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onFailure(Call call, IOException e) {
+ Logging.e("filetransfer.http.delete", "Error while connecting to '" + call.request().url() + "': " + e.getMessage());
+ MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), FileStatus.DELETE_FAILED);
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ if (response.isSuccessful()) {
+ MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), FileStatus.DELETED);
+ Logging.i("filetransfer.http.delete", "Remote file successfully deleted '" + remoteFile.getPath() + "'");
+ } else {
+ String detailedMessage = response.body().string();
+ FileStatus fileStatus = FileStatus.DELETE_FAILED;
+ switch (response.code()) {
+ case 403:
+ case 500:
+ try {
+ JSONObject jsonObject = new JSONObject(detailedMessage);
+ detailedMessage = jsonObject.getString("msg");
+ } catch (JSONException e) {
+ Logging.e("filetransfer.http.delete", "Failed to get error message from expected json response: " + detailedMessage);
+ }
+ break;
+ case 404:
+ fileStatus = FileStatus.DELETED;
+ Logging.i("filetransfer.http.delete", "Failed to delete file - it was already deleted.");
+ break;
+ }
+ Logging.e("filetransfer.http.delete", "Could not delete remote file '" + remoteFile.getPath() + "'. Response Code: " + response.code() + ", details: " + detailedMessage);
+ MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), fileStatus);
+ }
+ }
+ });
+
+ } catch (XmppException e) {
+ Logging.e("filetransfer.http.delete", "Error while trying to get the delete token: " + e.getMessage());
+ MessageUtil.setAndSaveFileStatus(remoteFile.getMessage(), FileStatus.DELETE_FAILED);
+ }
+ }
+}
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/HttpFileTransferEntity.java
new file mode 100644
index 00000000..9ec07679
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileTransferEntity.java
@@ -0,0 +1,97 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.upload;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferEntity;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadSlot;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.utils.CryptoHelper;
+
+/**
+ *
+ */
+public class HttpFileTransferEntity extends FileTransferEntity {
+ private HttpUploadSlot slot;
+ private final byte[] key;
+ private final boolean delayed;
+
+ public HttpFileTransferEntity(Message message, boolean delayed) {
+ super(message);
+ this.getMessage().setHttpUploaded(true);
+ this.getMessage().setNoDownloadable(); // TODO Set rmeote file status to uploaded
+ FileParams fileParams = this.getMessage().getFileParams();
+ if (null == fileParams) {
+ fileParams = new FileParams();
+ }
+ fileParams.setFileStatus(FileStatus.NEEDS_UPLOAD);
+ this.getMessage().setFileParams(fileParams);
+ if (Config.ENCRYPT_ON_HTTP_UPLOADED
+ || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
+ || message.getEncryption() == Message.ENCRYPTION_OTR) {
+ this.key = new byte[48];
+ ConversationsPlusApplication.getSecureRandom().nextBytes(this.key);
+ this.getFile().setKeyAndIv(this.key);
+ } else {
+ this.key = null;
+ }
+ this.delayed = delayed;
+ }
+
+ public void setSlot(HttpUploadSlot slot) {
+ this.slot = slot;
+ }
+
+ public String getGetUrl() {
+ return this.slot.getGetUrl();
+ }
+
+ public String getPutUrl() {
+ return this.slot.getPutUrl();
+ }
+
+ public byte[] getKey() {
+ return key;
+ }
+
+ public boolean isDelayed() {
+ return this.delayed;
+ }
+
+ @Override
+ public void fail(FileTransferFailureReason failureReason) {
+ this.getMessage().setHttpUploaded(false);
+ super.fail(failureReason);
+ }
+
+ @Override
+ public void cancel() {
+ this.getMessage().setHttpUploaded(false);
+ super.cancel();
+ }
+
+ @Override
+ public void transferred() {
+ try {
+ URL getUrl = new URL(this.getGetUrl());
+ if (this.getKey() != null) {
+ getUrl = new URL(getUrl.toString() + "#" + CryptoHelper.bytesToHex(this.getKey()));
+ }
+
+ this.getMessage().getFileParams().setFileStatus(FileStatus.UPLOADED);
+ MessageUtil.updateFileParams(this.getMessage(), getUrl);
+ } catch (MalformedURLException e) {
+ Logging.e("httpupload", "Not a valid get url");
+ }
+
+ super.transferred();
+ }
+}
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
new file mode 100644
index 00000000..9c949ed9
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpFileUploader.java
@@ -0,0 +1,151 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.upload;
+
+import android.os.PowerManager;
+
+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.util.Scanner;
+
+import javax.net.ssl.HttpsURLConnection;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUpload;
+
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.http.HttpConnectionManager;
+import eu.siacs.conversations.persistance.FileBackend;
+
+public class HttpFileUploader implements Runnable {
+ private static final String HTTP_METHOD = "PUT";
+ 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;
+
+ public HttpFileUploader(HttpFileTransferEntity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void run() {
+ this.upload();
+ }
+
+ private void upload() {
+ OutputStream os = null;
+
+ HttpURLConnection connection = null;
+ InputStream fileInputStream = null;
+ DownloadableFile file = this.entity.getFile();
+ PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_upload_" + entity.getMessage().getUuid());
+ try {
+ wakeLock.acquire();
+ Logging.d("httpupload", "uploading file to " + this.entity.getPutUrl());
+ URL putUrl = new URL(this.entity.getPutUrl());
+ fileInputStream = this.entity.getFileInputStream();
+ connection = (HttpURLConnection) putUrl.openConnection();
+
+ if (connection instanceof HttpsURLConnection) {
+ HttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
+ }
+ connection.setRequestMethod(HTTP_METHOD);
+ connection.setFixedLengthStreamingMode((int) file.getExpectedSize());
+ String mime = file.getMimeType();
+ connection.setRequestProperty(MIME_REQUEST_PROPERTY_NAME, mime == null ? HttpUpload.DEFAULT_MIME_TYPE : mime);
+ connection.setRequestProperty(USER_AGENT_REQUEST_PROPERTY_NAME, ConversationsPlusApplication.getNameAndVersion());
+ connection.setDoOutput(true);
+ connection.connect();
+ os = connection.getOutputStream();
+ int count = -1;
+ byte[] buffer = new byte[BUFFER_LENGTH];
+ while (((count = fileInputStream.read(buffer)) != -1) && !this.entity.isCanceled()) {
+ this.entity.updateProgress(count);
+ os.write(buffer, 0, count);
+ UiUpdateHelper.updateConversationUi();
+ }
+ os.flush();
+ os.close();
+ fileInputStream.close();
+ int code = connection.getResponseCode();
+ if (code == 200 || code == 201) {
+ Logging.d("httpupload", "finished uploading file");
+ this.entity.transferred();
+
+ FileBackend.updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService); // Why???
+
+ // Send the URL to the counterpart
+ Message message = this.entity.getMessage();
+ message.setBody(this.entity.getGetUrl());
+ message.setCounterpart(message.getConversation().getJid().toBareJid());
+ if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+ XmppConnectionServiceAccessor.xmppConnectionService.getPgpEngine().encrypt(message, new HttpUploadedFileEncryptionUiCallback(this.entity));
+ } else {
+ XmppConnectionServiceAccessor.xmppConnectionService.resendMessage(message, this.entity.isDelayed());
+ }
+ } else {
+ String httpResponseMessage = this.getErrorStreamContent(connection);
+ String errorMessage = "file upload failed: http code (" + code + ") " + httpResponseMessage;
+ Logging.e("httpupload", errorMessage);
+ FileTransferFailureReason failureReason = null;
+ switch (code) {
+ case 403:
+ failureReason = FileTransferFailureReason.createLimitedRecoverableFailureReason(errorMessage);
+ break;
+ case 404:
+ failureReason = FileTransferFailureReason.createNonRecoverableFailureReason("Upload URL not found");
+ break;
+ case 500:
+ failureReason = FileTransferFailureReason.createNonRecoverableFailureReason("Internal Server Error");
+ break;
+ default:
+ failureReason = FileTransferFailureReason.createRecoverableFailureReason(errorMessage);
+ }
+ this.entity.fail(failureReason);
+ }
+ } catch (UnknownHostException e) {
+ Logging.e("httpupload", "File upload failed due to unknown host. " + e.getMessage());
+ //if (!HAS_INTERNET_CONNECTION) {
+ this.entity.fail(FileTransferFailureReason.createRecoverableFailureReason(e, "Missing internet connection"));
+ //}
+ } catch (IOException e) {
+ String httpResponseMessage = this.getErrorStreamContent(connection);
+ Logging.e("httpupload", ((null != httpResponseMessage) ? ("http response: " + httpResponseMessage + ", ") : "") + "exception message: " + e.getMessage());
+ // TODO: Differentiate IOException while internet connection wasn't available and others
+ this.entity.fail(FileTransferFailureReason.createRecoverableFailureReason(e, httpResponseMessage));
+ } finally {
+ StreamUtil.close(os);
+ StreamUtil.close(fileInputStream);
+ if (connection != null) {
+ connection.disconnect();
+ }
+ wakeLock.release();
+ }
+ }
+
+ private String getErrorStreamContent(HttpURLConnection connection) {
+ InputStream errorStream = null;
+ String httpResponseMessage = null;
+ try {
+ errorStream = connection.getErrorStream();
+ if (null != errorStream) {
+ Scanner scanner = new Scanner(errorStream).useDelimiter("\\A");
+ if (scanner.hasNext()) {
+ httpResponseMessage = scanner.next();
+ }
+ }
+ } finally {
+ StreamUtil.close(errorStream);
+ }
+ return httpResponseMessage;
+ }
+} \ No newline at end of file
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
new file mode 100644
index 00000000..7fe12dc6
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadFileTransferService.java
@@ -0,0 +1,92 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.upload;
+
+import android.util.Pair;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+import de.thedevstack.android.logcat.Logging;
+import eu.siacs.conversations.services.AbstractConnectionManager;
+import de.thedevstack.conversationsplus.services.FileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+import de.thedevstack.conversationsplus.utils.XmppSendUtil;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUpload;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadRequestSlotPacketGenerator;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class HttpUploadFileTransferService extends AbstractFileTransferService implements FileTransferService {
+
+ public HttpUploadFileTransferService() {
+ }
+
+ /**
+ * 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 started successfully, <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 started successfully, <code>false</code> otherwise
+ */
+ @Override
+ public boolean transferFile(Message message, boolean delay) {
+ Logging.d("httpupload", "Starting to upload file");
+ boolean started = false;
+ try {
+ final HttpFileTransferEntity entity = new HttpFileTransferEntity(message, delay);
+ this.addStatusListenerToEntity(entity);
+ entity.startAttempt();
+ Account account = message.getConversation().getAccount();
+ DownloadableFile file = entity.getFile();
+ Pair<InputStream, Integer> inputStreamAndExpectedSize = AbstractConnectionManager.createInputStream(file, true);
+
+ entity.setFileInputStream(inputStreamAndExpectedSize.first);
+ file.setExpectedSize(inputStreamAndExpectedSize.second);
+
+ Logging.d("httpupload", "Requesting upload slot for file upload");
+ Jid host = account.getXmppConnection().findDiscoItemByFeature(HttpUpload.NAMESPACE);
+ IqPacket request = HttpUploadRequestSlotPacketGenerator.generate(host, file.getName(), file.getSize(), file.getMimeType());
+ XmppSendUtil.sendIqPacket(account, request, new HttpUploadSlotRequestReceived(entity));
+ MessageUtil.markMessage(message, Message.STATUS_UNSEND);
+
+ Logging.d("httpupload", "Upload slot for file upload requested");
+ started = true;
+ } catch (FileNotFoundException e) {
+ Logging.e("httpupload", "Could not find file, exception message: " + e.getMessage());
+ }
+ return started;
+ }
+
+ /**
+ * 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 null != message
+ && null != message.getConversation()
+ && null != message.getConversation().getAccount()
+ && null != message.getFileParams()
+ && message.getConversation().getAccount().httpUploadAvailable(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
new file mode 100644
index 00000000..5a12a4d4
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadSlotRequestReceived.java
@@ -0,0 +1,36 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.upload;
+
+import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.HttpUploadSlot;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload.SlotPacketParser;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class HttpUploadSlotRequestReceived implements OnIqPacketReceived {
+ private final HttpFileTransferEntity entity;
+
+ public HttpUploadSlotRequestReceived(HttpFileTransferEntity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ try {
+ HttpUploadSlot slot = SlotPacketParser.parseGetAndPutUrl(packet);
+ this.entity.setSlot(slot);
+ if (!this.entity.isCanceled()) {
+ new Thread(new HttpFileUploader(this.entity)).start();
+ }
+ } catch (XmppException e) {
+ Logging.e("httpupload", e.getMessage());
+ this.entity.fail(FileTransferFailureReason.createNonRecoverableFailureReason(e));
+ }
+ }
+}
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
new file mode 100644
index 00000000..f084bffa
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/http/upload/HttpUploadedFileEncryptionUiCallback.java
@@ -0,0 +1,35 @@
+package de.thedevstack.conversationsplus.services.filetransfer.http.upload;
+
+import android.app.PendingIntent;
+
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferFailureReason;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.UiCallback;
+
+/**
+ *
+ */
+public class HttpUploadedFileEncryptionUiCallback implements UiCallback<Message> {
+ private final HttpFileTransferEntity entity;
+
+ public HttpUploadedFileEncryptionUiCallback(HttpFileTransferEntity entity) {
+ this.entity = entity;
+ }
+
+ @Override
+ public void success(Message message) {
+ XmppConnectionServiceAccessor.xmppConnectionService.resendMessage(message, this.entity.isDelayed());
+ }
+
+ @Override
+ public void error(int errorCode, Message object) {
+ this.entity.fail(FileTransferFailureReason.createLimitedRecoverableFailureReason("Failed to encrypt"));
+ }
+
+ @Override
+ public void userInputRequried(PendingIntent pi, Message object) {
+ this.entity.fail(FileTransferFailureReason.createLimitedRecoverableFailureReason("Failed to encrypt, user input would have been required"));
+ }
+}
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
new file mode 100644
index 00000000..e48f30e5
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/services/filetransfer/jingle/JingleFileTransferService.java
@@ -0,0 +1,53 @@
+package de.thedevstack.conversationsplus.services.filetransfer.jingle;
+
+import de.thedevstack.conversationsplus.services.FileTransferService;
+import de.thedevstack.conversationsplus.services.filetransfer.AbstractFileTransferService;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.xmpp.jingle.JingleConnection;
+import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
+
+/**
+ *
+ */
+public class JingleFileTransferService extends AbstractFileTransferService implements FileTransferService {
+ private final JingleConnectionManager jingleConnectionManager;
+
+ public JingleFileTransferService() {
+ this.jingleConnectionManager = new JingleConnectionManager();
+ }
+ /**
+ * 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) {
+ JingleConnection jingleConnection = this.jingleConnectionManager.createNewConnection(message);
+ return null != jingleConnection;
+ }
+
+ /**
+ * 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 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/dialogs/MessageDetailsDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java
index 4f6cffb4..1b44d09b 100644
--- a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/MessageDetailsDialog.java
@@ -9,6 +9,7 @@ import java.util.Date;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusColors;
+import de.thedevstack.conversationsplus.entities.FileParams;
import de.thedevstack.conversationsplus.utils.ui.TextViewUtil;
import eu.siacs.conversations.R;
@@ -60,10 +61,10 @@ public class MessageDetailsDialog extends AbstractAlertDialog {
Logging.d("messagedetailsfile", "File is stored in path: " + message.getRelativeFilePath());
view.findViewById(R.id.dlgMsgDetFileTable).setVisibility(View.VISIBLE);
if (null != message.getFileParams()) {
- Message.FileParams params = message.getFileParams();
- TextViewUtil.setText(view, R.id.dlgMsgDetFileSize, UIHelper.getHumanReadableFileSize(params.size));
+ FileParams params = message.getFileParams();
+ TextViewUtil.setText(view, R.id.dlgMsgDetFileSize, UIHelper.getHumanReadableFileSize(params.getSize()));
+ TextViewUtil.setText(view, R.id.dlgMsgDetFileMimeType, params.getMimeType());
}
- TextViewUtil.setText(view, R.id.dlgMsgDetFileMimeType, message.getMimeType());
TextViewUtil.setText(view, R.id.dlgMsgDetFileHttpUploaded, message.isHttpUploaded() ? R.string.cplus_yes : R.string.cplus_no);
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java
new file mode 100644
index 00000000..6bf9c563
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/SimpleConfirmDialog.java
@@ -0,0 +1,37 @@
+package de.thedevstack.conversationsplus.ui.dialogs;
+
+import android.content.Context;
+import android.content.DialogInterface;
+
+import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback;
+
+import eu.siacs.conversations.R;
+
+/**
+ * A dialog to give the user the choice to decide whether to do something or not.
+ * A UserDecisionListener is used to provide the functionality to be performed by clicking on yes, or no.
+ */
+public class SimpleConfirmDialog extends AbstractAlertDialog {
+ protected final SimpleUserDecisionCallback callback;
+
+ public SimpleConfirmDialog(Context context, String title, SimpleUserDecisionCallback userDecisionCallback) {
+ super(context, title);
+ this.callback = userDecisionCallback;
+ this.setPositiveButton(R.string.cplus_ok, new ConfirmOnClickListener());
+ this.setNegativeButton(R.string.cancel, null);
+ }
+
+ public SimpleConfirmDialog(Context context, int titleTextId, SimpleUserDecisionCallback userDecisionCallback) {
+ super(context, titleTextId);
+ this.callback = userDecisionCallback;
+ this.setPositiveButton(R.string.cplus_ok, new ConfirmOnClickListener());
+ this.setNegativeButton(R.string.cancel, null);
+ }
+
+ private class ConfirmOnClickListener implements DialogInterface.OnClickListener {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ callback.onYes();
+ }
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java
index c29832a5..3305e8e1 100644
--- a/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/dialogs/UserDecisionDialog.java
@@ -15,13 +15,11 @@ import eu.siacs.conversations.R;
* The user also has the choice to save his answer for the future.
* A UserDecisionListener is used to provide the functionality to be performed by clicking on yes, or no.
*/
-public class UserDecisionDialog extends AbstractAlertDialog {
- protected final UserDecisionListener listener;
+public class UserDecisionDialog extends SimpleConfirmDialog {
protected final CheckBox rememberCheckBox;
public UserDecisionDialog(Activity context, int questionResourceId, UserDecisionListener userDecisionListener) {
- super(context, questionResourceId);
- this.listener = userDecisionListener;
+ super(context, questionResourceId, userDecisionListener);
int viewId = R.layout.dialog_userdecision;
View view = context.getLayoutInflater().inflate(viewId, null);
@@ -36,10 +34,10 @@ public class UserDecisionDialog extends AbstractAlertDialog {
public void decide(UserDecision baseDecision) {
switch (baseDecision) {
case ALWAYS:
- this.listener.onYes();
+ this.callback.onYes();
break;
case NEVER:
- this.listener.onNo();
+ this.callback.onNo();
break;
case ASK:
this.show();
@@ -51,9 +49,9 @@ public class UserDecisionDialog extends AbstractAlertDialog {
@Override
public void onClick(DialogInterface dialog, int which) {
- listener.onYes();
+ callback.onYes();
if (rememberCheckBox.isChecked()) {
- listener.onRemember(UserDecision.ALWAYS);
+ ((UserDecisionListener)callback).onRemember(UserDecision.ALWAYS);
}
}
}
@@ -62,9 +60,9 @@ public class UserDecisionDialog extends AbstractAlertDialog {
@Override
public void onClick(DialogInterface dialog, int which) {
- listener.onNo();
+ callback.onNo();
if (rememberCheckBox.isChecked()) {
- listener.onRemember(UserDecision.NEVER);
+ ((UserDecisionListener)callback).onRemember(UserDecision.NEVER);
}
}
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java
new file mode 100644
index 00000000..222e473b
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/DeleteFileCallback.java
@@ -0,0 +1,45 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
+
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.persistance.FileBackend;
+
+/**
+ * Callback for the user decision if a file should be deleted or not.
+ */
+public class DeleteFileCallback implements SimpleUserDecisionCallback {
+ private final Message message;
+
+ public DeleteFileCallback(Message message) {
+ this.message = message;
+ }
+
+ /**
+ * Deletes the file and updates the UI.
+ */
+ private void deleteFile() {
+ if (FileBackend.deleteFile(this.message)) {
+ this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
+ UiUpdateHelper.updateConversationUi();
+ }
+ }
+
+ /**
+ * Deletes the file.
+ */
+ @Override
+ public void onYes() {
+ this.deleteFile();
+ }
+
+ /**
+ * Nothing to do in this case.
+ */
+ @Override
+ public void onNo() {
+ // Nothing to do
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java
index 0cfee1d8..dec6b885 100644
--- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/ResizePictureUserDecisionListener.java
@@ -13,6 +13,8 @@ import java.io.InputStream;
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.enums.UserDecision;
import de.thedevstack.conversationsplus.exceptions.UiException;
import de.thedevstack.conversationsplus.utils.ImageUtil;
@@ -97,20 +99,18 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener {
@Override
public void onYes() {
this.showPrepareFileToast();
- final Message message;
- if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
- message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
- } else {
- message = new Message(conversation, "", conversation.getNextEncryption());
- }
- message.setCounterpart(conversation.getNextCounterpart());
- message.setType(Message.TYPE_IMAGE);
+ final Message message = createMessage();
ConversationsPlusApplication.executeFileAdding(new OnYesRunnable(message, uri));
}
@Override
public void onNo() {
this.showPrepareFileToast();
+ final Message message = createMessage();
+ ConversationsPlusApplication.executeFileAdding(new OnNoRunnable(message, uri));
+ }
+
+ protected Message createMessage() {
final Message message;
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
@@ -118,8 +118,11 @@ public class ResizePictureUserDecisionListener implements UserDecisionListener {
message = new Message(conversation, "", conversation.getNextEncryption());
}
message.setCounterpart(conversation.getNextCounterpart());
- message.setType(Message.TYPE_IMAGE);
- ConversationsPlusApplication.executeFileAdding(new OnNoRunnable(message, uri));
+ //message.setType(Message.TYPE_IMAGE);
+ message.setFileParams(new FileParams());
+ message.getFileParams().setFileStatus(FileStatus.NEEDS_UPLOAD);
+
+ return message;
}
@Override
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java
new file mode 100644
index 00000000..3ed49c2d
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/SimpleUserDecisionCallback.java
@@ -0,0 +1,9 @@
+package de.thedevstack.conversationsplus.ui.listeners;
+
+/**
+ * Callback to be executed on a user decision.
+ */
+public interface SimpleUserDecisionCallback {
+ void onYes();
+ void onNo();
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java
index fbee6290..45576225 100644
--- a/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java
+++ b/src/main/java/de/thedevstack/conversationsplus/ui/listeners/UserDecisionListener.java
@@ -3,10 +3,8 @@ package de.thedevstack.conversationsplus.ui.listeners;
import de.thedevstack.conversationsplus.enums.UserDecision;
/**
- * Created by tzur on 31.10.2015.
+ * Callback to be executed on a user decision with the possibility to remember the decision.
*/
-public interface UserDecisionListener {
- void onYes();
- void onNo();
+public interface UserDecisionListener extends SimpleUserDecisionCallback {
void onRemember(UserDecision decision);
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java
index 958e9de8..25c8b3ba 100644
--- a/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/ConversationUtil.java
@@ -3,6 +3,7 @@ package de.thedevstack.conversationsplus.utils;
import android.net.Uri;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.enums.FileStatus;
import de.thedevstack.conversationsplus.exceptions.FileCopyException;
import eu.siacs.conversations.crypto.PgpEngine;
@@ -45,10 +46,12 @@ public class ConversationUtil {
message = new Message(conversation, "", conversation.getNextEncryption());
}
message.setCounterpart(conversation.getNextCounterpart());
- message.setType(Message.TYPE_FILE);
+ //message.setType(Message.TYPE_FILE);
+ message.getFileParams().setFileStatus(FileStatus.NEEDS_UPLOAD);
String path = FileUtils.getPath(uri);
if (path != null) {
- message.setRelativeFilePath(path);
+ message.getFileParams().setPath(path);
+ message.setRelativeFilePath(path); // TODO: Remove when everything is moved to fileparams
MessageUtil.updateFileParams(message);
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
PgpEngine.getInstance().encrypt(message, callback);
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java
index b28e6f1c..0edf6ad0 100644
--- a/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/ImageUtil.java
@@ -338,20 +338,28 @@ public final class ImageUtil {
public static int calcSampleSize(Uri image, int size) throws FileNotFoundException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
- BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options);
- return calcSampleSize(options, size);
+ Bitmap bmp = BitmapFactory.decodeStream(StreamUtil.openInputStreamFromContentResolver(image), null, options);
+ int height = options.outHeight;
+ int width = options.outWidth;
+ if (null != bmp) {
+ bmp.recycle();
+ }
+ return calcSampleSize(width, height, size);
}
public static int calcSampleSize(File image, int size) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(image.getAbsolutePath(), options);
- return calcSampleSize(options, size);
- }
-
- public static int calcSampleSize(BitmapFactory.Options options, int size) {
+ Bitmap bmp = BitmapFactory.decodeFile(image.getAbsolutePath(), options);
int height = options.outHeight;
int width = options.outWidth;
+ if (null != bmp) {
+ bmp.recycle();
+ }
+ return calcSampleSize(width, height, size);
+ }
+
+ private static int calcSampleSize(int width, int height, int size) {
int inSampleSize = 1;
if (height > size || width > size) {
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java
index ca24bd1d..37e39285 100644
--- a/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/MessageUtil.java
@@ -1,5 +1,6 @@
package de.thedevstack.conversationsplus.utils;
+import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import java.net.URL;
@@ -7,6 +8,8 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -19,6 +22,29 @@ import eu.siacs.conversations.persistance.FileBackend;
*/
public final class MessageUtil {
+ public static boolean needsDownload(Message message) {
+ FileStatus fileStatus = (null != message.getFileParams()) ? message.getFileParams().getFileStatus() : null;
+ return (null != fileStatus && (fileStatus == FileStatus.NEEDS_DOWNLOAD
+ || fileStatus == FileStatus.UNDEFINED))
+ && message.treatAsDownloadable() != Message.Decision.NEVER;
+ }
+
+ public static boolean isMessageSent(Message message) {
+ switch (message.getStatus()) {
+ case Message.STATUS_SEND:
+ case Message.STATUS_SEND_DISPLAYED:
+ case Message.STATUS_SEND_RECEIVED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static void setAndSaveFileStatus(Message message, FileStatus fileStatus) {
+ message.getFileParams().setFileStatus(fileStatus);
+ DatabaseBackend.getInstance().updateMessage(message);
+ UiUpdateHelper.updateConversationUi();
+ }
public static boolean markMessage(Conversation conversation, String uuid, int status) {
if (uuid == null) {
@@ -67,7 +93,7 @@ public final class MessageUtil {
public static void updateMessageWithImageDetails(Message message, String filePath, long size, int imageWidth, int imageHeight) {
message.setRelativeFilePath(filePath);
- MessageUtil.updateMessageBodyWithImageParams(message, size, imageWidth, imageHeight);
+ MessageUtil.updateMessageWithFileParams(message, null, size, imageWidth, imageHeight);
}
public static void updateFileParams(Message message) {
@@ -81,42 +107,37 @@ public final class MessageUtil {
if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+ Bitmap bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
imageHeight = options.outHeight;
imageWidth = options.outWidth;
+ if (null != bmp) {
+ bmp.recycle();
+ }
}
- MessageUtil.updateMessageBodyWithFileParams(message, url, file.getSize(), imageWidth, imageHeight);
- }
-
- private static void updateMessageBodyWithFileParams(Message message, URL url, long fileSize, int imageWidth, int imageHeight) {
- message.setBody(MessageUtil.getMessageBodyWithImageParams(url, fileSize, imageWidth, imageHeight));
- }
-
- private static void updateMessageBodyWithImageParams(Message message, long size, int imageWidth, int imageHeight) {
- MessageUtil.updateMessageBodyWithImageParams(message, null, size, imageWidth, imageHeight);
+ MessageUtil.updateMessageWithFileParams(message, url, file.getSize(), imageWidth, imageHeight);
}
- private static void updateMessageBodyWithImageParams(Message message, URL url, long size, int imageWidth, int imageHeight) {
- message.setBody(MessageUtil.getMessageBodyWithImageParams(url, size, imageWidth, imageHeight));
- }
-
- private static String getMessageBodyWithImageParams(URL url, long size, int imageWidth, int imageHeight) {
- StringBuilder sb = new StringBuilder();
+ private static void updateMessageWithFileParams(Message message, URL url, long size, int imageWidth, int imageHeight) {
+ FileParams fileParams = message.getFileParams();
+ if (null == fileParams) {
+ fileParams = new FileParams();
+ }
+ fileParams.setSize(size);
if (null != url) {
- sb.append(url.toString());
- sb.append('|');
+ fileParams.setUrl(url.toString());
}
- sb.append(size);
if (-1 < imageWidth) {
- sb.append('|');
- sb.append(imageWidth);
+ fileParams.setWidth(imageWidth);
}
if (-1 < imageHeight) {
- sb.append('|');
- sb.append(imageHeight);
+ fileParams.setHeight(imageHeight);
+ }
+ String relativeFilePathFromMessage = message.getRelativeFilePath();
+ if (null != relativeFilePathFromMessage && relativeFilePathFromMessage.startsWith("/")) {
+ fileParams.setPath(relativeFilePathFromMessage);
}
- return sb.toString();
+ message.setFileParams(fileParams);
}
private MessageUtil() {
diff --git a/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java b/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java
new file mode 100644
index 00000000..0a8c80d1
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/utils/SimpleCryptoUtil.java
@@ -0,0 +1,110 @@
+package de.thedevstack.conversationsplus.utils;
+
+
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ *
+ */
+public final class SimpleCryptoUtil {
+
+ public static String encrypt(String seed, String cleartext) {
+ String result = null;
+ if (null != seed && null != cleartext) {
+ try {
+ byte[] rawKey = getRawKey(seed.getBytes());
+ byte[] encryptedBytes = encrypt(rawKey, cleartext.getBytes());
+ result = toHex(encryptedBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) {
+ // FIXME
+ }
+ }
+
+ return result;
+ }
+
+ public static String decrypt(String seed, String encrypted) {
+ String result = null;
+ if (null != seed && null != encrypted) {
+ try {
+ byte[] rawKey = getRawKey(seed.getBytes());
+ byte[] enc = toByte(encrypted);
+ byte[] decryptedBytes = decrypt(rawKey, enc);
+ result = new String(decryptedBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException e) {
+ // FIXME
+ }
+ }
+
+ return result;
+ }
+
+ private static byte[] encrypt(byte[] raw, byte[] clear) throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
+ byte[] encrypted = doCipherOperation(raw, clear, Cipher.ENCRYPT_MODE);
+ return encrypted;
+ }
+
+ private static byte[] decrypt(byte[] raw, byte[] encrypted) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+ byte[] decrypted = doCipherOperation(raw, encrypted, Cipher.DECRYPT_MODE);
+ return decrypted;
+ }
+
+ private static byte[] doCipherOperation(byte[] raw, byte[] input, int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+ SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
+ Cipher cipher = Cipher.getInstance("AES");
+ cipher.init(mode, skeySpec);
+
+ return cipher.doFinal(input);
+ }
+
+ private static byte[] getRawKey(byte[] seed) throws NoSuchAlgorithmException {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] md5Bytes = md.digest(seed); // 128 Bit = 16 byte
+ SecretKey skey = new SecretKeySpec(md5Bytes, "AES");
+ byte[] raw = skey.getEncoded();
+ return raw;
+ }
+
+ public static byte[] toByte(String hexString) {
+ int len = hexString.length() / 2;
+ byte[] result = new byte[len];
+ for (int i = 0; i < len; i++) {
+ result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue();
+ }
+ return result;
+ }
+
+ public static String toHex(byte[] buf) {
+ if (buf == null) {
+ return null;
+ }
+ StringBuffer result = new StringBuffer(2 * buf.length);
+ for (int i = 0; i < buf.length; i++) {
+ appendHex(result, buf[i]);
+ }
+ return result.toString();
+ }
+
+ private final static String HEX = "0123456789ABCDEF";
+
+ private static void appendHex(StringBuffer sb, byte b) {
+ sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f));
+ }
+
+ /**
+ * private constructor to avoid instantiation
+ */
+ private SimpleCryptoUtil() {
+ // private constructor to avoid instantiation
+ }
+}
+
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java
new file mode 100644
index 00000000..eee5b0aa
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/IqPacketParser.java
@@ -0,0 +1,52 @@
+package de.thedevstack.conversationsplus.xmpp;
+
+import de.thedevstack.conversationsplus.xmpp.exceptions.MissingRequiredContentException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.MissingRequiredElementException;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ *
+ */
+public abstract class IqPacketParser {
+ public static Element findRequiredChild(Element element, String elementName, String namespace) throws MissingRequiredElementException {
+ Element child = findChild(element, elementName, namespace);
+ if (child == null) {
+ throw new MissingRequiredElementException(elementName, namespace, element);
+ }
+ return child;
+ }
+
+ public static Element findChild(Element element, String elementName, String namespace) {
+ if (null == element) {
+ return null;
+ }
+ return element.findChild(elementName, namespace);
+ }
+
+ public static String findRequiredChildContent(Element element, String elementName) throws MissingRequiredContentException {
+ if (null == element) {
+ return null;
+ }
+ String childContent = element.findChildContent(elementName);
+ if (null == childContent) {
+ throw new MissingRequiredContentException(elementName, element);
+ }
+ return childContent;
+ }
+
+ public static String findRequiredChildContent(Element element, String elementName, String namespace) throws MissingRequiredContentException {
+ String childContent = findChildContent(element, elementName, namespace);
+ if (null == childContent) {
+ throw new MissingRequiredContentException(elementName, namespace, element);
+ }
+ return childContent;
+ }
+
+ public static String findChildContent(Element element, String elementName, String namespace) {
+ if (null == element) {
+ return null;
+ }
+ return element.findChildContent(elementName, namespace);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java
new file mode 100644
index 00000000..120ef495
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/BadRequestIqErrorException.java
@@ -0,0 +1,17 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public class BadRequestIqErrorException extends IqPacketErrorException {
+ public BadRequestIqErrorException(Element context, String errorMessage) {
+ super(context, errorMessage);
+ }
+
+ @Override
+ public String getMessage() {
+ return "Bad Request: " + super.getMessage();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java
new file mode 100644
index 00000000..1a1cded2
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/InternalServerErrorException.java
@@ -0,0 +1,17 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public class InternalServerErrorException extends IqPacketErrorException {
+ public InternalServerErrorException(Element packet, String errorText) {
+ super(packet, errorText);
+ }
+
+ @Override
+ public String getMessage() {
+ return "Internal Server Error: " + super.getMessage();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java
new file mode 100644
index 00000000..65e02688
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/IqPacketErrorException.java
@@ -0,0 +1,20 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public class IqPacketErrorException extends XmppException {
+ private final String iqErrorText;
+
+ public IqPacketErrorException(Element context, String errorMessage) {
+ super(context);
+ this.iqErrorText = errorMessage;
+ }
+
+ @Override
+ public String getMessage() {
+ return this.iqErrorText + " in context: " + this.getContext();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java
new file mode 100644
index 00000000..060bb618
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredContentException.java
@@ -0,0 +1,26 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ *
+ */
+public class MissingRequiredContentException extends XmppException {
+ private String elementName;
+ private String namespace;
+
+ public MissingRequiredContentException(String elementName, Element context) {
+ super(context);
+ this.elementName = elementName;
+ }
+
+ public MissingRequiredContentException(String elementName, String namespace, Element context) {
+ this(elementName, context);
+ this.namespace = namespace;
+ }
+
+ @Override
+ public String getMessage() {
+ return "Missing required element content " + ((namespace != null) ? namespace + ":" : "") + elementName + " in context " + getContext();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java
new file mode 100644
index 00000000..de24262f
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/MissingRequiredElementException.java
@@ -0,0 +1,26 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ *
+ */
+public class MissingRequiredElementException extends XmppException {
+ private String elementName;
+ private String namespace;
+
+ public MissingRequiredElementException(String elementName, Element context) {
+ super(context);
+ this.elementName = elementName;
+ }
+
+ public MissingRequiredElementException(String elementName, String namespace, Element context) {
+ this(elementName, context);
+ this.namespace = namespace;
+ }
+
+ @Override
+ public String getMessage() {
+ return "Missing required element " + ((namespace != null) ? namespace + ":" : "") + elementName + " in context " + getContext();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java
new file mode 100644
index 00000000..88d5389b
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/ServiceUnavailableException.java
@@ -0,0 +1,17 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public class ServiceUnavailableException extends IqPacketErrorException {
+ public ServiceUnavailableException(Element context, String errorMessage) {
+ super(context, errorMessage);
+ }
+
+ @Override
+ public String getMessage() {
+ return "Service unavailable: " + super.getMessage();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java
new file mode 100644
index 00000000..de821873
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UndefinedConditionException.java
@@ -0,0 +1,17 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public class UndefinedConditionException extends IqPacketErrorException {
+ public UndefinedConditionException(Element packet, String errorText) {
+ super(packet, errorText);
+ }
+
+ @Override
+ public String getMessage() {
+ return "Undefined Condition: " + super.getMessage();
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java
new file mode 100644
index 00000000..82f229e8
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/UnexpectedIqPacketTypeException.java
@@ -0,0 +1,25 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import java.util.Arrays;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class UnexpectedIqPacketTypeException extends XmppException {
+ private final IqPacket.TYPE current;
+ private final IqPacket.TYPE[] expected;
+
+ public UnexpectedIqPacketTypeException(Element context, IqPacket.TYPE current, IqPacket.TYPE... expected) {
+ super(context);
+ this.expected = expected;
+ this.current = current;
+ }
+
+ @Override
+ public String getMessage() {
+ return "Unexpected IQ packet type '" + this.current + "' retrieved. One of " + Arrays.toString(expected) + " was expected.";
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java
new file mode 100644
index 00000000..b8c6c89d
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/exceptions/XmppException.java
@@ -0,0 +1,56 @@
+package de.thedevstack.conversationsplus.xmpp.exceptions;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ *
+ */
+public class XmppException extends Exception {
+ private Element context;
+ /**
+ * Constructs a new {@code Exception} that includes the current stack trace.
+ */
+ public XmppException() {
+ }
+
+ /**
+ * Constructs a new {@code Exception} that includes the current stack trace.
+ */
+ public XmppException(Element context) {
+ this.context = context;
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace and the
+ * specified cause.
+ *
+ * @param throwable the cause of this exception.
+ */
+ public XmppException(Throwable throwable) {
+ super(throwable);
+ }
+
+ /**
+ * Constructs a new {@code Exception} with the current stack trace and the
+ * specified cause.
+ *
+ * @param throwable the cause of this exception.
+ */
+ public XmppException(Element context, Throwable throwable) {
+ super(throwable);
+ this.context = context;
+ }
+
+ @Override
+ public String getMessage() {
+ if (null != context) {
+ return "Error in XMPP Element. XML element is: " + this.context.toString();
+ } else {
+ return super.getMessage();
+ }
+ }
+
+ public Element getContext() {
+ return context;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java
new file mode 100644
index 00000000..28f4f870
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/FileTransferHttp.java
@@ -0,0 +1,8 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http;
+
+/**
+ * Created by steckbrief on 21.08.2016.
+ */
+public interface FileTransferHttp {
+ String NAMESPACE = "urn:xmpp:filetransfer:http";
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java
new file mode 100644
index 00000000..a6408e9f
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotPacketParser.java
@@ -0,0 +1,30 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete;
+
+import de.thedevstack.conversationsplus.xmpp.IqPacketParser;
+import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException;
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp;
+import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by steckbrief on 21.08.2016.
+ */
+public class DeleteSlotPacketParser extends IqPacketParser {
+ public static String parseDeleteToken(IqPacket packet) throws XmppException {
+ String deletetoken = null;
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element slot = findRequiredChild(packet, "slot", FileTransferHttp.NAMESPACE);
+
+ deletetoken = findRequiredChildContent(slot, "deletetoken");
+ } else if (packet.getType() == IqPacket.TYPE.ERROR) {
+ ErrorIqPacketExceptionHelper.throwIqErrorException(packet);
+ } else {
+ throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR);
+ }
+
+ return deletetoken;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java
new file mode 100644
index 00000000..67deeb6f
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/DeleteSlotRequestPacket.java
@@ -0,0 +1,35 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete;
+
+import de.thedevstack.conversationsplus.xmpp.filetransfer.http.FileTransferHttp;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by steckbrief on 21.08.2016.
+ */
+public class DeleteSlotRequestPacket extends IqPacket {
+ public static final String ELEMENT_NAME = "request";
+ public static final String FILEURL_ELEMENT_NAME = "fileurl";
+ private Element requestElement;
+ private String fileurl;
+
+ private DeleteSlotRequestPacket() {
+ super(TYPE.GET);
+ this.requestElement = super.addChild(DeleteSlotRequestPacket.ELEMENT_NAME, FileTransferHttp.NAMESPACE);
+ this.requestElement.setAttribute("type", "delete");
+ }
+
+ public DeleteSlotRequestPacket(String fileurl) {
+ this();
+ this.setFileURL(fileurl);
+ }
+
+ public void setFileURL(String fileurl) {
+ if (null == fileurl || fileurl.isEmpty()) {
+ throw new IllegalArgumentException("fileurl must not be null or empty.");
+ }
+ this.fileurl = fileurl;
+ this.requestElement.addChild(FILEURL_ELEMENT_NAME).setContent(fileurl);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java
new file mode 100644
index 00000000..fa0f7a34
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/delete/FileTransferHttpDeleteSlotRequestPacketGenerator.java
@@ -0,0 +1,39 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.delete;
+
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Created by steckbrief on 21.08.2016.
+ */
+public final class FileTransferHttpDeleteSlotRequestPacketGenerator {
+ /**
+ * Generates the IqPacket to request a slot to delete a file previously uploaded via http upload.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq from='romeo@montague.tld/garden'
+ * id='step_01'
+ * to='upload.montague.tld'
+ * type='get'>
+ * <request type='delete' xmlns='urn:xmpp:filetransfer:http'>
+ * <fileurl>http://upload.montague.tld/files/1e56ee17-ee4c-4a9c-aedd-cb09cb3984a7/my_juliet.png</fileurl>
+ * </request>
+ * </iq>
+ * </pre>
+ * @param host the jid of the host to request a slot from
+ * @param fileurl the URL of the file to be deleted
+ * @return the IqPacket
+ */
+ public static IqPacket generate(Jid host, String fileurl) {
+ DeleteSlotRequestPacket packet = new DeleteSlotRequestPacket(fileurl);
+ packet.setTo(host);
+ return packet;
+ }
+
+ /**
+ * Utility class - avoid instantiation
+ */
+ private FileTransferHttpDeleteSlotRequestPacketGenerator() {
+ // Helper class - avoid instantiation
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java
new file mode 100644
index 00000000..3bcbd219
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUpload.java
@@ -0,0 +1,9 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload;
+
+/**
+ *
+ */
+public interface HttpUpload {
+ String NAMESPACE = "urn:xmpp:http:upload";
+ String DEFAULT_MIME_TYPE = "application/octet-stream";
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java
new file mode 100644
index 00000000..915cf9a7
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadRequestSlotPacketGenerator.java
@@ -0,0 +1,46 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload;
+
+import eu.siacs.conversations.xmpp.jid.Jid;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ * Generates the IQ Packets for requesting a http upload slot
+ * as defined in XEP-0363.
+ * @see <a href="http://xmpp.org/extensions/xep-0363.html">http://xmpp.org/extensions/xep-0363.html</a>
+ */
+public final class HttpUploadRequestSlotPacketGenerator {
+ /**
+ * Generates the IqPacket to request a http upload slot.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq from='romeo@montague.tld/garden'
+ * id='step_03'
+ * to='upload.montague.tld'
+ * type='get'>
+ * <request xmlns='urn:xmpp:http:upload'>
+ * <filename>my_juliet.png</filename>
+ * <size>23456</size>
+ * <content-type>image/jpeg</content-type>
+ * </request>
+ * </iq>
+ * </pre>
+ * @param host the jid of the host to request a slot from
+ * @param filename the filename of the file which will be transferred
+ * @param filesize the filesize of the file which will be transferred
+ * @param mime the mime type of the file which will be transferred - <code>optional</code> and therefore nullable
+ * @return the IqPacket
+ */
+ public static IqPacket generate(Jid host, String filename, long filesize, String mime) {
+ SlotRequestPacket packet = new SlotRequestPacket(filename, filesize);
+ packet.setTo(host);
+ packet.setMime((mime == null) ? HttpUpload.DEFAULT_MIME_TYPE : mime);
+ return packet;
+ }
+
+ /**
+ * Utility class - avoid instantiation
+ */
+ private HttpUploadRequestSlotPacketGenerator() {
+ // Helper class - avoid instantiation
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java
new file mode 100644
index 00000000..1e320afe
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/HttpUploadSlot.java
@@ -0,0 +1,22 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload;
+
+/**
+ *
+ */
+public class HttpUploadSlot {
+ private final String getUrl;
+ private final String putUrl;
+
+ public HttpUploadSlot(String getUrl, String putUrl) {
+ this.getUrl = getUrl;
+ this.putUrl = putUrl;
+ }
+
+ public String getGetUrl() {
+ return this.getUrl;
+ }
+
+ public String getPutUrl() {
+ return this.putUrl;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java
new file mode 100644
index 00000000..40e7db96
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotPacketParser.java
@@ -0,0 +1,31 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload;
+
+import de.thedevstack.conversationsplus.xmpp.IqPacketParser;
+import de.thedevstack.conversationsplus.xmpp.exceptions.UnexpectedIqPacketTypeException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.XmppException;
+import de.thedevstack.conversationsplus.xmpp.utils.ErrorIqPacketExceptionHelper;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public final class SlotPacketParser extends IqPacketParser {
+ public static HttpUploadSlot parseGetAndPutUrl(IqPacket packet) throws XmppException {
+ HttpUploadSlot httpUploadSlot = null;
+ if (packet.getType() == IqPacket.TYPE.RESULT) {
+ Element slot = findRequiredChild(packet, "slot", HttpUpload.NAMESPACE);
+
+ String getUrl = findRequiredChildContent(slot, "get");
+ String putUrl = findRequiredChildContent(slot, "put");
+
+ httpUploadSlot = new HttpUploadSlot(getUrl, putUrl);
+ } else if (packet.getType() == IqPacket.TYPE.ERROR) {
+ ErrorIqPacketExceptionHelper.throwIqErrorException(packet);
+ } else {
+ throw new UnexpectedIqPacketTypeException(packet, packet.getType(), IqPacket.TYPE.RESULT, IqPacket.TYPE.ERROR);
+ }
+ return httpUploadSlot;
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java
new file mode 100644
index 00000000..30c5db79
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/filetransfer/http/upload/SlotRequestPacket.java
@@ -0,0 +1,53 @@
+package de.thedevstack.conversationsplus.xmpp.filetransfer.http.upload;
+
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
+
+/**
+ *
+ */
+public class SlotRequestPacket extends IqPacket {
+ public static final String ELEMENT_NAME = "request";
+ public static final String FILENAME_ELEMENT_NAME = "filename";
+ public static final String FILESIZE_ELEMENT_NAME = "size";
+ public static final String MIME_ELEMENT_NAME = "content-type";
+ private Element requestElement;
+ private String filename;
+ private long filesize;
+ private String mime;
+
+ private SlotRequestPacket() {
+ super(TYPE.GET);
+ this.requestElement = super.addChild(SlotRequestPacket.ELEMENT_NAME, HttpUpload.NAMESPACE);
+ }
+
+ public SlotRequestPacket(String filename, long filesize) {
+ this();
+ this.setFilename(filename);
+ this.setFilesize(filesize);
+ }
+
+ public void setFilename(String filename) {
+ if (null == filename || filename.isEmpty()) {
+ throw new IllegalArgumentException("filename must not be null or empty.");
+ }
+ this.filename = filename;
+ this.requestElement.addChild(FILENAME_ELEMENT_NAME).setContent(filename);
+ }
+
+ public void setFilesize(long filesize) {
+ if (0 >= filesize) {
+ throw new IllegalArgumentException("filesize must not be null or empty.");
+ }
+ this.filesize = filesize;
+ this.requestElement.addChild(FILESIZE_ELEMENT_NAME).setContent(String.valueOf(filesize));
+ }
+
+ public void setMime(String mime) {
+ if (null == mime || mime.isEmpty()) {
+ throw new IllegalArgumentException("mime type must not be null or empty.");
+ }
+ this.mime = mime;
+ this.requestElement.addChild(MIME_ELEMENT_NAME).setContent(mime);
+ }
+}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java
index 7868a2f5..00bd1158 100644
--- a/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/httpuploadim/HttpUploadHint.java
@@ -3,10 +3,20 @@ package de.thedevstack.conversationsplus.xmpp.httpuploadim;
import eu.siacs.conversations.xml.Element;
/**
- * Created by steckbrief on 17.04.2016.
+ * Representation of the HttpUploadHint.
+ * <pre>
+ * <httpupload xmlns="urn:xmpp:hints"/>
+ * </pre>
*/
public class HttpUploadHint extends Element {
+ /**
+ * The namespace of message processing hints as defined in XEP-0334.
+ * @see <a href="http://xmpp.org/extensions/xep-0334.html">http://xmpp.org/extensions/xep-0334.html</a>
+ */
public static final String NAMESPACE = "urn:xmpp:hints";
+ /**
+ * The element name of the hint for an http upload.
+ */
public static final String ELEMENT_NAME = "httpupload";
public HttpUploadHint() {
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java
index 961277cb..31186191 100644
--- a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacket.java
@@ -4,28 +4,69 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
/**
- * Created by tzur on 15.01.2016.
+ * Representation of a PubSub IQ packet as defined in XEP-0060.
+ * <br>One example:
+ * <pre>
+ * <iq type='get'
+ * from='romeo@montague.lit'
+ * to='pubsub.shakespeare.lit'
+ * id='items1'>
+ * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ * <items node='urn:xmpp:pubsub:subscription'/>
+ * </pubsub>
+ * </iq>
+ * </pre>
+ * @see <a href="http://xmpp.org/extensions/xep-0330.html">http://xmpp.org/extensions/xep-0060.html</a>
*/
public class PubSubPacket extends IqPacket {
+ /**
+ * The namespace of pubsub.
+ */
public static final String NAMESPACE = "http://jabber.org/protocol/pubsub";
+ /**
+ * The name of the root element.
+ */
public static final String ELEMENT_NAME = "pubsub";
+ /**
+ * The PubSub element - everything which is added to this packet is a child of this element.
+ */
private Element pubSubElement;
+ /**
+ * Instantiate the PubSubPacket for the given type.
+ * @param type the IqPacket.TYPE
+ */
public PubSubPacket(IqPacket.TYPE type) {
super(type);
this.pubSubElement = super.addChild(PubSubPacket.ELEMENT_NAME, PubSubPacket.NAMESPACE);
}
+ /**
+ * Adds an element to the PubSub element instead of the IqPacket.
+ * @param child the children to be added
+ * @return the added children
+ */
@Override
public Element addChild(Element child) {
return this.pubSubElement.addChild(child);
}
+ /**
+ * Adds an element to the PubSub element instead of the IqPacket.
+ * @param name name of the children to be added
+ * @return the added children
+ */
@Override
public Element addChild(String name) {
return this.pubSubElement.addChild(name);
}
+ /**
+ * Adds an element to the PubSub element instead of the IqPacket.
+ * @param name name of the children to be added
+ * @param xmlns namespace of the children to be added
+ * @return the added children
+ */
@Override
public Element addChild(String name, String xmlns) {
return this.pubSubElement.addChild(name, xmlns);
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java
index 398ec032..f72bf777 100644
--- a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketGenerator.java
@@ -4,10 +4,32 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
/**
- * Created by tzur on 15.01.2016.
+ * Generates the IQ packets for Pubsub Subscription as defined in XEP-0060.
+ * @see <a href="http://xmpp.org/extensions/xep-0060.html">http://xmpp.org/extensions/xep-0060.html</a>
*/
public final class PubSubPacketGenerator {
+ /**
+ * Generates a pubsub publish packet.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq type='set'
+ * from='hamlet@denmark.lit/blogbot'
+ * to='pubsub.shakespeare.lit'
+ * id='publish1'>
+ * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ * <publish node='princely_musings'>
+ * <item id='bnd81g37d61f49fgn581'>
+ * ...
+ * </item>
+ * </publish>
+ * </pubsub>
+ * </iq>
+ * </pre>
+ * @param nodeName the name of the publish node
+ * @param item the item element
+ * @return the generated PubSubPacket
+ */
public static PubSubPacket generatePubSubPublishPacket(String nodeName, Element item) {
final PubSubPacket pubsub = new PubSubPacket(IqPacket.TYPE.SET);
final Element publish = pubsub.addChild("publish");
@@ -16,6 +38,25 @@ public final class PubSubPacketGenerator {
return pubsub;
}
+ /**
+ * Generates a pubsub retrieve packet.
+ * The attributes from and id are not set in here - this is added while sending the packet.
+ * <pre>
+ * <iq type='get'
+ * from='romeo@montague.lit/home'
+ * to='juliet@capulet.lit'
+ * id='retrieve1'>
+ * <pubsub xmlns='http://jabber.org/protocol/pubsub'>
+ * <items node='urn:xmpp:avatar:data'>
+ * <item id='111f4b3c50d7b0df729d299bc6f8e9ef9066971f'/>
+ * </items>
+ * </pubsub>
+ * </iq>
+ * </pre>
+ * @param nodeName
+ * @param item
+ * @return
+ */
public static PubSubPacket generatePubSubRetrievePacket(String nodeName, Element item) {
final PubSubPacket pubsub = new PubSubPacket(IqPacket.TYPE.GET);
final Element items = pubsub.addChild("items");
@@ -26,6 +67,9 @@ public final class PubSubPacketGenerator {
return pubsub;
}
+ /**
+ * Utility class - avoid instantiation
+ */
private PubSubPacketGenerator() {
// Avoid instantiation
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java
index 394fb5b2..c8df0d5e 100644
--- a/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/pubsub/PubSubPacketParser.java
@@ -4,16 +4,28 @@ import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
/**
- * Created by tzur on 15.01.2016.
+ * Parses the IQ Packets for handling pubsub
+ * as defined in XEP-0060.
+ * @see <a href="http://xmpp.org/extensions/xep-0060.html">http://xmpp.org/extensions/xep-0060.html</a>
*/
-public class PubSubPacketParser {
+public final class PubSubPacketParser {
+ /**
+ * Finds the pubsub element within an IQ packet.
+ * @param packet the retrieved IQ packet
+ * @return the {@value PubSubPacket#ELEMENT_NAME} Element or <code>null</code> if the IqPacket is null or the IQ packet does not contain an pubsub element.
+ */
public static Element findPubSubPacket(IqPacket packet){
if (null == packet) {
return null;
}
- return packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
+ return packet.findChild(PubSubPacket.ELEMENT_NAME, PubSubPacket.NAMESPACE);
}
+ /**
+ * Finds the "items" element within an pubSubPacket element.
+ * @param pubSubPacket the pubSubPacket element
+ * @return the items Element or <code>null</code> if the pubSubPacket is null or the pubSubPacket does not contain an items element.
+ */
public static Element findItemsFromPubSubElement(Element pubSubPacket) {
if (null == pubSubPacket) {
return null;
@@ -21,7 +33,19 @@ public class PubSubPacketParser {
return pubSubPacket.findChild("items");
}
+ /**
+ * Finds the "items" element within an pubSubPacket element.
+ * @param packet the IqPacket element
+ * @return the items Element or <code>null</code> if the IqPacket is null or the IQ packet does not contain an pubsub element with an items element.
+ */
public static Element findItems(IqPacket packet) {
return PubSubPacketParser.findItemsFromPubSubElement(PubSubPacketParser.findPubSubPacket(packet));
}
+
+ /**
+ * Utility class - avoid instantiation
+ */
+ private PubSubPacketParser() {
+ // Avoid instantiation
+ }
}
diff --git a/src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java b/src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java
new file mode 100644
index 00000000..15771248
--- /dev/null
+++ b/src/main/java/de/thedevstack/conversationsplus/xmpp/utils/ErrorIqPacketExceptionHelper.java
@@ -0,0 +1,45 @@
+package de.thedevstack.conversationsplus.xmpp.utils;
+
+import de.thedevstack.conversationsplus.xmpp.IqPacketParser;
+import de.thedevstack.conversationsplus.xmpp.exceptions.BadRequestIqErrorException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.InternalServerErrorException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.IqPacketErrorException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.ServiceUnavailableException;
+import de.thedevstack.conversationsplus.xmpp.exceptions.UndefinedConditionException;
+
+import eu.siacs.conversations.xml.Element;
+
+/**
+ * Created by steckbrief on 22.08.2016.
+ */
+public final class ErrorIqPacketExceptionHelper {
+ private final static String ERROR_NAMESPACE = "urn:ietf:params:xml:ns:xmpp-stanzas";
+
+ public static void throwIqErrorException(Element packet) throws IqPacketErrorException {
+ if (hasErrorElement(packet, "bad-request")) {
+ throw new BadRequestIqErrorException(packet, getErrorText(packet));
+ }
+ if (hasErrorElement(packet, "service-unavailable")) {
+ throw new ServiceUnavailableException(packet, getErrorText(packet));
+ }
+ if (hasErrorElement(packet, "internal-server-error")) {
+ throw new InternalServerErrorException(packet, getErrorText(packet));
+ }
+ if (hasErrorElement(packet, "undefined-condition")) {
+ throw new UndefinedConditionException(packet, getErrorText(packet));
+ }
+ throw new IqPacketErrorException(packet, "Unknown error packet.");
+ }
+
+ private static boolean hasErrorElement(Element packet, String elementName) {
+ return null != IqPacketParser.findChild(packet, elementName, ERROR_NAMESPACE);
+ }
+
+ private static String getErrorText(Element packet) {
+ return IqPacketParser.findChildContent(packet, "text", ERROR_NAMESPACE);
+ }
+
+ private ErrorIqPacketExceptionHelper() {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
index 92eb158f..4ad01089 100644
--- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
+++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java
@@ -14,10 +14,8 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.URL;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
-import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.utils.StreamUtil;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -65,12 +63,11 @@ public class PgpEngine {
&& message.getUuid().equals(uuid)) {
message.setBody(os.toString());
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
- final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
if (message.trusted()
&& message.treatAsDownloadable() != Message.Decision.NEVER
&& (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())
&& ConversationsPlusPreferences.autoAcceptFileSize() > 0) {
- manager.createNewDownloadConnection(message);
+ HttpConnectionManager.createNewDownloadConnection(message);
}
mXmppConnectionService.updateMessage(message);
callback.success(message);
@@ -107,8 +104,6 @@ public class PgpEngine {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
- URL url = message.getFileParams().url;
- MessageUtil.updateFileParams(message, url);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
PgpEngine.this.mXmppConnectionService
.updateMessage(message);
@@ -150,12 +145,7 @@ public class PgpEngine {
if (!message.needsUploading()) {
params.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true);
- String body;
- if (message.hasFileOnRemoteHost()) {
- body = message.getFileParams().url.toString();
- } else {
- body = message.getBody();
- }
+ String body = message.getBody();
InputStream is = new ByteArrayInputStream(body.getBytes());
final OutputStream os = new ByteArrayOutputStream();
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java
index 3433a63b..f91dc066 100644
--- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlServiceImpl.java
@@ -33,6 +33,7 @@ import java.util.Map;
import java.util.Random;
import java.util.Set;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import eu.siacs.conversations.Config;
@@ -463,7 +464,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo
PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
Signature verifier = Signature.getInstance("sha256WithRSA");
- verifier.initSign(x509PrivateKey,mXmppConnectionService.getRNG());
+ verifier.initSign(x509PrivateKey, ConversationsPlusApplication.getSecureRandom());
verifier.update(axolotlPublicKey.serialize());
byte[] signature = verifier.sign();
IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());
@@ -670,7 +671,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo
verifier.update(identityKey.serialize());
if (verifier.verify(verification.second)) {
try {
- mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
String fingerprint = session.getFingerprint();
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
@@ -925,12 +926,7 @@ public class AxolotlServiceImpl implements OnAdvancedStreamFeaturesLoaded, Axolo
XmppAxolotlMessage axolotlMessage = buildHeader(message.getConversation());
if (axolotlMessage != null) {
- final String content;
- if (message.hasFileOnRemoteHost()) {
- content = message.getFileParams().url.toString();
- } else {
- content = message.getBody();
- }
+ final String content = message.getBody();
try {
axolotlMessage.encrypt(content);
} catch (CryptoFailedException e) {
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index 4c4a1916..9b4198c8 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -20,6 +20,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
@@ -172,6 +174,8 @@ public class Account extends AbstractEntity {
private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
+ public static final String PW_SEED = "sadjgdiahsdkhashp3zt98edAFSFIOKZUIUOz23ejj12ezhez2398iehz";
+
public Account() {
this.uuid = "0";
}
@@ -210,9 +214,10 @@ public class Account extends AbstractEntity {
cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
} catch (final InvalidJidException ignored) {
}
+ String password = SimpleCryptoUtil.decrypt(PW_SEED, cursor.getString(cursor.getColumnIndex(PASSWORD)));
return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
jid,
- cursor.getString(cursor.getColumnIndex(PASSWORD)),
+ password,
cursor.getInt(cursor.getColumnIndex(OPTIONS)),
cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
cursor.getString(cursor.getColumnIndex(KEYS)),
@@ -342,7 +347,7 @@ public class Account extends AbstractEntity {
values.put(UUID, uuid);
values.put(USERNAME, jid.getLocalpart());
values.put(SERVER, jid.getDomainpart());
- values.put(PASSWORD, password);
+ values.put(PASSWORD, SimpleCryptoUtil.encrypt(PW_SEED, password));
values.put(OPTIONS, options);
values.put(KEYS, this.keys.toString());
values.put(ROSTERVERSION, rosterVersion);
diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java
index 7878cecd..299c5f7d 100644
--- a/src/main/java/eu/siacs/conversations/entities/Conversation.java
+++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java
@@ -748,12 +748,7 @@ public class Conversation extends AbstractEntity implements Blockable {
for (int i = this.messages.size() - 1; i >= 0; --i) {
Message message = this.messages.get(i);
if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
- String otherBody;
- if (message.hasFileOnRemoteHost()) {
- otherBody = message.getFileParams().url.toString();
- } else {
- otherBody = message.getBody();
- }
+ String otherBody = message.getBody();
if (otherBody != null && otherBody.equals(body)) {
return message;
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index 6faebc65..5abba478 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -6,6 +6,9 @@ import android.database.Cursor;
import java.net.MalformedURLException;
import java.net.URL;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.MimeUtils;
@@ -84,6 +87,7 @@ public class Message extends AbstractEntity {
private Decision mTreatAsDownloadAble = Decision.NOT_DECIDED;
private boolean httpUploaded;
+ private FileParams fileParams;
private Message() {
@@ -335,6 +339,9 @@ public class Message extends AbstractEntity {
public void setType(int type) {
this.type = type;
+ if (null != this.fileParams && (type == Message.TYPE_FILE || type == Message.TYPE_IMAGE)) {
+ this.setFileParams(new FileParams());
+ }
}
public boolean isCarbon() {
@@ -368,17 +375,8 @@ public class Message extends AbstractEntity {
|| message.getBody() == null || message.getCounterpart() == null) {
return false;
} else {
- String body, otherBody;
- if (this.hasFileOnRemoteHost()) {
- body = this.getFileParams().url.toString();
- } else {
- body = this.getBody();
- }
- if (message.hasFileOnRemoteHost()) {
- otherBody = message.getFileParams().url.toString();
- } else {
- otherBody = message.getBody();
- }
+ String body = this.getBody();
+ String otherBody = message.getBody();
if (message.getRemoteMsgId() != null && this.getRemoteMsgId() != null) {
return (message.getRemoteMsgId().equals(this.getRemoteMsgId())
@@ -493,27 +491,10 @@ public class Message extends AbstractEntity {
}
private String extractRelevantExtension(String path) {
- if (path == null || path.isEmpty()) {
- return null;
- }
-
- String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
-
- final String lastPart = FileUtils.getLastExtension(filename);
-
- if (!lastPart.isEmpty()) {
- // we want the real file extension, not the crypto one
- final String secondToLastPart = FileUtils.getSecondToLastExtension(filename);
- if (!secondToLastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
- return secondToLastPart;
- } else {
- return lastPart;
- }
- }
- return null;
+ return FileUtils.getRelevantExtension(path);
}
- public String getMimeType() {
+ public String getMimeType() { // TODO: Move to fileparams
if (relativeFilePath != null) {
int start = relativeFilePath.lastIndexOf('.') + 1;
if (start < relativeFilePath.length()) {
@@ -592,103 +573,6 @@ public class Message extends AbstractEntity {
}
}
- public FileParams getFileParams() {
- FileParams params = getLegacyFileParams();
- if (params != null) {
- return params;
- }
- params = new FileParams();
- if (this.transferable != null) {
- params.size = this.transferable.getFileSize();
- }
- if (this.getBody() == null) {
- return params;
- }
- String parts[] = this.getBody().split("\\|");
- switch (parts.length) {
- case 1:
- try {
- params.size = Long.parseLong(parts[0]);
- } catch (NumberFormatException e) {
- try {
- params.url = new URL(parts[0]);
- } catch (MalformedURLException e1) {
- params.url = null;
- }
- }
- break;
- case 2:
- case 4:
- try {
- params.url = new URL(parts[0]);
- } catch (MalformedURLException e1) {
- params.url = null;
- }
- try {
- params.size = Long.parseLong(parts[1]);
- } catch (NumberFormatException e) {
- params.size = 0;
- }
- try {
- params.width = Integer.parseInt(parts[2]);
- } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
- params.width = 0;
- }
- try {
- params.height = Integer.parseInt(parts[3]);
- } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
- params.height = 0;
- }
- break;
- case 3:
- try {
- params.size = Long.parseLong(parts[0]);
- } catch (NumberFormatException e) {
- params.size = 0;
- }
- try {
- params.width = Integer.parseInt(parts[1]);
- } catch (NumberFormatException e) {
- params.width = 0;
- }
- try {
- params.height = Integer.parseInt(parts[2]);
- } catch (NumberFormatException e) {
- params.height = 0;
- }
- break;
- }
- return params;
- }
-
- public FileParams getLegacyFileParams() {
- FileParams params = new FileParams();
- if (this.getBody() == null) {
- return params;
- }
- String parts[] = this.getBody().split(",");
- if (parts.length == 3) {
- try {
- params.size = Long.parseLong(parts[0]);
- } catch (NumberFormatException e) {
- return null;
- }
- try {
- params.width = Integer.parseInt(parts[1]);
- } catch (NumberFormatException e) {
- return null;
- }
- try {
- params.height = Integer.parseInt(parts[2]);
- } catch (NumberFormatException e) {
- return null;
- }
- return params;
- } else {
- return null;
- }
- }
-
public void untie() {
this.mNextMessage = null;
this.mPreviousMessage = null;
@@ -698,19 +582,22 @@ public class Message extends AbstractEntity {
return type == TYPE_FILE || type == TYPE_IMAGE;
}
- public boolean hasFileOnRemoteHost() {
- return isFileOrImage() && getFileParams().url != null;
- }
+ public boolean hasFileAttached() {
+ return isFileOrImage() || isHttpUploaded() || (null != fileParams && null != fileParams.getPath());
+ }
- public boolean needsUploading() {
- return isFileOrImage() && getFileParams().url == null;
+ /*
+ @TODO better
+ */
+ public boolean hasFileOnRemoteHost() {
+ return hasFileAttached() && null != getFileParams() && getFileParams().isRemoteAvailable();
}
- public class FileParams {
- public URL url;
- public long size = 0;
- public int width = 0;
- public int height = 0;
+ /*
+ @TODO better
+ */
+ public boolean needsUploading() {
+ return hasFileAttached() && getFileParams().getFileStatus() == FileStatus.NEEDS_UPLOAD;
}
public void setFingerprint(String fingerprint) {
@@ -765,6 +652,14 @@ public class Message extends AbstractEntity {
this.httpUploaded = httpUploaded;
}
+ public FileParams getFileParams() {
+ return this.fileParams;
+ }
+
+ public void setFileParams(FileParams params) {
+ this.fileParams = params;
+ }
+
private static int getCleanedEncryption(int encryption) {
if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
return ENCRYPTION_PGP;
diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
index eff9d9c0..012a5943 100644
--- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java
@@ -16,11 +16,11 @@ import java.util.List;
import java.util.Set;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
-import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
@@ -244,18 +244,6 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
- public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
- IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
- packet.setTo(host);
- Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD);
- request.addChild("filename").setContent(file.getName());
- request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
- if (mime != null) {
- request.addChild("content-type").setContent(mime);
- }
- return packet;
- }
-
public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
index c003da43..582d64e6 100644
--- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
+++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java
@@ -10,6 +10,7 @@ import java.util.Locale;
import java.util.TimeZone;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.entities.FileParams;
import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
@@ -71,13 +72,13 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
- public static void addXhtmlImImage(MessagePacket packet, Message.FileParams params) {
+ public static void addXhtmlImImage(MessagePacket packet, FileParams params) {
Element html = packet.addChild("html", "http://jabber.org/protocol/xhtml-im");
Element body = html.addChild("body", "http://www.w3.org/1999/xhtml");
Element img = body.addChild("img");
- img.setAttribute("src", params.url.toString());
- img.setAttribute("height", params.height);
- img.setAttribute("width", params.width);
+ img.setAttribute("src", params.getUrl());
+ img.setAttribute("height", params.getHeight());
+ img.setAttribute("width", params.getWidth());
}
public static void addMessageHints(MessagePacket packet) {
@@ -95,12 +96,7 @@ public class MessageGenerator extends AbstractGenerator {
MessagePacket packet = preparePacket(message);
addMessageHints(packet);
try {
- String content;
- if (message.hasFileOnRemoteHost()) {
- content = message.getFileParams().url.toString();
- } else {
- content = message.getBody();
- }
+ String content = message.getBody();
packet.setBody(otrSession.transformSending(content)[0]);
return packet;
} catch (OtrException e) {
@@ -112,14 +108,14 @@ public class MessageGenerator extends AbstractGenerator {
MessagePacket packet = preparePacket(message);
String content;
if (message.hasFileOnRemoteHost()) {
- Message.FileParams fileParams = message.getFileParams();
- content = fileParams.url.toString();
+ FileParams fileParams = message.getFileParams();
+ content = message.getBody();
if (message.isHttpUploaded()) {
packet.addChild(new HttpUploadHint());
}
packet.addChild("x","jabber:x:oob").addChild("url").setContent(content);
- if (fileParams.width > 0 && fileParams.height > 0) {
- addXhtmlImImage(packet,fileParams);
+ if (fileParams.getWidth() > 0 && fileParams.getHeight() > 0) {
+ addXhtmlImImage(packet, fileParams);
}
} else {
content = message.getBody();
diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
index f105646f..a1ad3c01 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java
@@ -17,6 +17,8 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -24,29 +26,22 @@ import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.SSLSocketHelper;
public class HttpConnectionManager extends AbstractConnectionManager {
+ private static HttpConnectionManager INSTANCE;
- public HttpConnectionManager(XmppConnectionService service) {
- super(service);
- }
+ public static void init() {
+ INSTANCE = new HttpConnectionManager();
+ }
private List<HttpDownloadConnection> downloadConnections = new CopyOnWriteArrayList<>();
- private List<HttpUploadConnection> uploadConnections = new CopyOnWriteArrayList<>();
- public HttpDownloadConnection createNewDownloadConnection(Message message) {
- return this.createNewDownloadConnection(message, false);
+ public static HttpDownloadConnection createNewDownloadConnection(Message message) {
+ return createNewDownloadConnection(message, false);
}
- public HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) {
- HttpDownloadConnection connection = new HttpDownloadConnection(this);
+ public static HttpDownloadConnection createNewDownloadConnection(Message message, boolean interactive) {
+ HttpDownloadConnection connection = new HttpDownloadConnection(INSTANCE);
connection.init(message,interactive);
- this.downloadConnections.add(connection);
- return connection;
- }
-
- public HttpUploadConnection createNewUploadConnection(Message message, boolean delay) {
- HttpUploadConnection connection = new HttpUploadConnection(this);
- connection.init(message,delay);
- this.uploadConnections.add(connection);
+ INSTANCE.downloadConnections.add(connection);
return connection;
}
@@ -54,30 +49,24 @@ public class HttpConnectionManager extends AbstractConnectionManager {
this.downloadConnections.remove(connection);
}
- public void finishUploadConnection(HttpUploadConnection httpUploadConnection) {
- this.uploadConnections.remove(httpUploadConnection);
- }
-
- public void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) {
+ public static void setupTrustManager(final HttpsURLConnection connection, final boolean interactive) {
final X509TrustManager trustManager;
final HostnameVerifier hostnameVerifier;
if (interactive) {
- trustManager = mXmppConnectionService.getMemorizingTrustManager();
- hostnameVerifier = mXmppConnectionService
- .getMemorizingTrustManager().wrapHostnameVerifier(
+ trustManager = ConversationsPlusApplication.getMemorizingTrustManager();
+ hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager().wrapHostnameVerifier(
new StrictHostnameVerifier());
} else {
- trustManager = mXmppConnectionService.getMemorizingTrustManager()
+ trustManager = ConversationsPlusApplication.getMemorizingTrustManager()
.getNonInteractive();
- hostnameVerifier = mXmppConnectionService
- .getMemorizingTrustManager()
+ hostnameVerifier = ConversationsPlusApplication.getMemorizingTrustManager()
.wrapHostnameVerifierNonInteractive(
new StrictHostnameVerifier());
}
try {
final SSLContext sc = SSLSocketHelper.getSSLContext();
sc.init(null, new X509TrustManager[]{trustManager},
- mXmppConnectionService.getRNG());
+ ConversationsPlusApplication.getSecureRandom());
final SSLSocketFactory sf = sc.getSocketFactory();
final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
index 66687c3a..8b905fd1 100644
--- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
+++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java
@@ -18,9 +18,12 @@ 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.enums.FileStatus;
import de.thedevstack.conversationsplus.exceptions.RemoteFileNotFoundException;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -48,7 +51,7 @@ public class HttpDownloadConnection implements Transferable {
public HttpDownloadConnection(HttpConnectionManager manager) {
this.mHttpConnectionManager = manager;
- this.mXmppConnectionService = manager.getXmppConnectionService();
+ this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService;
}
@Override
@@ -73,11 +76,7 @@ public class HttpDownloadConnection implements Transferable {
this.message = message;
this.message.setTransferable(this);
try {
- if (message.hasFileOnRemoteHost()) {
- mUrl = message.getFileParams().url;
- } else {
- mUrl = new URL(message.getBody());
- }
+ mUrl = new URL(message.getFileParams().getUrl());
final String sUrlFilename = mUrl.getPath().substring(mUrl.getPath().lastIndexOf('/')).toLowerCase();
final String lastPart = FileUtils.getLastExtension(sUrlFilename);
@@ -130,6 +129,7 @@ public class HttpDownloadConnection implements Transferable {
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);
@@ -175,7 +175,7 @@ public class HttpDownloadConnection implements Transferable {
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
return;
} catch (RemoteFileNotFoundException e) {
- message.setNoDownloadable();
+ message.setNoDownloadable(); // TODO Set remote file status to not-available
cancel();
return;
} catch (IOException e) {
@@ -265,7 +265,7 @@ public class HttpDownloadConnection implements Transferable {
private void download() throws SSLHandshakeException, IOException {
InputStream is = null;
- PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
+ PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("http_download_"+message.getUuid());
try {
wakeLock.acquire();
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
deleted file mode 100644
index a7375b8a..00000000
--- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java
+++ /dev/null
@@ -1,242 +0,0 @@
-package eu.siacs.conversations.http;
-
-import android.app.PendingIntent;
-import android.os.PowerManager;
-import android.util.Pair;
-
-import java.io.FileNotFoundException;
-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.Scanner;
-
-import javax.net.ssl.HttpsURLConnection;
-
-import de.thedevstack.android.logcat.Logging;
-import de.thedevstack.conversationsplus.ConversationsPlusApplication;
-import de.thedevstack.conversationsplus.utils.MessageUtil;
-import de.thedevstack.conversationsplus.utils.StreamUtil;
-import eu.siacs.conversations.Config;
-import eu.siacs.conversations.entities.Account;
-import eu.siacs.conversations.entities.DownloadableFile;
-import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Transferable;
-import eu.siacs.conversations.persistance.FileBackend;
-import eu.siacs.conversations.services.AbstractConnectionManager;
-import eu.siacs.conversations.services.XmppConnectionService;
-import eu.siacs.conversations.ui.UiCallback;
-import eu.siacs.conversations.utils.CryptoHelper;
-import eu.siacs.conversations.utils.Xmlns;
-import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
-import eu.siacs.conversations.xmpp.jid.Jid;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
-
-public class HttpUploadConnection implements Transferable {
-
- private HttpConnectionManager mHttpConnectionManager;
- private XmppConnectionService mXmppConnectionService;
-
- private boolean canceled = false;
- private boolean delayed = false;
- private Account account;
- private DownloadableFile file;
- private Message message;
- private String mime;
- private URL mGetUrl;
- private URL mPutUrl;
-
- private byte[] key = null;
-
- private long transmitted = 0;
-
- private InputStream mFileInputStream;
-
- public HttpUploadConnection(HttpConnectionManager httpConnectionManager) {
- this.mHttpConnectionManager = httpConnectionManager;
- this.mXmppConnectionService = httpConnectionManager.getXmppConnectionService();
- }
-
- @Override
- public boolean start() {
- return false;
- }
-
- @Override
- public int getStatus() {
- return STATUS_UPLOADING;
- }
-
- @Override
- public long getFileSize() {
- return file == null ? 0 : file.getExpectedSize();
- }
-
- @Override
- public int getProgress() {
- if (file == null) {
- return 0;
- }
- return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
- }
-
- @Override
- public void cancel() {
- this.canceled = true;
- }
-
- private void fail() {
- mHttpConnectionManager.finishUploadConnection(this);
- message.setTransferable(null);
- MessageUtil.markMessage(message, Message.STATUS_SEND_FAILED);
- StreamUtil.close(mFileInputStream);
- }
-
- public void init(Message message, boolean delay) {
- this.message = message;
- this.message.setHttpUploaded(true);
- this.message.setNoDownloadable();
- this.account = message.getConversation().getAccount();
- this.file = FileBackend.getFile(message, false);
- this.mime = this.file.getMimeType();
- this.delayed = delay;
- if (Config.ENCRYPT_ON_HTTP_UPLOADED
- || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
- || message.getEncryption() == Message.ENCRYPTION_OTR) {
- this.key = new byte[48];
- mXmppConnectionService.getRNG().nextBytes(this.key);
- this.file.setKeyAndIv(this.key);
- }
- Pair<InputStream,Integer> pair;
- try {
- pair = AbstractConnectionManager.createInputStream(file, true);
- } catch (FileNotFoundException e) {
- fail();
- return;
- }
- this.file.setExpectedSize(pair.second);
- this.mFileInputStream = pair.first;
- Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD);
- IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime);
- mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() {
- @Override
- public void onIqPacketReceived(Account account, IqPacket packet) {
- if (packet.getType() == IqPacket.TYPE.RESULT) {
- Element slot = packet.findChild("slot",Xmlns.HTTP_UPLOAD);
- if (slot != null) {
- try {
- mGetUrl = new URL(slot.findChildContent("get"));
- mPutUrl = new URL(slot.findChildContent("put"));
- if (!canceled) {
- new Thread(new FileUploader()).start();
- }
- } catch (MalformedURLException e) {
- fail();
- }
- } else {
- fail();
- }
- } else {
- fail();
- }
- }
- });
- message.setTransferable(this);
- MessageUtil.markMessage(message, Message.STATUS_UNSEND);
- }
-
- private class FileUploader implements Runnable {
-
- @Override
- public void run() {
- this.upload();
- }
-
- private void upload() {
- OutputStream os = null;
- InputStream errorStream = null;
- HttpURLConnection connection = null;
- PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid());
- try {
- wakeLock.acquire();
- Logging.d(Config.LOGTAG, "uploading to " + mPutUrl.toString());
- connection = (HttpURLConnection) mPutUrl.openConnection();
-
- if (connection instanceof HttpsURLConnection) {
- mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, true);
- }
- connection.setRequestMethod("PUT");
- connection.setFixedLengthStreamingMode((int) file.getExpectedSize());
- connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
- connection.setRequestProperty("User-Agent", ConversationsPlusApplication.getNameAndVersion());
- connection.setDoOutput(true);
- connection.connect();
- os = connection.getOutputStream();
- transmitted = 0;
- int count = -1;
- byte[] buffer = new byte[4096];
- while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
- transmitted += count;
- os.write(buffer, 0, count);
- mXmppConnectionService.updateConversationUi();
- }
- os.flush();
- os.close();
- mFileInputStream.close();
- int code = connection.getResponseCode();
- if (code == 200 || code == 201) {
- Logging.d(Config.LOGTAG, "finished uploading file");
- if (key != null) {
- mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
- }
- MessageUtil.updateFileParams(message, mGetUrl);
- FileBackend.updateMediaScanner(file, mXmppConnectionService);
- message.setTransferable(null);
- message.setCounterpart(message.getConversation().getJid().toBareJid());
- if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
- mXmppConnectionService.getPgpEngine().encrypt(message, new UiCallback<Message>() {
- @Override
- public void success(Message message) {
- mXmppConnectionService.resendMessage(message,delayed);
- }
-
- @Override
- public void error(int errorCode, Message object) {
- fail();
- }
-
- @Override
- public void userInputRequried(PendingIntent pi, Message object) {
- fail();
- }
- });
- } else {
- mXmppConnectionService.resendMessage(message, delayed);
- }
- } else {
- errorStream = connection.getErrorStream();
- Logging.e("httpupload", "file upload failed: http code (" + code + ") " + new Scanner(errorStream).useDelimiter("\\A").next());
- fail();
- }
- } catch (IOException e) {
- errorStream = (null != connection) ? connection.getErrorStream() : null;
- String httpResponseMessage = null;
- if (null != errorStream) {
- httpResponseMessage = new Scanner(errorStream).useDelimiter("\\A").next();
- }
- Logging.e("httpupload", ((null != httpResponseMessage) ? ("http response: " + httpResponseMessage + ", ") : "") + "exception message: " + e.getMessage());
- fail();
- } finally {
- StreamUtil.close(os);
- StreamUtil.close(errorStream);
- if (connection != null) {
- connection.disconnect();
- }
- wakeLock.release();
- }
- }
- }
-}
diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
index c79110c9..571a29b4 100644
--- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java
@@ -3,6 +3,8 @@ package eu.siacs.conversations.parser;
import android.util.Log;
import android.util.Pair;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.xmpp.httpuploadim.HttpUploadHint;
import de.tzur.conversations.Settings;
@@ -10,7 +12,6 @@ import de.tzur.conversations.Settings;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
-import java.net.URL;
import java.util.ArrayList;
import java.util.Set;
@@ -473,7 +474,11 @@ public class MessageParser extends AbstractParser implements
&& message.treatAsDownloadable() != Message.Decision.NEVER
&& ConversationsPlusPreferences.autoAcceptFileSize() > 0
&& (message.isHttpUploaded() || ConversationsPlusPreferences.autoDownloadFileLink())) {
- this.mXmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message);
+ FileParams fileParams = new FileParams();
+ fileParams.setFileStatus(FileStatus.NEEDS_DOWNLOAD);
+ fileParams.setUrl(message.getBody());
+ message.setFileParams(fileParams);
+ HttpConnectionManager.createNewDownloadConnection(message);
} else {
if (query == null) {
mXmppConnectionService.getNotificationService().push(message);
diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
index 76da5a31..3d933364 100644
--- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
+++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java
@@ -17,17 +17,14 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
-import eu.siacs.conversations.entities.Presences;
-import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.generator.PresenceGenerator;
+import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
-import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
-import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
public class PresenceParser extends AbstractParser implements
@@ -186,14 +183,19 @@ public class PresenceParser extends AbstractParser implements
final String resource = from.isBareJid() ? "" : from.getResourcepart();
contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick"));
Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
- if (avatar != null && !contact.isSelf()) {
+ if (avatar != null && (!contact.isSelf() || null == account.getAvatar())) {
avatar.owner = from.toBareJid();
if (AvatarUtil.isAvatarCached(avatar)) {
- if (contact.setAvatar(avatar)) {
+ if (avatar.owner.equals(account.getJid().toBareJid())) {
+ account.setAvatar(avatar.getFilename());
+ DatabaseBackend.getInstance().updateAccount(account);
+ AvatarService.getInstance().clear(account);
+ UiUpdateHelper.updateAccountUi();
+ } else if (contact.setAvatar(avatar)) {
AvatarService.getInstance().clear(contact);
- UiUpdateHelper.updateConversationUi();
UiUpdateHelper.updateRosterUi();
}
+ UiUpdateHelper.updateConversationUi();
} else {
AvatarService.getInstance().fetchAvatar(account, avatar);
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index bd20e694..793b050f 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -34,7 +34,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONException;
import de.thedevstack.android.logcat.Logging;
-import de.thedevstack.conversationsplus.persistance.MessageDatabaseAccess;
+
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import de.thedevstack.conversationsplus.persistance.db.access.CursorHelper;
+import de.thedevstack.conversationsplus.persistance.db.access.MessageDatabaseAccess;
+import de.thedevstack.conversationsplus.utils.SimpleCryptoUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlServiceImpl;
@@ -56,7 +60,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 25;
private static final int C_TO_CPLUS_VERSION_OFFSET = 1000;
- private static final int CPLUS_DATABASE_VERSION = 1;
+ private static final int CPLUS_DATABASE_VERSION = 3;
private static final int CPLUS_DATABASE_VERSION_MULTIPLIER = 100;
private static final int PHYSICAL_DATABASE_VERSION = DATABASE_VERSION + C_TO_CPLUS_VERSION_OFFSET + (CPLUS_DATABASE_VERSION * CPLUS_DATABASE_VERSION_MULTIPLIER);
@@ -192,18 +196,31 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
- // Create Conversations+ related tables
- db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V0);
+ MessageDatabaseAccess.create(db);
}
protected void onUpgradeCPlusDatabase(SQLiteDatabase db, int oldVersion, int newVersion) {
Logging.d("db.upgrade.cplus", "Updating Conversations+ database from version '" + oldVersion + "' to '" + newVersion + "'");
if (oldVersion < newVersion) {
- if (oldVersion == 0 && newVersion == 1) {
- Logging.d("db.upgrade.cplus", "Creating additional parameters table for messages.");
- db.execSQL(MessageDatabaseAccess.TABLE_ADDITIONAL_PARAMETERS_CREATE_V0);
- db.execSQL("INSERT INTO " + MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS + "(" + MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + ") "
- + " SELECT " + Message.UUID + " FROM " + Message.TABLENAME);
+ if (oldVersion < 1 && newVersion >= 1) {
+ MessageDatabaseAccess.upgrade(db, oldVersion, newVersion);
+ }
+ if (oldVersion < 2 && newVersion >= 2) {
+ Logging.d("db.upgrade.cplus", "Encrypt all passwords for the first time");
+ Cursor cursor = db.rawQuery("SELECT " + Account.UUID + ", " + Account.PASSWORD + " FROM " + Account.TABLENAME, new String[0]);
+ while (cursor.moveToNext()) {
+ String uuid = CursorHelper.getString(cursor, Account.UUID);
+ String password = CursorHelper.getString(cursor, Account.PASSWORD);
+ String encryptedPassword = SimpleCryptoUtil.encrypt(Account.PW_SEED, password);
+ ContentValues values = new ContentValues();
+ values.put(Account.PASSWORD, encryptedPassword);
+ db.update(Account.TABLENAME, values, Account.UUID + "=?", new String[] {uuid});
+ }
+ cursor.close();
+ }
+
+ if (oldVersion < 3 && newVersion >= 3) {
+ MessageDatabaseAccess.upgrade(db, oldVersion, newVersion);
}
}
}
@@ -422,6 +439,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
+ public static synchronized DatabaseBackend getInstance() {
+ return getInstance(ConversationsPlusApplication.getAppContext());
+ }
+
public static synchronized DatabaseBackend getInstance(Context context) {
if (instance == null) {
instance = new DatabaseBackend(context);
@@ -645,15 +666,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
public void updateMessage(Message message) {
- Logging.d("db.msg.update", "Updating message with uuid '" + message.getUuid() + "', isRead: " + message.isRead());
- SQLiteDatabase db = this.getWritableDatabase();
- String[] args = {message.getUuid()};
- db.beginTransaction();
- db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
- + "=?", args);
- db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, MessageDatabaseAccess.getAdditionalParametersContentValues(message), MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", args);
- db.setTransactionSuccessful();
- db.endTransaction();
+ this.updateMessage(message, message.getUuid());
}
public void updateMessage(Message message, String uuid) {
@@ -664,7 +677,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.beginTransaction();
db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ "=?", args);
- db.update(MessageDatabaseAccess.TABLE_NAME_ADDITIONAL_PARAMETERS, MessageDatabaseAccess.getAdditionalParametersContentValues(message), MessageDatabaseAccess.COLUMN_NAME_MSG_PARAMS_MSGUUID + "=?", args);
+ MessageDatabaseAccess.updateMessageParameters(db, message, uuid);
db.setTransactionSuccessful();
db.endTransaction();
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
index 8abbb0cb..6f99ef55 100644
--- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java
@@ -20,9 +20,12 @@ import java.util.Locale;
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.exceptions.FileCopyException;
import de.thedevstack.conversationsplus.persistance.observers.FileDeletionObserver;
import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -90,10 +93,10 @@ public class FileBackend {
}
}
- public static boolean deleteFile(Message message, XmppConnectionService xmppConnectionService) {
+ public static boolean deleteFile(Message message) {
File file = getFile(message);
if (file.delete()) {
- updateMediaScanner(file, xmppConnectionService);
+ updateMediaScanner(file, XmppConnectionServiceAccessor.xmppConnectionService);
return true;
} else {
return false;
@@ -105,31 +108,55 @@ public class FileBackend {
}
public static DownloadableFile getFile(Message message, boolean decrypted) {
- final boolean encrypted = !decrypted
- && (message.getEncryption() == Message.ENCRYPTION_PGP
- || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
- final DownloadableFile file;
- String path = message.getRelativeFilePath();
- if (path == null) {
- path = message.getUuid();
- }
- if (path.startsWith("/")) {
- file = new DownloadableFile(path);
- } else {
- String mime = message.getMimeType();
- if (mime != null && mime.startsWith("image")) {
- file = new DownloadableFile(getConversationsImageDirectory() + path);
- } else {
- file = new DownloadableFile(getConversationsFileDirectory() + path);
- }
- }
- if (encrypted) {
- return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp");
- } else {
- return file;
- }
+ return new DownloadableFile(getFilePath(message, decrypted));
}
+ protected static String getFilePath(Message message, String extension) {
+ String path = FileBackend.getFilePath(message, true);
+ if (!path.endsWith(extension)) {
+ path += "." + extension;
+ }
+
+ return path;
+ }
+
+ protected static String getFilePath(Message message, Uri uri) {
+ String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri);
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
+
+ return getFilePath(message, extension);
+ }
+
+ protected static String getFilePath(Message message, boolean decrypted) {
+ final boolean encrypted = !decrypted
+ && (message.getEncryption() == Message.ENCRYPTION_PGP
+ || message.getEncryption() == Message.ENCRYPTION_DECRYPTED);
+ FileParams fileParams = message.getFileParams();
+ if (null == fileParams) {
+ fileParams = new FileParams();
+ message.setFileParams(fileParams);
+ }
+ String path = fileParams.getPath();
+
+ if (null == path) { // File does not yet exist
+ path = message.getUuid();
+ String mime = message.getMimeType();
+ if (mime != null && mime.startsWith("image")) { // TODO: Check if this can be determined in a better way
+ path = getConversationsImageDirectory() + path;
+ } else {
+ path = getConversationsFileDirectory() + path;
+ }
+ String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
+ path += "." + extension;
+
+ fileParams.setPath(path);
+ }
+ if (encrypted) {
+ path += ".pgp";
+ }
+ return path;
+ }
+
public static String getConversationsFileDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + ConversationsPlusPreferences.fileTransferFolder() + File.separator;
}
@@ -174,14 +201,16 @@ public class FileBackend {
public static void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
- String mime = ConversationsPlusApplication.getInstance().getContentResolver().getType(uri);
- String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
- message.setRelativeFilePath(message.getUuid() + "." + extension);
+ String path = getFilePath(message, uri);
+ message.getFileParams().setPath(path);
+ message.setRelativeFilePath(path); // TODO: Remove
copyFileToPrivateStorage(getFile(message), uri);
}
public static DownloadableFile compressImageAndCopyToPrivateStorage(Message message, Bitmap scaledBitmap) throws FileCopyException {
- message.setRelativeFilePath(FileBackend.getPrivateImageDirectoryPath() + message.getUuid() + ".jpg");
+ String path = getFilePath(message, "jpg");
+ message.getFileParams().setPath(path);
+ message.setRelativeFilePath(path); // TODO: Remove
DownloadableFile file = getFile(message);
file.getParentFile().mkdirs();
OutputStream os = null;
diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
index 7728c38a..dbee8720 100644
--- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java
@@ -30,23 +30,16 @@ import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile;
public class AbstractConnectionManager {
- protected XmppConnectionService mXmppConnectionService;
-
- public AbstractConnectionManager(XmppConnectionService service) {
- this.mXmppConnectionService = service;
- }
-
- public XmppConnectionService getXmppConnectionService() {
- return this.mXmppConnectionService;
- }
public boolean hasStoragePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ return ConversationsPlusApplication.getAppContext().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
} else {
return true;
}
@@ -125,9 +118,4 @@ public class AbstractConnectionManager {
return null;
}
}
-
- public PowerManager.WakeLock createWakeLock(String name) {
- PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
- return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name);
- }
}
diff --git a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
index 53d0caaf..38c8e1d4 100644
--- a/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
+++ b/src/main/java/eu/siacs/conversations/services/ExportLogsService.java
@@ -103,7 +103,7 @@ public class ExportLogsService extends Service {
break;
}
if (jid != null) {
- String body = message.hasFileOnRemoteHost() ? message.getFileParams().url.toString() : message.getBody();
+ String body = message.hasFileOnRemoteHost() ? message.getFileParams().getUrl() : message.getBody();
bw.write(String.format(MESSAGE_STRING_FORMAT, date, jid,
body.replace("\\\n", "\\ \n").replace("\n", "\\ \n")));
}
diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
index 613d7150..e76eb16b 100644
--- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
+++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java
@@ -9,6 +9,8 @@ import java.util.Iterator;
import java.util.List;
import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@@ -290,7 +292,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.account = account;
this.start = start;
this.end = end;
- this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
+ this.queryId = new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32);
}
private Query page(String reference) {
diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java
index 12c11dd0..d11661fa 100644
--- a/src/main/java/eu/siacs/conversations/services/NotificationService.java
+++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java
@@ -356,7 +356,8 @@ public class NotificationService {
if (message.getType() != Message.TYPE_TEXT
&& message.getTransferable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP
- && message.getFileParams().height > 0) {
+ && message.getFileParams() != null
+ && message.getFileParams().getHeight() > 0) { // TODO Use FileParams.getMimeType()
return message;
}
}
diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
index 5f7a9a9e..90f9027a 100644
--- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
+++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java
@@ -12,7 +12,6 @@ import android.graphics.Bitmap;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
-import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -58,13 +57,14 @@ import de.duenndns.ssl.MemorizingTrustManager;
import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
-import de.thedevstack.conversationsplus.exceptions.FileCopyException;
+import de.thedevstack.conversationsplus.services.filetransfer.FileTransferManager;
import de.thedevstack.conversationsplus.utils.ImageUtil;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.utils.UiUpdateHelper;
import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
import de.thedevstack.conversationsplus.utils.XmppSendUtil;
import de.tzur.conversations.Settings;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
@@ -97,7 +97,6 @@ import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
-import eu.siacs.conversations.utils.PRNGFixes;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
@@ -122,6 +121,7 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
+
import me.leolin.shortcutbadger.ShortcutBadger;
public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
@@ -148,7 +148,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
startService(intent);
}
};
- private MemorizingTrustManager mMemorizingTrustManager;
private NotificationService mNotificationService = new NotificationService(
this);
private OnMessagePacketReceived mMessageParser = new MessageParser(this);
@@ -169,8 +168,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private MessageGenerator mMessageGenerator = new MessageGenerator();
private PresenceGenerator mPresenceGenerator = new PresenceGenerator();
private List<Account> accounts;
- private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
- this);
+ private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager();
public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
@Override
@@ -197,8 +195,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
};
- private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
- this);
private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
private PushManagementService mPushManagementService = new PushManagementService(this);
private OnConversationUpdate mOnConversationUpdate = null;
@@ -239,7 +235,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int mucRosterChangedListenerCount = 0;
private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
private int keyStatusUpdatedListenerCount = 0;
- private SecureRandom mRandom;
private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
private final OnBindListener mOnBindListener = new OnBindListener() {
@@ -311,7 +306,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
&& checkListeners();
Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode));
if (!disabled && !pushMode) {
- int timeToReconnect = mRandom.nextInt(20) + 10;
+ int timeToReconnect = ConversationsPlusApplication.getSecureRandom().nextInt(20) + 10;
scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
}
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
@@ -334,8 +329,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
private WakeLock wakeLock;
- private PowerManager pm;
- private LruCache<String, Bitmap> mBitmapCache;
private Thread mPhoneContactMergerThread;
private EventReceiver mEventReceiver = new EventReceiver();
@@ -577,17 +570,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
@Override
public void onCreate() {
ExceptionHelper.init(getApplicationContext());
- PRNGFixes.apply();
- this.mRandom = new SecureRandom();
- updateMemorizingTrustmanager();
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
- this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
- @Override
- protected int sizeOf(final String key, final Bitmap bitmap) {
- return bitmap.getByteCount() / 1024;
- }
- };
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
@@ -614,8 +598,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
this.pgpServiceConnection.bindToService();
}
- this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
+ this.wakeLock = ConversationsPlusApplication.createPartialWakeLock("XmppConnectionService");
toggleForegroundService();
updateUnreadCountBadge();
UiUpdateHelper.initXmppConnectionService(this);
@@ -628,7 +611,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
super.onTrimMemory(level);
if (level >= TRIM_MEMORY_COMPLETE) {
Log.d(Config.LOGTAG, "clear cache due to low memory");
- getBitmapCache().evictAll();
+ ImageUtil.evictBitmapCache();
}
}
@@ -739,16 +722,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
- private void sendFileMessage(final Message message, final boolean delay) {
- Logging.d(Config.LOGTAG, "send file message");
- final Account account = message.getConversation().getAccount();
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())) {
- mHttpConnectionManager.createNewUploadConnection(message, delay);
- } else {
- mJingleConnectionManager.createNewConnection(message);
- }
- }
-
public void sendMessage(final Message message) {
sendMessage(message, false, false);
}
@@ -776,28 +749,23 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
if (account.isOnlineAndConnected()) {
+ FileTransferManager fileTransferManager = FileTransferManager.getInstance();
switch (message.getEncryption()) {
case Message.ENCRYPTION_NONE:
- if (message.needsUploading()) {
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())
- || message.fixCounterpart()) {
- this.sendFileMessage(message, delay);
- } else {
- break;
- }
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
} else {
packet = mMessageGenerator.generateChat(message);
}
break;
case Message.ENCRYPTION_PGP:
case Message.ENCRYPTION_DECRYPTED:
- if (message.needsUploading()) {
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())
- || message.fixCounterpart()) {
- this.sendFileMessage(message, delay);
- } else {
- break;
- }
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
} else {
packet = mMessageGenerator.generatePgpChat(message);
}
@@ -810,7 +778,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} catch (InvalidJidException e) {
break;
}
- if (message.needsUploading()) {
+ if (message.needsUploading()) { //TODO: Use FileTransferManager with a preselection of filetransfer method
mJingleConnectionManager.createNewConnection(message);
} else {
packet = mMessageGenerator.generateOtrChat(message);
@@ -828,13 +796,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
break;
case Message.ENCRYPTION_AXOLOTL:
message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
- if (message.needsUploading()) {
- if (account.httpUploadAvailable(FileBackend.getFile(message,false).getSize())
- || message.fixCounterpart()) {
- this.sendFileMessage(message, delay);
- } else {
- break;
- }
+ if (fileTransferManager.accept(message)) {
+ if (!fileTransferManager.transferFile(message, delay)) {
+ break;
+ }
} else {
XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
if (axolotlMessage == null) {
@@ -1331,7 +1296,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
callback.onAccountCreated(account);
if (Config.X509_VERIFICATION) {
try {
- getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
callback.informUser(R.string.certificate_chain_is_not_trusted);
}
@@ -1359,7 +1324,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
databaseBackend.updateAccount(account);
if (Config.X509_VERIFICATION) {
try {
- getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
+ ConversationsPlusApplication.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
}
@@ -1901,7 +1866,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
return;
}
- String name = new BigInteger(75, getRNG()).toString(32);
+ String name = new BigInteger(75, ConversationsPlusApplication.getSecureRandom()).toString(32);
Jid jid = Jid.fromParts(name, server, null);
final Conversation conversation = findOrCreateConversation(account, jid, true);
joinMuc(conversation, new OnConferenceJoined() {
@@ -2170,7 +2135,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public boolean renewSymmetricKey(Conversation conversation) {
Account account = conversation.getAccount();
byte[] symmetricKey = new byte[32];
- this.mRandom.nextBytes(symmetricKey);
+ ConversationsPlusApplication.getSecureRandom().nextBytes(symmetricKey);
Session otrSession = conversation.getOtrSession();
if (otrSession != null) {
MessagePacket packet = new MessagePacket();
@@ -2440,36 +2405,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
- public SecureRandom getRNG() {
- return this.mRandom;
- }
-
- public MemorizingTrustManager getMemorizingTrustManager() {
- return this.mMemorizingTrustManager;
- }
-
- public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
- this.mMemorizingTrustManager = trustManager;
- }
-
- public void updateMemorizingTrustmanager() {
- final MemorizingTrustManager tm;
- if (ConversationsPlusPreferences.dontTrustSystemCAs()) {
- tm = new MemorizingTrustManager(getApplicationContext(), null);
- } else {
- tm = new MemorizingTrustManager(getApplicationContext());
- }
- setMemorizingTrustManager(tm);
- }
-
- public PowerManager getPowerManager() {
- return this.pm;
- }
-
- public LruCache<String, Bitmap> getBitmapCache() {
- return this.mBitmapCache;
- }
-
public void syncRosterToDisk(final Account account) {
Runnable runnable = new Runnable() {
@@ -2599,10 +2534,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
return this.mNotificationService;
}
- public HttpConnectionManager getHttpConnectionManager() {
- return this.mHttpConnectionManager;
- }
-
public void resendFailedMessages(final Message message) {
if (message.getStatus() == Message.STATUS_SEND_FAILED) {
message.setTime(System.currentTimeMillis());
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
index f7e53112..2a7432dd 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java
@@ -49,6 +49,7 @@ import de.thedevstack.conversationsplus.ui.dialogs.UserDecisionDialog;
import de.thedevstack.conversationsplus.ui.listeners.ResizePictureUserDecisionListener;
import de.thedevstack.conversationsplus.utils.ConversationUtil;
import de.timroes.android.listview.EnhancedListView;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -60,6 +61,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
+import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
@@ -620,7 +622,7 @@ 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) {
- xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message, true);
+ HttpConnectionManager.createNewDownloadConnection(message, true);
}
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
index 6d170787..5068af8d 100644
--- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
+++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java
@@ -8,7 +8,6 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
-import android.content.IntentSender;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.support.annotation.Nullable;
@@ -24,8 +23,6 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
-import android.widget.AbsListView;
-import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ImageButton;
@@ -49,7 +46,12 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.UUID;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
+import de.thedevstack.conversationsplus.services.filetransfer.http.delete.DeleteRemoteFileService;
+import de.thedevstack.conversationsplus.ui.dialogs.SimpleConfirmDialog;
import de.thedevstack.conversationsplus.ui.dialogs.MessageDetailsDialog;
+import de.thedevstack.conversationsplus.ui.listeners.DeleteFileCallback;
+import de.thedevstack.conversationsplus.ui.listeners.SimpleUserDecisionCallback;
+import de.thedevstack.conversationsplus.ui.listeners.UserDecisionListener;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import eu.siacs.conversations.Config;
@@ -64,6 +66,7 @@ import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
+import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
@@ -580,12 +583,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
deleteFile.setVisible(true);
deleteFile.setTitle(activity.getString(R.string.delete_x_file,UIHelper.getFileDescriptionString(activity, m)));
}
+ if (m.isHttpUploaded() && MessageUtil.isMessageSent(m)) {
+ MenuItem deleteRemoteFile = menu.findItem(R.id.msg_ctx_menu_delete_remote_file);
+ deleteRemoteFile.setVisible(true);
+ }
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
+ case R.id.msg_ctx_menu_delete_remote_file:
+ new SimpleConfirmDialog(getActivity(), R.string.cplus_are_you_sure, new DeleteRemoteFileService(selectedMessage)).show();
+ return true;
case R.id.msg_ctx_mnu_details:
new MessageDetailsDialog(getActivity(), selectedMessage).show();
return true;
@@ -611,7 +621,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
retryDecryption(selectedMessage);
return true;
case R.id.delete_file:
- deleteFile(selectedMessage);
+ new SimpleConfirmDialog(getActivity(), R.string.cplus_are_you_sure, new DeleteFileCallback(selectedMessage)).show();
return true;
default:
return super.onContextItemSelected(item);
@@ -650,13 +660,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
- private void deleteFile(Message message) {
- if (FileBackend.deleteFile(message, activity.xmppConnectionService)) {
- message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
- activity.xmppConnectionService.updateConversationUi();
- }
- }
-
private void resendMessage(Message message) {
if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
DownloadableFile file = FileBackend.getFile(message);
@@ -678,7 +681,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
url = message.getBody();
} else if (message.hasFileOnRemoteHost()) {
resId = R.string.file_url;
- url = message.getFileParams().url.toString();
+ url = message.getFileParams().getUrl();
} else {
url = message.getBody();
resId = R.string.file_url;
@@ -690,8 +693,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
private void downloadFile(Message message) {
- activity.xmppConnectionService.getHttpConnectionManager()
- .createNewDownloadConnection(message,true);
+ HttpConnectionManager.createNewDownloadConnection(message,true);
}
private void cancelTransmission(Message message) {
diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
index 30f71229..6dd8a84f 100644
--- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Locale;
import de.duenndns.ssl.MemorizingTrustManager;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.tzur.conversations.Settings;
import eu.siacs.conversations.R;
@@ -73,7 +74,7 @@ public class SettingsActivity extends XmppActivity implements
removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
- final MemorizingTrustManager mtm = xmppConnectionService.getMemorizingTrustManager();
+ final MemorizingTrustManager mtm = ConversationsPlusApplication.getMemorizingTrustManager();
final ArrayList<String> aliases = Collections.list(mtm.getCertificates());
if (aliases.size() == 0) {
displayToast(getString(R.string.toast_no_trusted_certs));
@@ -180,7 +181,7 @@ public class SettingsActivity extends XmppActivity implements
xmppConnectionService.refreshAllPresences();
}
} else if (name.equals("dont_trust_system_cas")) {
- xmppConnectionService.updateMemorizingTrustmanager();
+ ConversationsPlusApplication.updateMemorizingTrustmanager();
reconnectAccounts();
} else if ("parse_emoticons".equals(name)) {
EmojiconHandler.setParseEmoticons(Settings.PARSE_EMOTICONS);
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
index 0567de06..4ea7d7cf 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java
@@ -91,7 +91,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
convName.setTypeface(null, Typeface.NORMAL);
}
- if (message.getFileParams().width > 0
+ if ((null != message.getFileParams() && message.getFileParams().getWidth() > 0) // TODO: Use FileParams.getMimeType()
&& (message.getTransferable() == null
|| message.getTransferable().getStatus() != Transferable.STATUS_DELETED)) {
mLastMessage.setVisibility(View.GONE);
diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
index 9d2917d5..509e3b9b 100644
--- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
+++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java
@@ -20,6 +20,7 @@ import android.text.style.StyleSpan;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Patterns;
+import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
@@ -41,16 +42,19 @@ import java.util.regex.Pattern;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusColors;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
-import eu.siacs.conversations.providers.ConversationsPlusFileProvider;
+import de.thedevstack.conversationsplus.entities.FileParams;
+import de.thedevstack.conversationsplus.enums.FileStatus;
+import de.thedevstack.conversationsplus.utils.MessageUtil;
+
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
-import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
+import eu.siacs.conversations.providers.ConversationsPlusFileProvider;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.utils.CryptoHelper;
@@ -138,13 +142,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
&& message.getStatus() <= Message.STATUS_RECEIVED;
- if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) {
+ if (message.hasFileAttached() || message.getTransferable() != null) {
FileParams params = message.getFileParams();
- if (params.size > (1.5 * 1024 * 1024)) {
- filesize = params.size / (1024 * 1024)+ " MiB";
- } else if (params.size > 0) {
- filesize = params.size / 1024 + " KiB";
- }
+ if (null != params) {
+ long size = params.getSize();
+ if (size > (1.5 * 1024 * 1024)) {
+ filesize = size / (1024 * 1024)+ " MiB";
+ } else if (size > 0) {
+ filesize = size / 1024 + " KiB";
+ }
+ }
+
if (message.getTransferable() != null && message.getTransferable().getStatus() == Transferable.STATUS_FAILED) {
error = true;
}
@@ -247,6 +255,25 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.time.setText(formatedTime);
}
}
+
+ 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) {
+ viewHolder.remoteFileStatus.setVisibility(View.VISIBLE);
+ viewHolder.remoteFileStatus.setTypeface(null, Typeface.ITALIC);
+ switch (fileStatus) {
+ case DELETE_FAILED:
+ viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_failed);
+ break;
+ case DELETED:
+ viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_success);
+ break;
+ case DELETING:
+ viewHolder.remoteFileStatus.setText(R.string.remote_filestatus_delete_inprogress);
+ break;
+ }
+ }
+ }
}
private void displayInfoMessage(ViewHolder viewHolder, String text, boolean darkBackground) {
@@ -434,9 +461,10 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
viewHolder.messageBody.setVisibility(View.GONE);
viewHolder.image.setVisibility(View.VISIBLE);
- FileParams params = message.getFileParams();
//TODO: Check what value add the following lines have (compared with setting height/width in XmppActivity.loadBitmap from thumbnail after thumbnail is created)
- /*double target = metrics.density * 288;
+ /*
+ FileParams params = message.getFileParams();
+ double target = metrics.density * 288;
int scalledW;
int scalledH;
if (params.width <= params.height) {
@@ -491,6 +519,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
.findViewById(R.id.message_time);
viewHolder.indicatorReceived = (ImageView) view
.findViewById(R.id.indicator_received);
+ viewHolder.remoteFileStatus = (TextView) view.findViewById(R.id.remote_file_status);
break;
case RECEIVED:
view = activity.getLayoutInflater().inflate(
@@ -585,7 +614,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
displayImageMessage(viewHolder, message);
} else if (message.getType() == Message.TYPE_FILE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
- if (message.getFileParams().width > 0) {
+ if (message.getFileParams() != null && message.getFileParams().getWidth() > 0) { // TODO Use FileParams.getMimeType()
displayImageMessage(viewHolder,message);
} else {
displayOpenableMessage(viewHolder, message);
@@ -612,12 +641,20 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
displayDecryptionFailed(viewHolder,darkBackground);
- } else {
+ } else if (message.hasFileAttached()) { // TODO: Move to a single block with Message.TYPE_FILE OR Message.TYPE_IMAGE
+ FileParams fileParams = message.getFileParams();
+ String mimeType = (null != fileParams) ? fileParams.getMimeType() : null;
+ if (null != mimeType && mimeType.startsWith("image/")) {
+ displayImageMessage(viewHolder, message);
+ } else {
+ displayOpenableMessage(viewHolder, message);
+ }
+ } else {
if (GeoHelper.isGeoUri(message.getBody())) {
displayLocationMessage(viewHolder,message);
- } else if (message.treatAsDownloadable() == Message.Decision.MUST) {
+ } else if (MessageUtil.needsDownload(message)) {
try {
- URL url = new URL(message.getBody());
+ URL url = new URL(message.getFileParams().getUrl());
displayDownloadableMessage(viewHolder,
message,
activity.getString(R.string.check_x_filesize_on_host,
@@ -724,7 +761,8 @@ public class MessageAdapter extends ArrayAdapter<Message> {
protected ImageView contact_picture;
protected TextView status_message;
protected TextView encryption;
- }
+ public TextView remoteFileStatus;
+ }
class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
index 1568eb8c..9caa53f7 100644
--- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
+++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java
@@ -135,15 +135,20 @@ public class DNSHelper {
Logging.d("dns", "using dns server: " + dnsServerHostAddress + " to look up " + qname);
try {
DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServerHostAddress);
- Record[] rrset = message.getAnswers();
- for (Record rr : rrset) {
- Data d = rr.getPayload();
- if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
- SRV srv = (SRV) d;
- SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord);
- result.add(srvRecord);
+ if (null != message) {
+ Record[] rrset = message.getAnswers();
+ for (Record rr : rrset) {
+ Data d = rr.getPayload();
+ if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
+ SRV srv = (SRV) d;
+ SrvRecord srvRecord = new SrvRecord(srv.getPriority(), srv.getName(), srv.getPort(), tlsSrvRecord);
+ result.add(srvRecord);
+ }
}
+ } else {
+ Logging.e("dns", "No valid DNS message retrieved.");
}
+
} catch (IOException e) {
Logging.d("dns", "Error while retrieving SRV record '" + qname + "' for '" + host + "' from DNS '" + dnsServerHostAddress + "': " + e.getMessage());
}
diff --git a/src/main/java/eu/siacs/conversations/utils/FileUtils.java b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
index 1f2a71ca..18014894 100644
--- a/src/main/java/eu/siacs/conversations/utils/FileUtils.java
+++ b/src/main/java/eu/siacs/conversations/utils/FileUtils.java
@@ -17,6 +17,8 @@ import java.util.List;
import de.thedevstack.conversationsplus.ConversationsPlusApplication;
+import eu.siacs.conversations.entities.Transferable;
+
public final class FileUtils {
/**
@@ -160,6 +162,27 @@ public final class FileUtils {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
+ public static String getRelevantExtension(String path) {
+ if (path == null || path.isEmpty()) {
+ return null;
+ }
+
+ String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
+
+ final String lastPart = FileUtils.getLastExtension(filename);
+
+ if (!lastPart.isEmpty()) {
+ // we want the real file extension, not the crypto one
+ final String secondToLastPart = FileUtils.getSecondToLastExtension(filename);
+ if (!secondToLastPart.isEmpty() && Transferable.VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
+ return secondToLastPart;
+ } else {
+ return lastPart;
+ }
+ }
+ return null;
+ }
+
/**
* @param filename The filename to extract extension from
* @return last extension or empty string
diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
index a45cce01..a5d0321d 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -2,9 +2,6 @@ package eu.siacs.conversations.xmpp;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.security.KeyChain;
@@ -53,6 +50,7 @@ import javax.net.ssl.X509TrustManager;
import de.duenndns.ssl.MemorizingTrustManager;
import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.dto.SrvRecord;
import eu.siacs.conversations.Config;
@@ -203,8 +201,7 @@ public class XmppConnection implements Runnable {
public XmppConnection(final Account account, final XmppConnectionService service) {
this.account = account;
- this.wakeLock = service.getPowerManager().newWakeLock(
- PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
+ this.wakeLock = ConversationsPlusApplication.createPartialWakeLock(account.getJid().toBareJid().toString());
tagWriter = new TagWriter();
mXmppConnectionService = service;
}
@@ -386,14 +383,14 @@ public class XmppConnection implements Runnable {
private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException {
final SSLContext sc = SSLSocketHelper.getSSLContext();
- MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager();
+ MemorizingTrustManager trustManager = ConversationsPlusApplication.getMemorizingTrustManager();
KeyManager[] keyManager;
if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) {
keyManager = new KeyManager[]{mKeyManager};
} else {
keyManager = null;
}
- sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
+ sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, ConversationsPlusApplication.getSecureRandom());
final SSLSocketFactory factory = sc.getSocketFactory();
final HostnameVerifier verifier;
if (mInteractive) {
@@ -741,13 +738,13 @@ public class XmppConnection implements Runnable {
final Element auth = new Element("auth");
auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
- saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
+ saslMechanism = new External(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
} else if (mechanisms.contains("SCRAM-SHA-1")) {
- saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
+ saslMechanism = new ScramSha1(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
} else if (mechanisms.contains("PLAIN")) {
saslMechanism = new Plain(tagWriter, account);
} else if (mechanisms.contains("DIGEST-MD5")) {
- saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
+ saslMechanism = new DigestMd5(tagWriter, account, ConversationsPlusApplication.getSecureRandom());
}
if (saslMechanism != null) {
final JSONObject keys = account.getKeys();
@@ -1175,7 +1172,7 @@ public class XmppConnection implements Runnable {
}
private String nextRandomId() {
- return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
+ return new BigInteger(50, ConversationsPlusApplication.getSecureRandom()).toString(32);
}
public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
index f0431869..fca0597f 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java
@@ -1,14 +1,11 @@
package eu.siacs.conversations.xmpp.jingle;
-import android.content.Intent;
-import android.net.Uri;
import android.util.Pair;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
@@ -19,6 +16,8 @@ import de.thedevstack.android.logcat.Logging;
import de.thedevstack.conversationsplus.ConversationsPlusPreferences;
import de.thedevstack.conversationsplus.utils.MessageUtil;
import de.thedevstack.conversationsplus.utils.StreamUtil;
+import de.thedevstack.conversationsplus.utils.XmppConnectionServiceAccessor;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
@@ -157,8 +156,7 @@ public class JingleConnection implements Transferable {
public JingleConnection(JingleConnectionManager mJingleConnectionManager) {
this.mJingleConnectionManager = mJingleConnectionManager;
- this.mXmppConnectionService = mJingleConnectionManager
- .getXmppConnectionService();
+ this.mXmppConnectionService = XmppConnectionServiceAccessor.xmppConnectionService;
}
public String getSessionId() {
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
index c7865292..19ac354d 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java
@@ -33,10 +33,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
@SuppressLint("TrulyRandom")
private SecureRandom random = new SecureRandom();
- public JingleConnectionManager(XmppConnectionService service) {
- super(service);
- }
-
public void deliverPacket(Account account, JinglePacket packet) {
if (packet.isAction("session-initiate")) {
JingleConnection connection = new JingleConnection(this);
diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
index 76cd0c87..318a35f7 100644
--- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
+++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java
@@ -14,6 +14,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import de.thedevstack.android.logcat.Logging;
+import de.thedevstack.conversationsplus.ConversationsPlusApplication;
import de.thedevstack.conversationsplus.utils.StreamUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile;
@@ -84,7 +85,7 @@ public class JingleSocks5Transport extends JingleTransport {
@Override
public void run() {
InputStream fileInputStream = null;
- final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_"+connection.getSessionId());
+ final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_send_"+connection.getSessionId());
try {
wakeLock.acquire();
MessageDigest digest = MessageDigest.getInstance("SHA-1");
@@ -134,7 +135,7 @@ public class JingleSocks5Transport extends JingleTransport {
@Override
public void run() {
OutputStream fileOutputStream = null;
- final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_"+connection.getSessionId());
+ final PowerManager.WakeLock wakeLock = ConversationsPlusApplication.createPartialWakeLock("jingle_receive_"+connection.getSessionId());
try {
wakeLock.acquire();
MessageDigest digest = MessageDigest.getInstance("SHA-1");
diff --git a/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml
index 0edbab51..30003309 100644
--- a/src/main/res/layout/message_sent.xml
+++ b/src/main/res/layout/message_sent.xml
@@ -103,6 +103,18 @@
android:gravity="center_vertical"
android:src="@drawable/ic_received_indicator" />
</LinearLayout>
+
+ <TextView
+ android:id="@+id/remote_file_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:gravity="right"
+ android:text=""
+ android:textColor="@color/secondaryText"
+ android:textSize="?attr/TextSizeInfo"
+ android:visibility="gone"
+ android:textStyle="italic"/>
</LinearLayout>
</LinearLayout>
diff --git a/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml
index 0c7d8eef..da7f0636 100644
--- a/src/main/res/menu/message_context.xml
+++ b/src/main/res/menu/message_context.xml
@@ -36,4 +36,8 @@
android:id="@+id/delete_file"
android:title="@string/delete_x_file"
android:visible="false"/>
+ <item
+ android:id="@+id/msg_ctx_menu_delete_remote_file"
+ android:title="@string/delete_remote_file_x"
+ android:visible="false"/>
</menu> \ No newline at end of file
diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml
index b8729940..02a9c9ee 100644
--- a/src/main/res/values-de/strings.xml
+++ b/src/main/res/values-de/strings.xml
@@ -634,4 +634,5 @@
<string name="no_keys_just_confirm">Du vertraust diesem Kontakt bereits. Durch Auswählen von \"Fertig\" bestätigst du, dass %s Teil dieser Konferenz ist.</string>
<string name="select_image_and_crop">Bild auswählen und zuschneiden</string>
<string name="this_account_is_disabled">Du hast diesen Account deaktiviert</string>
+ <string name="cplus_are_you_sure">Bist du dir sicher?</string>
</resources>
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 77e4fc8c..16494044 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -674,4 +674,9 @@
<string name="security_error_invalid_file_access">Security error: Invalid file access</string>
<string name="no_application_to_share_uri">No application found to share URI</string>
<string name="share_uri_with">Share URI with…</string>
+ <string name="delete_remote_file_x">Delete remote file</string>
+ <string name="cplus_are_you_sure">Are you sure?</string>
+ <string name="remote_filestatus_delete_failed">Failed to delete remote file</string>
+ <string name="remote_filestatus_delete_success">Remote file deleted</string>
+ <string name="remote_filestatus_delete_inprogress">Deleting remote file...</string>
</resources>