diff options
Diffstat (limited to 'src/main/java/de/thedevstack')
62 files changed, 2898 insertions, 141 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)); + MessageUtil.updateMessageWithFileParams(message, url, file.getSize(), imageWidth, imageHeight); } - private static void updateMessageBodyWithImageParams(Message message, long size, int imageWidth, int imageHeight) { - MessageUtil.updateMessageBodyWithImageParams(message, null, size, imageWidth, imageHeight); + private static void updateMessageWithFileParams(Message message, URL url, long size, int imageWidth, int imageHeight) { + FileParams fileParams = message.getFileParams(); + if (null == fileParams) { + fileParams = new FileParams(); } - - 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(); + 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 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 class PubSubPacketParser { 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() { + + } +} |