Merge branch 'feature/file_transfer' into development

iNPUTmice created

Conflicts:
	src/main/res/values/strings.xml

Change summary

src/main/java/eu/siacs/conversations/Config.java                              |   3 
src/main/java/eu/siacs/conversations/crypto/PgpEngine.java                    |  27 
src/main/java/eu/siacs/conversations/entities/Downloadable.java               |   9 
src/main/java/eu/siacs/conversations/entities/DownloadableFile.java           |  14 
src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java    |  39 
src/main/java/eu/siacs/conversations/entities/Message.java                    |  25 
src/main/java/eu/siacs/conversations/http/HttpConnection.java                 |  34 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java         |   7 
src/main/java/eu/siacs/conversations/persistance/FileBackend.java             | 134 
src/main/java/eu/siacs/conversations/services/NotificationService.java        |  12 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      |  99 
src/main/java/eu/siacs/conversations/ui/ConversationActivity.java             |  77 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java             |  27 
src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java      |  36 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java           | 165 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java        | 235 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java |   4 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java   |  56 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java   |  22 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleTransport.java         |   2 
src/main/res/menu/attachment_choices.xml                                      |   3 
src/main/res/menu/message_context.xml                                         |   3 
src/main/res/values/strings.xml                                               |  14 
23 files changed, 772 insertions(+), 275 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/Config.java πŸ”—

@@ -19,6 +19,9 @@ public final class Config {
 	public static final int MESSAGE_MERGE_WINDOW = 20;
 
 	public static final boolean PARSE_EMOTICONS = false;
+	public static final int  PROGRESS_UI_UPDATE_INTERVAL = 750;
+
+	public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb
 
 	private Config() {
 

src/main/java/eu/siacs/conversations/crypto/PgpEngine.java πŸ”—

@@ -3,7 +3,6 @@ package eu.siacs.conversations.crypto;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -24,7 +23,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.ui.UiCallback;
 import android.app.PendingIntent;
 import android.content.Intent;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
 
 public class PgpEngine {
@@ -80,12 +78,13 @@ public class PgpEngine {
                     }
 				}
 			});
-		} else if (message.getType() == Message.TYPE_IMAGE) {
+		} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 			try {
 				final DownloadableFile inputFile = this.mXmppConnectionService
 						.getFileBackend().getFile(message, false);
 				final DownloadableFile outputFile = this.mXmppConnectionService
 						.getFileBackend().getFile(message, true);
+				outputFile.getParentFile().mkdirs();
 				outputFile.createNewFile();
 				InputStream is = new FileInputStream(inputFile);
 				OutputStream os = new FileOutputStream(outputFile);
@@ -97,24 +96,7 @@ public class PgpEngine {
 								OpenPgpApi.RESULT_CODE_ERROR)) {
 						case OpenPgpApi.RESULT_CODE_SUCCESS:
 							URL url = message.getImageParams().url;
-							BitmapFactory.Options options = new BitmapFactory.Options();
-							options.inJustDecodeBounds = true;
-							BitmapFactory.decodeFile(
-									outputFile.getAbsolutePath(), options);
-							int imageHeight = options.outHeight;
-							int imageWidth = options.outWidth;
-							if (url == null) {
-								message.setBody(Long.toString(outputFile
-										.getSize())
-										+ '|'
-										+ imageWidth
-										+ '|'
-										+ imageHeight);
-							} else {
-								message.setBody(url.toString() + "|"
-										+ Long.toString(outputFile.getSize())
-										+ '|' + imageWidth + '|' + imageHeight);
-							}
+							mXmppConnectionService.getFileBackend().updateFileParams(message,url);
 							message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 							PgpEngine.this.mXmppConnectionService
 									.updateMessage(message);
@@ -199,12 +181,13 @@ public class PgpEngine {
 					}
 				}
 			});
-		} else if (message.getType() == Message.TYPE_IMAGE) {
+		} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 			try {
 				DownloadableFile inputFile = this.mXmppConnectionService
 						.getFileBackend().getFile(message, true);
 				DownloadableFile outputFile = this.mXmppConnectionService
 						.getFileBackend().getFile(message, false);
+				outputFile.getParentFile().mkdirs();
 				outputFile.createNewFile();
 				InputStream is = new FileInputStream(inputFile);
 				OutputStream os = new FileOutputStream(outputFile);

src/main/java/eu/siacs/conversations/entities/Downloadable.java πŸ”—

@@ -2,7 +2,7 @@ package eu.siacs.conversations.entities;
 
 public interface Downloadable {
 
-	public final String[] VALID_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"};
+	public final String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"};
 	public final String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"};
 
 	public static final int STATUS_UNKNOWN = 0x200;
@@ -12,10 +12,17 @@ public interface Downloadable {
 	public static final int STATUS_DOWNLOADING = 0x204;
 	public static final int STATUS_DELETED = 0x205;
 	public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+	public static final int STATUS_UPLOADING = 0x207;
 
 	public boolean start();
 
 	public int getStatus();
 
 	public long getFileSize();
+
+	public int getProgress();
+
+	public String getMimeType();
+
+	public void cancel();
 }

src/main/java/eu/siacs/conversations/entities/DownloadableFile.java πŸ”—

@@ -6,6 +6,7 @@ import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.URLConnection;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.Key;
@@ -28,6 +29,7 @@ public class DownloadableFile extends File {
 	private long expectedSize = 0;
 	private String sha1sum;
 	private Key aeskey;
+	private String mime;
 
 	private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
 			0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
@@ -52,6 +54,18 @@ public class DownloadableFile extends File {
 		}
 	}
 
+	public String getMimeType() {
+		String path = this.getAbsolutePath();
+		String mime = URLConnection.guessContentTypeFromName(path);
+		if (mime != null) {
+			return mime;
+		} else if (mime == null && path.endsWith(".webp")) {
+			return "image/webp";
+		} else {
+			return "";
+		}
+	}
+
 	public void setExpectedSize(long size) {
 		this.expectedSize = size;
 	}

src/main/java/eu/siacs/conversations/entities/DownloadablePlaceholder.java πŸ”—

@@ -0,0 +1,39 @@
+package eu.siacs.conversations.entities;
+
+public class DownloadablePlaceholder implements Downloadable {
+
+	private int status;
+
+	public DownloadablePlaceholder(int status) {
+		this.status = status;
+	}
+	@Override
+	public boolean start() {
+		return false;
+	}
+
+	@Override
+	public int getStatus() {
+		return status;
+	}
+
+	@Override
+	public long getFileSize() {
+		return 0;
+	}
+
+	@Override
+	public int getProgress() {
+		return 0;
+	}
+
+	@Override
+	public String getMimeType() {
+		return "";
+	}
+
+	@Override
+	public void cancel() {
+
+	}
+}

src/main/java/eu/siacs/conversations/entities/Message.java πŸ”—

@@ -32,7 +32,7 @@ public class Message extends AbstractEntity {
 
 	public static final int TYPE_TEXT = 0;
 	public static final int TYPE_IMAGE = 1;
-	public static final int TYPE_AUDIO = 2;
+	public static final int TYPE_FILE = 2;
 	public static final int TYPE_STATUS = 3;
 	public static final int TYPE_PRIVATE = 4;
 
@@ -45,6 +45,7 @@ public class Message extends AbstractEntity {
 	public static String STATUS = "status";
 	public static String TYPE = "type";
 	public static String REMOTE_MSG_ID = "remoteMsgId";
+	public static String RELATIVE_FILE_PATH = "relativeFilePath";
 	public boolean markable = false;
 	protected String conversationUuid;
 	protected Jid counterpart;
@@ -55,6 +56,7 @@ public class Message extends AbstractEntity {
 	protected int encryption;
 	protected int status;
 	protected int type;
+	protected String relativeFilePath;
 	protected boolean read = true;
 	protected String remoteMsgId = null;
 	protected Conversation conversation = null;
@@ -74,13 +76,13 @@ public class Message extends AbstractEntity {
 		this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
 				conversation.getContactJid().toBareJid(), null, body, System
 						.currentTimeMillis(), encryption,
-				status, TYPE_TEXT, null);
+				status, TYPE_TEXT, null,null);
 		this.conversation = conversation;
 	}
 
 	public Message(final String uuid, final String conversationUUid, final Jid counterpart,
 				   final String trueCounterpart, final String body, final long timeSent,
-				   final int encryption, final int status, final int type, final String remoteMsgId) {
+				   final int encryption, final int status, final int type, final String remoteMsgId, final String relativeFilePath) {
 		this.uuid = uuid;
 		this.conversationUuid = conversationUUid;
 		this.counterpart = counterpart;
@@ -91,6 +93,7 @@ public class Message extends AbstractEntity {
 		this.status = status;
 		this.type = type;
 		this.remoteMsgId = remoteMsgId;
+		this.relativeFilePath = relativeFilePath;
 	}
 
 	public static Message fromCursor(Cursor cursor) {
@@ -114,7 +117,8 @@ public class Message extends AbstractEntity {
 				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
 				cursor.getInt(cursor.getColumnIndex(STATUS)),
 				cursor.getInt(cursor.getColumnIndex(TYPE)),
-				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)));
+				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
+				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)));
 	}
 
 	public static Message createStatusMessage(Conversation conversation) {
@@ -141,6 +145,7 @@ public class Message extends AbstractEntity {
 		values.put(STATUS, status);
 		values.put(TYPE, type);
 		values.put(REMOTE_MSG_ID, remoteMsgId);
+		values.put(RELATIVE_FILE_PATH, relativeFilePath);
 		return values;
 	}
 
@@ -205,6 +210,14 @@ public class Message extends AbstractEntity {
 		this.status = status;
 	}
 
+	public void setRelativeFilePath(String path) {
+		this.relativeFilePath = path;
+	}
+
+	public String getRelativeFilePath() {
+		return  this.relativeFilePath;
+	}
+
 	public String getRemoteMsgId() {
 		return this.remoteMsgId;
 	}
@@ -376,14 +389,14 @@ public class Message extends AbstractEntity {
 			}
 			String[] extensionParts = filename.split("\\.");
 			if (extensionParts.length == 2
-					&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+					&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
 					extensionParts[extensionParts.length - 1])) {
 				return true;
 			} else if (extensionParts.length == 3
 					&& Arrays
 					.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
 					.contains(extensionParts[extensionParts.length - 1])
-					&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
+					&& Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains(
 					extensionParts[extensionParts.length - 2])) {
 				return true;
 			} else {

src/main/java/eu/siacs/conversations/http/HttpConnection.java πŸ”—

@@ -3,6 +3,7 @@ package eu.siacs.conversations.http;
 import android.content.Intent;
 import android.graphics.BitmapFactory;
 import android.net.Uri;
+import android.os.SystemClock;
 
 import org.apache.http.conn.ssl.StrictHostnameVerifier;
 
@@ -21,6 +22,7 @@ import javax.net.ssl.SSLContext;
 import javax.net.ssl.SSLHandshakeException;
 import javax.net.ssl.X509TrustManager;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Downloadable;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
@@ -37,6 +39,8 @@ public class HttpConnection implements Downloadable {
 	private DownloadableFile file;
 	private int mStatus = Downloadable.STATUS_UNKNOWN;
 	private boolean acceptedAutomatically = false;
+	private int mProgress = 0;
+	private long mLastGuiRefresh = 0;
 
 	public HttpConnection(HttpConnectionManager manager) {
 		this.mHttpConnectionManager = manager;
@@ -235,10 +239,14 @@ public class HttpConnection implements Downloadable {
 			if (os == null) {
 				throw new IOException();
 			}
+			long transmitted = 0;
+			long expected = file.getExpectedSize();
 			int count = -1;
 			byte[] buffer = new byte[1024];
 			while ((count = is.read(buffer)) != -1) {
+				transmitted += count;
 				os.write(buffer, 0, count);
+				updateProgress((int) ((((double) transmitted) / expected) * 100));
 			}
 			os.flush();
 			os.close();
@@ -246,19 +254,21 @@ public class HttpConnection implements Downloadable {
 		}
 
 		private void updateImageBounds() {
-			BitmapFactory.Options options = new BitmapFactory.Options();
-			options.inJustDecodeBounds = true;
-			BitmapFactory.decodeFile(file.getAbsolutePath(), options);
-			int imageHeight = options.outHeight;
-			int imageWidth = options.outWidth;
-			message.setBody(mUrl.toString() + "|" + file.getSize() + '|'
-					+ imageWidth + '|' + imageHeight);
 			message.setType(Message.TYPE_IMAGE);
+			mXmppConnectionService.getFileBackend().updateFileParams(message,mUrl);
 			mXmppConnectionService.updateMessage(message);
 		}
 
 	}
 
+	public void updateProgress(int i) {
+		this.mProgress = i;
+		if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
+			this.mLastGuiRefresh = SystemClock.elapsedRealtime();
+			mXmppConnectionService.updateConversationUi();
+		}
+	}
+
 	@Override
 	public int getStatus() {
 		return this.mStatus;
@@ -272,4 +282,14 @@ public class HttpConnection implements Downloadable {
 			return 0;
 		}
 	}
+
+	@Override
+	public int getProgress() {
+		return this.mProgress;
+	}
+
+	@Override
+	public String getMimeType() {
+		return "";
+	}
 }

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java πŸ”—

@@ -22,7 +22,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 	private static DatabaseBackend instance = null;
 
 	private static final String DATABASE_NAME = "history";
-	private static final int DATABASE_VERSION = 9;
+	private static final int DATABASE_VERSION = 10;
 
 	private static String CREATE_CONTATCS_STATEMENT = "create table "
 			+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@@ -64,6 +64,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 				+ " TEXT, " + Message.TRUE_COUNTERPART + " TEXT,"
 				+ Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, "
 				+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+				+ Message.RELATIVE_FILE_PATH + " TEXT, "
 				+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
 				+ Message.CONVERSATION + ") REFERENCES "
 				+ Conversation.TABLENAME + "(" + Conversation.UUID
@@ -110,6 +111,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
             db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN "
                     + Contact.LAST_PRESENCE + " TEXT");
         }
+		if (oldVersion < 10 && newVersion >= 10) {
+			db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+					+ Message.RELATIVE_FILE_PATH + " TEXT");
+		}
 	}
 
 	public static synchronized DatabaseBackend getInstance(Context context) {

src/main/java/eu/siacs/conversations/persistance/FileBackend.java πŸ”—

@@ -7,6 +7,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.net.URL;
 import java.security.DigestOutputStream;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -26,6 +27,8 @@ import android.provider.MediaStore;
 import android.util.Base64;
 import android.util.Base64OutputStream;
 import android.util.Log;
+import android.webkit.MimeTypeMap;
+
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.DownloadableFile;
@@ -53,25 +56,40 @@ public class FileBackend {
 	}
 
 	public DownloadableFile getFile(Message message, boolean decrypted) {
-		StringBuilder filename = new StringBuilder();
-		filename.append(getConversationsDirectory());
-		filename.append(message.getUuid());
-		if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
-			filename.append(".webp");
-		} else {
-			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
-				filename.append(".webp");
+		String path = message.getRelativeFilePath();
+		if (!decrypted && (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED)) {
+			String extension;
+			if (path != null && !path.isEmpty()) {
+				String[] parts = path.split("\\.");
+				extension = "."+parts[parts.length - 1];
+			} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_TEXT) {
+				extension = ".webp";
 			} else {
-				filename.append(".webp.pgp");
+				extension = "";
 			}
+			return new DownloadableFile(getConversationsFileDirectory()+message.getUuid()+extension+".pgp");
+		} else if (path != null && !path.isEmpty()) {
+			if (path.startsWith("/")) {
+				return new DownloadableFile(path);
+			} else {
+				return new DownloadableFile(getConversationsFileDirectory()+path);
+			}
+		} else {
+			StringBuilder filename = new StringBuilder();
+			filename.append(getConversationsImageDirectory());
+			filename.append(message.getUuid()+".webp");
+			return new DownloadableFile(filename.toString());
 		}
-		return new DownloadableFile(filename.toString());
 	}
 
-	public static String getConversationsDirectory() {
+	public static String getConversationsFileDirectory() {
+		return  Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
+	}
+
+	public static String getConversationsImageDirectory() {
 		return Environment.getExternalStoragePublicDirectory(
-				Environment.DIRECTORY_PICTURES).getAbsolutePath()
-				+ "/Conversations/";
+			Environment.DIRECTORY_PICTURES).getAbsolutePath()
+			+ "/Conversations/";
 	}
 
 	public Bitmap resize(Bitmap originalBitmap, int size) {
@@ -103,13 +121,60 @@ public class FileBackend {
 		return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
 	}
 
+	public String getOriginalPath(Uri uri) {
+		String path = null;
+		if (uri.getScheme().equals("file")) {
+			return uri.getPath();
+		} else if (uri.toString().startsWith("content://media/")) {
+			String[] projection = {MediaStore.MediaColumns.DATA};
+			Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri,
+					projection, null, null, null);
+			if (metaCursor != null) {
+				try {
+					if (metaCursor.moveToFirst()) {
+						path = metaCursor.getString(0);
+					}
+				} finally {
+					metaCursor.close();
+				}
+			}
+		}
+		return path;
+	}
+
+	public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
+		try {
+			Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
+			String mime = mXmppConnectionService.getContentResolver().getType(uri);
+			String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
+			message.setRelativeFilePath(message.getUuid() + "." + extension);
+			DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
+			OutputStream os = new FileOutputStream(file);
+			InputStream is = mXmppConnectionService.getContentResolver().openInputStream(uri);
+			byte[] buffer = new byte[1024];
+            int length;
+            while ((length = is.read(buffer)) > 0) {
+				os.write(buffer, 0, length);
+            }
+			os.flush();
+			os.close();
+			is.close();
+			Log.d(Config.LOGTAG, "output file name " + mXmppConnectionService.getFileBackend().getFile(message));
+			return file;
+		} catch (FileNotFoundException e) {
+			throw new FileCopyException(R.string.error_file_not_found);
+		} catch (IOException e) {
+			throw new FileCopyException(R.string.error_io_exception);
+		}
+	}
+
 	public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
-			throws ImageCopyException {
+			throws FileCopyException {
 		return this.copyImageToPrivateStorage(message, image, 0);
 	}
 
 	private DownloadableFile copyImageToPrivateStorage(Message message,
-			Uri image, int sampleSize) throws ImageCopyException {
+			Uri image, int sampleSize) throws FileCopyException {
 		try {
 			InputStream is = mXmppConnectionService.getContentResolver()
 					.openInputStream(image);
@@ -125,7 +190,7 @@ public class FileBackend {
 			originalBitmap = BitmapFactory.decodeStream(is, null, options);
 			is.close();
 			if (originalBitmap == null) {
-				throw new ImageCopyException(R.string.error_not_an_image_file);
+				throw new FileCopyException(R.string.error_not_an_image_file);
 			}
 			Bitmap scalledBitmap = resize(originalBitmap, IMAGE_SIZE);
 			originalBitmap = null;
@@ -137,7 +202,7 @@ public class FileBackend {
 			boolean success = scalledBitmap.compress(
 					Bitmap.CompressFormat.WEBP, 75, os);
 			if (!success) {
-				throw new ImageCopyException(R.string.error_compressing_image);
+				throw new FileCopyException(R.string.error_compressing_image);
 			}
 			os.flush();
 			os.close();
@@ -147,18 +212,18 @@ public class FileBackend {
 			message.setBody(Long.toString(size) + ',' + width + ',' + height);
 			return file;
 		} catch (FileNotFoundException e) {
-			throw new ImageCopyException(R.string.error_file_not_found);
+			throw new FileCopyException(R.string.error_file_not_found);
 		} catch (IOException e) {
-			throw new ImageCopyException(R.string.error_io_exception);
+			throw new FileCopyException(R.string.error_io_exception);
 		} catch (SecurityException e) {
-			throw new ImageCopyException(
+			throw new FileCopyException(
 					R.string.error_security_exception_during_image_copy);
 		} catch (OutOfMemoryError e) {
 			++sampleSize;
 			if (sampleSize <= 3) {
 				return copyImageToPrivateStorage(message, image, sampleSize);
 			} else {
-				throw new ImageCopyException(R.string.error_out_of_memory);
+				throw new FileCopyException(R.string.error_out_of_memory);
 			}
 		}
 	}
@@ -400,11 +465,34 @@ public class FileBackend {
 		return Uri.parse("file://" + file.getAbsolutePath());
 	}
 
-	public class ImageCopyException extends Exception {
+	public void updateFileParams(Message message) {
+		updateFileParams(message,null);
+	}
+
+	public void updateFileParams(Message message, URL url) {
+		DownloadableFile file = getFile(message);
+		if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
+			BitmapFactory.Options options = new BitmapFactory.Options();
+			options.inJustDecodeBounds = true;
+			BitmapFactory.decodeFile(file.getAbsolutePath(), options);
+			int imageHeight = options.outHeight;
+			int imageWidth = options.outWidth;
+			if (url == null) {
+				message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
+			} else {
+				message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
+			}
+		} else {
+			message.setBody(Long.toString(file.getSize()));
+		}
+
+	}
+
+	public class FileCopyException extends Exception {
 		private static final long serialVersionUID = -1010013599132881427L;
 		private int resId;
 
-		public ImageCopyException(int resId) {
+		public FileCopyException(int resId) {
 			this.resId = resId;
 		}
 

src/main/java/eu/siacs/conversations/services/NotificationService.java πŸ”—

@@ -28,6 +28,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.ui.ConversationActivity;
 
@@ -267,14 +268,21 @@ public class NotificationService {
 		if (message.getDownloadable() != null
 				&& (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message
 				.getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) {
-			return mXmppConnectionService.getText(
-					R.string.image_offered_for_download).toString();
+			if (message.getType() == Message.TYPE_FILE) {
+				return mXmppConnectionService.getString(R.string.file_offered_for_download);
+			} else {
+				return mXmppConnectionService.getText(
+						R.string.image_offered_for_download).toString();
+			}
 		} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 			return mXmppConnectionService.getText(
 					R.string.encrypted_message_received).toString();
 		} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
 			return mXmppConnectionService.getText(R.string.decryption_failed)
 					.toString();
+		} else if (message.getType() == Message.TYPE_FILE) {
+			DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message);
+			return mXmppConnectionService.getString(R.string.file,file.getMimeType());
 		} else if (message.getType() == Message.TYPE_IMAGE) {
 			return mXmppConnectionService.getText(R.string.image_file)
 					.toString();

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java πŸ”—

@@ -56,6 +56,8 @@ import eu.siacs.conversations.entities.Bookmark;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.DownloadablePlaceholder;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
@@ -211,7 +213,7 @@ public class XmppConnectionService extends Service {
 	private Integer rosterChangedListenerCount = 0;
 	private SecureRandom mRandom;
 	private FileObserver fileObserver = new FileObserver(
-			FileBackend.getConversationsDirectory()) {
+			FileBackend.getConversationsImageDirectory()) {
 
 		@Override
 		public void onEvent(int event, String path) {
@@ -295,7 +297,49 @@ public class XmppConnectionService extends Service {
 		return this.mAvatarService;
 	}
 
-	public Message attachImageToConversation(final Conversation conversation,
+	public void attachFileToConversation(Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
+		final Message message;
+		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
+			message = new Message(conversation, "",
+					Message.ENCRYPTION_DECRYPTED);
+		} else {
+			message = new Message(conversation, "",
+					conversation.getNextEncryption(forceEncryption()));
+		}
+		message.setCounterpart(conversation.getNextCounterpart());
+		message.setType(Message.TYPE_FILE);
+		message.setStatus(Message.STATUS_OFFERED);
+		String path = getFileBackend().getOriginalPath(uri);
+		if (path!=null) {
+			message.setRelativeFilePath(path);
+			getFileBackend().updateFileParams(message);
+			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+				getPgpEngine().encrypt(message, callback);
+			} else {
+				callback.success(message);
+			}
+		} else {
+			new Thread(new Runnable() {
+				@Override
+				public void run() {
+					try {
+						getFileBackend().copyFileToPrivateStorage(message, uri);
+						getFileBackend().updateFileParams(message);
+						if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+							getPgpEngine().encrypt(message, callback);
+						} else {
+							callback.success(message);
+						}
+					} catch (FileBackend.FileCopyException e) {
+						callback.error(e.getResId(),message);
+					}
+				}
+			}).start();
+
+		}
+	}
+
+	public void attachImageToConversation(final Conversation conversation,
 											 final Uri uri, final UiCallback<Message> callback) {
 		final Message message;
 		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
@@ -313,18 +357,17 @@ public class XmppConnectionService extends Service {
 			@Override
 			public void run() {
 				try {
-					getFileBackend().copyImageToPrivateStorage(message, uri);
+					DownloadableFile file = getFileBackend().copyImageToPrivateStorage(message, uri);
 					if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 						getPgpEngine().encrypt(message, callback);
 					} else {
 						callback.success(message);
 					}
-				} catch (FileBackend.ImageCopyException e) {
+				} catch (FileBackend.FileCopyException e) {
 					callback.error(e.getResId(), message);
 				}
 			}
 		}).start();
-		return message;
 	}
 
 	public Conversation find(Bookmark bookmark) {
@@ -561,7 +604,7 @@ public class XmppConnectionService extends Service {
 		boolean send = false;
 		if (account.getStatus() == Account.STATUS_ONLINE
 				&& account.getXmppConnection() != null) {
-			if (message.getType() == Message.TYPE_IMAGE) {
+			if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 				if (message.getCounterpart() != null) {
 					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 						if (!conv.hasValidOtrSession()) {
@@ -678,11 +721,16 @@ public class XmppConnectionService extends Service {
 			} else {
 				if (message.getConversation().getOtrSession()
 						.getSessionStatus() == SessionStatus.ENCRYPTED) {
-					if (message.getType() == Message.TYPE_TEXT) {
-						packet = mMessageGenerator.generateOtrChat(message,
-								true);
-					} else if (message.getType() == Message.TYPE_IMAGE) {
-						mJingleConnectionManager.createNewConnection(message);
+					try {
+						message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID()));
+						if (message.getType() == Message.TYPE_TEXT) {
+							packet = mMessageGenerator.generateOtrChat(message,
+									true);
+						} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
+							mJingleConnectionManager.createNewConnection(message);
+						}
+					} catch (InvalidJidException e) {
+
 					}
 				}
 			}
@@ -693,7 +741,7 @@ public class XmppConnectionService extends Service {
 					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
 				packet = mMessageGenerator.generatePgpChat(message, true);
 			}
-		} else if (message.getType() == Message.TYPE_IMAGE) {
+		} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 			Contact contact = message.getConversation().getContact();
 			Presences presences = contact.getPresences();
 			if ((message.getCounterpart() != null)
@@ -852,10 +900,10 @@ public class XmppConnectionService extends Service {
 
 	private void checkDeletedFiles(Conversation conversation) {
 		for (Message message : conversation.getMessages()) {
-			if (message.getType() == Message.TYPE_IMAGE
+			if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
 					&& message.getEncryption() != Message.ENCRYPTION_PGP) {
 				if (!getFileBackend().isFileAvailable(message)) {
-					message.setDownloadable(new DeletedDownloadable());
+					message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
 				}
 			}
 		}
@@ -868,7 +916,7 @@ public class XmppConnectionService extends Service {
 						&& message.getEncryption() != Message.ENCRYPTION_PGP
 						&& message.getUuid().equals(uuid)) {
 					if (!getFileBackend().isFileAvailable(message)) {
-						message.setDownloadable(new DeletedDownloadable());
+						message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
 						updateConversationUi();
 					}
 					return;
@@ -1424,7 +1472,7 @@ public class XmppConnectionService extends Service {
 						databaseBackend.updateMessage(msg);
 						sendMessagePacket(account, outPacket);
 					}
-				} else if (msg.getType() == Message.TYPE_IMAGE) {
+				} else if (msg.getType() == Message.TYPE_IMAGE || msg.getType() == Message.TYPE_FILE) {
 					mJingleConnectionManager.createNewConnection(msg);
 				}
 			}
@@ -1979,23 +2027,4 @@ public class XmppConnectionService extends Service {
 			return XmppConnectionService.this;
 		}
 	}
-
-	private class DeletedDownloadable implements Downloadable {
-
-		@Override
-		public boolean start() {
-			return false;
-		}
-
-		@Override
-		public int getStatus() {
-			return Downloadable.STATUS_DELETED;
-		}
-
-		@Override
-		public long getFileSize() {
-			return 0;
-		}
-
-	}
 }

src/main/java/eu/siacs/conversations/ui/ConversationActivity.java πŸ”—

@@ -52,13 +52,14 @@ public class ConversationActivity extends XmppActivity implements
 	public static final int REQUEST_SEND_MESSAGE = 0x0201;
 	public static final int REQUEST_DECRYPT_PGP = 0x0202;
 	public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
-	private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0203;
+	private static final int REQUEST_ATTACH_IMAGE_DIALOG = 0x0203;
 	private static final int REQUEST_IMAGE_CAPTURE = 0x0204;
 	private static final int REQUEST_RECORD_AUDIO = 0x0205;
 	private static final int REQUEST_SEND_PGP_IMAGE = 0x0206;
+	private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0208;
 	private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
 	private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
-	private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0303;
+	private static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
 	private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
 	private static final String STATE_PANEL_OPEN = "state_panel_open";
 	private static final String STATE_PENDING_URI = "state_pending_uri";
@@ -66,6 +67,7 @@ public class ConversationActivity extends XmppActivity implements
 	private String mOpenConverstaion = null;
 	private boolean mPanelOpen = true;
 	private Uri mPendingImageUri = null;
+	private Uri mPendingFileUri = null;
 
 	private View mContentView;
 
@@ -76,7 +78,7 @@ public class ConversationActivity extends XmppActivity implements
 
 	private ArrayAdapter<Conversation> listAdapter;
 
-	private Toast prepareImageToast;
+	private Toast prepareFileToast;
 
 
 	public List<Conversation> getConversationList() {
@@ -306,13 +308,18 @@ public class ConversationActivity extends XmppActivity implements
 					Intent attachFileIntent = new Intent();
 					attachFileIntent.setType("image/*");
 					attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
+					Intent chooser = Intent.createChooser(attachFileIntent,
+							getString(R.string.attach_file));
+					startActivityForResult(chooser, REQUEST_ATTACH_IMAGE_DIALOG);
+				} else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_FILE) {
+					Intent attachFileIntent = new Intent();
+					//attachFileIntent.setType("file/*");
+					attachFileIntent.setType("*/*");
+					attachFileIntent.addCategory(Intent.CATEGORY_OPENABLE);
+					attachFileIntent.setAction(Intent.ACTION_GET_CONTENT);
 					Intent chooser = Intent.createChooser(attachFileIntent,
 							getString(R.string.attach_file));
 					startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG);
-				} else if (attachmentChoice == ATTACHMENT_CHOICE_RECORD_VOICE) {
-					Intent intent = new Intent(
-							MediaStore.Audio.Media.RECORD_SOUND_ACTION);
-					startActivityForResult(intent, REQUEST_RECORD_AUDIO);
 				}
 			}
 		});
@@ -483,7 +490,7 @@ public class ConversationActivity extends XmppActivity implements
 								attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO);
 								break;
 							case R.id.attach_record_voice:
-								attachFile(ATTACHMENT_CHOICE_RECORD_VOICE);
+								attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE);
 								break;
 						}
 						return false;
@@ -675,14 +682,17 @@ public class ConversationActivity extends XmppActivity implements
 		} else {
 			showConversationsOverview();
 			mPendingImageUri = null;
+			mPendingFileUri = null;
 			setSelectedConversation(conversationList.get(0));
 			this.mConversationFragment.reInit(getSelectedConversation());
 		}
 
 		if (mPendingImageUri != null) {
-			attachImageToConversation(getSelectedConversation(),
-					mPendingImageUri);
+			attachImageToConversation(getSelectedConversation(),mPendingImageUri);
 			mPendingImageUri = null;
+		} else if (mPendingFileUri != null) {
+			attachFileToConversation(getSelectedConversation(),mPendingFileUri);
+			mPendingFileUri = null;
 		}
 		ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
 		setIntent(new Intent());
@@ -726,13 +736,20 @@ public class ConversationActivity extends XmppActivity implements
 					selectedFragment.hideSnackbar();
 					selectedFragment.updateMessages();
 				}
-			} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
+			} else if (requestCode == REQUEST_ATTACH_IMAGE_DIALOG) {
 				mPendingImageUri = data.getData();
 				if (xmppConnectionServiceBound) {
 					attachImageToConversation(getSelectedConversation(),
 							mPendingImageUri);
 					mPendingImageUri = null;
 				}
+			} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
+				mPendingFileUri = data.getData();
+				if (xmppConnectionServiceBound) {
+					attachFileToConversation(getSelectedConversation(),
+							mPendingFileUri);
+					mPendingFileUri = null;
+				}
 			} else if (requestCode == REQUEST_SEND_PGP_IMAGE) {
 
 			} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) {
@@ -754,9 +771,6 @@ public class ConversationActivity extends XmppActivity implements
 						Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
 				intent.setData(mPendingImageUri);
 				sendBroadcast(intent);
-			} else if (requestCode == REQUEST_RECORD_AUDIO) {
-				attachAudioToConversation(getSelectedConversation(),
-						data.getData());
 			}
 		} else {
 			if (requestCode == REQUEST_IMAGE_CAPTURE) {
@@ -765,21 +779,40 @@ public class ConversationActivity extends XmppActivity implements
 		}
 	}
 
-	private void attachAudioToConversation(Conversation conversation, Uri uri) {
+	private void attachFileToConversation(Conversation conversation, Uri uri) {
+		prepareFileToast = Toast.makeText(getApplicationContext(),
+				getText(R.string.preparing_file), Toast.LENGTH_LONG);
+		prepareFileToast.show();
+		xmppConnectionService.attachFileToConversation(conversation,uri, new UiCallback<Message>() {
+			@Override
+			public void success(Message message) {
+				hidePrepareFileToast();
+				xmppConnectionService.sendMessage(message);
+			}
+
+			@Override
+			public void error(int errorCode, Message message) {
+				displayErrorDialog(errorCode);
+			}
 
+			@Override
+			public void userInputRequried(PendingIntent pi, Message message) {
+
+			}
+		});
 	}
 
 	private void attachImageToConversation(Conversation conversation, Uri uri) {
-		prepareImageToast = Toast.makeText(getApplicationContext(),
+		prepareFileToast = Toast.makeText(getApplicationContext(),
 				getText(R.string.preparing_image), Toast.LENGTH_LONG);
-		prepareImageToast.show();
+		prepareFileToast.show();
 		xmppConnectionService.attachImageToConversation(conversation, uri,
 				new UiCallback<Message>() {
 
 					@Override
 					public void userInputRequried(PendingIntent pi,
 												  Message object) {
-						hidePrepareImageToast();
+						hidePrepareFileToast();
 						ConversationActivity.this.runIntent(pi,
 								ConversationActivity.REQUEST_SEND_PGP_IMAGE);
 					}
@@ -791,19 +824,19 @@ public class ConversationActivity extends XmppActivity implements
 
 					@Override
 					public void error(int error, Message message) {
-						hidePrepareImageToast();
+						hidePrepareFileToast();
 						displayErrorDialog(error);
 					}
 				});
 	}
 
-	private void hidePrepareImageToast() {
-		if (prepareImageToast != null) {
+	private void hidePrepareFileToast() {
+		if (prepareFileToast != null) {
 			runOnUiThread(new Runnable() {
 
 				@Override
 				public void run() {
-					prepareImageToast.cancel();
+					prepareFileToast.cancel();
 				}
 			});
 		}

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java πŸ”—

@@ -42,6 +42,9 @@ import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.DownloadablePlaceholder;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.MucOptions;
 import eu.siacs.conversations.entities.Presences;
@@ -354,6 +357,7 @@ public class ConversationFragment extends Fragment {
 			MenuItem sendAgain = menu.findItem(R.id.send_again);
 			MenuItem copyUrl = menu.findItem(R.id.copy_url);
 			MenuItem downloadImage = menu.findItem(R.id.download_image);
+			MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission);
 			if (this.selectedMessage.getType() != Message.TYPE_TEXT
 					|| this.selectedMessage.getDownloadable() != null) {
 				copyText.setVisible(false);
@@ -370,12 +374,15 @@ public class ConversationFragment extends Fragment {
 					|| this.selectedMessage.getImageParams().url == null) {
 				copyUrl.setVisible(false);
 			}
-
 			if (this.selectedMessage.getType() != Message.TYPE_TEXT
 					|| this.selectedMessage.getDownloadable() != null
 					|| !this.selectedMessage.bodyContainsDownloadable()) {
 				downloadImage.setVisible(false);
 			}
+			if (this.selectedMessage.getDownloadable() == null
+					|| this.selectedMessage.getDownloadable() instanceof DownloadablePlaceholder) {
+				cancelTransmission.setVisible(false);
+			}
 		}
 	}
 
@@ -397,6 +404,9 @@ public class ConversationFragment extends Fragment {
 			case R.id.download_image:
 				downloadImage(selectedMessage);
 				return true;
+			case R.id.cancel_transmission:
+				cancelTransmission(selectedMessage);
+				return true;
 			default:
 				return super.onContextItemSelected(item);
 		}
@@ -423,6 +433,14 @@ public class ConversationFragment extends Fragment {
 	}
 
 	private void resendMessage(Message message) {
+		if (message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) {
+			DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
+			if (!file.exists()) {
+				Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show();
+				message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
+				return;
+			}
+		}
 		activity.xmppConnectionService.resendFailedMessages(message);
 	}
 
@@ -439,6 +457,13 @@ public class ConversationFragment extends Fragment {
 				.createNewConnection(message);
 	}
 
+	private void cancelTransmission(Message message) {
+		Downloadable downloadable = message.getDownloadable();
+		if (downloadable!=null) {
+			downloadable.cancel();
+		}
+	}
+
 	protected void privateMessageWith(final Jid counterpart) {
 		this.mEditMessage.setText("");
 		this.conversation.setNextCounterpart(counterpart);

src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java πŸ”—

@@ -6,6 +6,7 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.ui.ConversationActivity;
 import eu.siacs.conversations.ui.XmppActivity;
@@ -75,7 +76,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
 			convName.setTypeface(null, Typeface.NORMAL);
 		}
 
-		if (message.getType() == Message.TYPE_IMAGE
+		if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE
 				|| message.getDownloadable() != null) {
 			Downloadable d = message.getDownloadable();
 			if (conversation.isRead()) {
@@ -89,13 +90,35 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
 				if (d.getStatus() == Downloadable.STATUS_CHECKING) {
 					mLastMessage.setText(R.string.checking_image);
 				} else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
-					mLastMessage.setText(R.string.receiving_image);
+					if (message.getType() == Message.TYPE_FILE) {
+						mLastMessage.setText(getContext().getString(R.string.receiving_file,d.getMimeType(), d.getProgress()));
+					} else {
+						mLastMessage.setText(getContext().getString(R.string.receiving_image, d.getProgress()));
+					}
 				} else if (d.getStatus() == Downloadable.STATUS_OFFER) {
-					mLastMessage.setText(R.string.image_offered_for_download);
+					if (message.getType() == Message.TYPE_FILE) {
+						mLastMessage.setText(R.string.file_offered_for_download);
+					} else {
+						mLastMessage.setText(R.string.image_offered_for_download);
+					}
 				} else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
 					mLastMessage.setText(R.string.image_offered_for_download);
 				} else if (d.getStatus() == Downloadable.STATUS_DELETED) {
-					mLastMessage.setText(R.string.image_file_deleted);
+					if (message.getType() == Message.TYPE_FILE) {
+						mLastMessage.setText(R.string.file_deleted);
+					} else {
+						mLastMessage.setText(R.string.image_file_deleted);
+					}
+				} else if (d.getStatus() == Downloadable.STATUS_FAILED) {
+					if (message.getType() == Message.TYPE_FILE) {
+						mLastMessage.setText(R.string.file_transmission_failed);
+					} else {
+						mLastMessage.setText(R.string.image_transmission_failed);
+					}
+				} else if (message.getImageParams().width > 0) {
+					mLastMessage.setVisibility(View.GONE);
+					imagePreview.setVisibility(View.VISIBLE);
+					activity.loadBitmap(message, imagePreview);
 				} else {
 					mLastMessage.setText("");
 				}
@@ -103,6 +126,11 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
 				imagePreview.setVisibility(View.GONE);
 				mLastMessage.setVisibility(View.VISIBLE);
 				mLastMessage.setText(R.string.encrypted_message_received);
+			} else if (message.getType() == Message.TYPE_FILE && message.getImageParams().width <= 0) {
+				DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
+				mLastMessage.setVisibility(View.VISIBLE);
+				imagePreview.setVisibility(View.GONE);
+				mLastMessage.setText(getContext().getString(R.string.file,file.getMimeType()));
 			} else {
 				mLastMessage.setVisibility(View.GONE);
 				imagePreview.setVisibility(View.VISIBLE);

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java πŸ”—

@@ -1,16 +1,21 @@
 package eu.siacs.conversations.ui.adapter;
 
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.graphics.Typeface;
+import android.net.Uri;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.style.ForegroundColorSpan;
 import android.text.style.StyleSpan;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
 import android.view.ViewGroup;
+import android.webkit.MimeTypeMap;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
 import android.widget.ImageView;
@@ -18,6 +23,9 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
 import java.util.List;
 
 import eu.siacs.conversations.Config;
@@ -25,6 +33,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.entities.Contact;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Downloadable;
+import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.entities.Message.ImageParams;
 import eu.siacs.conversations.ui.ConversationActivity;
@@ -96,10 +105,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		}
 		boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
 				&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
-		if (message.getType() == Message.TYPE_IMAGE
-				|| message.getDownloadable() != null) {
+		if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getDownloadable() != null) {
 			ImageParams params = message.getImageParams();
-			if (params.size != 0) {
+			if (params.size > (1.5 * 1024 * 1024)) {
+				filesize = params.size / (1024 * 1024)+ " MB";
+			} else if (params.size > 0) {
 				filesize = params.size / 1024 + " KB";
 			}
 			if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) {
@@ -111,7 +121,12 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 			info = getContext().getString(R.string.waiting);
 			break;
 		case Message.STATUS_UNSEND:
-			info = getContext().getString(R.string.sending);
+			Downloadable d = message.getDownloadable();
+			if (d!=null) {
+				info = getContext().getString(R.string.sending_file,d.getProgress());
+			} else {
+				info = getContext().getString(R.string.sending);
+			}
 			break;
 		case Message.STATUS_OFFERED:
 			info = getContext().getString(R.string.offering);
@@ -181,13 +196,13 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		}
 	}
 
-	private void displayInfoMessage(ViewHolder viewHolder, int r) {
+	private void displayInfoMessage(ViewHolder viewHolder, String text) {
 		if (viewHolder.download_button != null) {
 			viewHolder.download_button.setVisibility(View.GONE);
 		}
 		viewHolder.image.setVisibility(View.GONE);
 		viewHolder.messageBody.setVisibility(View.VISIBLE);
-		viewHolder.messageBody.setText(getContext().getString(r));
+		viewHolder.messageBody.setText(text);
 		viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
 		viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
 		viewHolder.messageBody.setTextIsSelectable(false);
@@ -252,11 +267,11 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 	}
 
 	private void displayDownloadableMessage(ViewHolder viewHolder,
-			final Message message, int resid) {
+			final Message message, String text) {
 		viewHolder.image.setVisibility(View.GONE);
 		viewHolder.messageBody.setVisibility(View.GONE);
 		viewHolder.download_button.setVisibility(View.VISIBLE);
-		viewHolder.download_button.setText(resid);
+		viewHolder.download_button.setText(text);
 		viewHolder.download_button.setOnClickListener(new OnClickListener() {
 
 			@Override
@@ -267,6 +282,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		viewHolder.download_button.setOnLongClickListener(openContextMenu);
 	}
 
+	private void displayOpenableMessage(ViewHolder viewHolder,final Message message) {
+		final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
+		viewHolder.image.setVisibility(View.GONE);
+		viewHolder.messageBody.setVisibility(View.GONE);
+		viewHolder.download_button.setVisibility(View.VISIBLE);
+		viewHolder.download_button.setText(activity.getString(R.string.open_file,file.getMimeType()));
+		viewHolder.download_button.setOnClickListener(new OnClickListener() {
+
+			@Override
+			public void onClick(View v) {
+				openDonwloadable(file);
+			}
+		});
+		viewHolder.download_button.setOnLongClickListener(openContextMenu);
+	}
+
 	private void displayImageMessage(ViewHolder viewHolder,
 			final Message message) {
 		if (viewHolder.download_button != null) {
@@ -455,58 +486,66 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 					});
 		}
 
-		if (item.getType() == Message.TYPE_IMAGE
-				|| item.getDownloadable() != null) {
+		if (item.getDownloadable() != null && item.getDownloadable().getStatus() != Downloadable.STATUS_UPLOADING) {
 			Downloadable d = item.getDownloadable();
-			if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
-				displayInfoMessage(viewHolder, R.string.receiving_image);
-			} else if (d != null
-					&& d.getStatus() == Downloadable.STATUS_CHECKING) {
-				displayInfoMessage(viewHolder, R.string.checking_image);
-			} else if (d != null
-					&& d.getStatus() == Downloadable.STATUS_DELETED) {
-				displayInfoMessage(viewHolder, R.string.image_file_deleted);
-			} else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) {
-				displayDownloadableMessage(viewHolder, item,
-						R.string.download_image);
-			} else if (d != null
-					&& d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
-				displayDownloadableMessage(viewHolder, item,
-						R.string.check_image_filesize);
-			} else if (d != null && d.getStatus() == Downloadable.STATUS_FAILED) {
-				displayInfoMessage(viewHolder, R.string.image_transmission_failed);
-			} else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
-					|| (item.getEncryption() == Message.ENCRYPTION_NONE)
-					|| (item.getEncryption() == Message.ENCRYPTION_OTR)) {
-				displayImageMessage(viewHolder, item);
-			} else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
-				displayInfoMessage(viewHolder, R.string.encrypted_message);
-			} else {
-				displayDecryptionFailed(viewHolder);
-			}
-		} else {
-			if (item.getEncryption() == Message.ENCRYPTION_PGP) {
-				if (activity.hasPgp()) {
-					displayInfoMessage(viewHolder, R.string.encrypted_message);
+			if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
+				if (item.getType() == Message.TYPE_FILE) {
+					displayInfoMessage(viewHolder,activity.getString(R.string.receiving_file,d.getMimeType(),d.getProgress()));
 				} else {
-					displayInfoMessage(viewHolder,
-							R.string.install_openkeychain);
-                    if (viewHolder != null) {
-                        viewHolder.message_box
-                                .setOnClickListener(new OnClickListener() {
-
-                                    @Override
-                                    public void onClick(View v) {
-                                        activity.showInstallPgpDialog();
-                                    }
-                                });
-                    }
+					displayInfoMessage(viewHolder,activity.getString(R.string.receiving_image,d.getProgress()));
 				}
-			} else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
-				displayDecryptionFailed(viewHolder);
+			} else if (d.getStatus() == Downloadable.STATUS_CHECKING) {
+				displayInfoMessage(viewHolder,activity.getString(R.string.checking_image));
+			} else if (d.getStatus() == Downloadable.STATUS_DELETED) {
+				if (item.getType() == Message.TYPE_FILE) {
+					displayInfoMessage(viewHolder, activity.getString(R.string.file_deleted));
+				} else {
+					displayInfoMessage(viewHolder, activity.getString(R.string.image_file_deleted));
+				}
+			} else if (d.getStatus() == Downloadable.STATUS_OFFER) {
+				if (item.getType() == Message.TYPE_FILE) {
+					displayDownloadableMessage(viewHolder,item,activity.getString(R.string.download_file,d.getMimeType()));
+				} else {
+					displayDownloadableMessage(viewHolder, item,activity.getString(R.string.download_image));
+				}
+			} else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
+				displayDownloadableMessage(viewHolder, item,activity.getString(R.string.check_image_filesize));
+			} else if (d.getStatus() == Downloadable.STATUS_FAILED) {
+				if (item.getType() == Message.TYPE_FILE) {
+					displayInfoMessage(viewHolder, activity.getString(R.string.file_transmission_failed));
+				} else {
+					displayInfoMessage(viewHolder, activity.getString(R.string.image_transmission_failed));
+				}
+			}
+		} else if (item.getType() == Message.TYPE_IMAGE && item.getEncryption() != Message.ENCRYPTION_PGP && item.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
+			displayImageMessage(viewHolder, item);
+		} else if (item.getType() == Message.TYPE_FILE && item.getEncryption() != Message.ENCRYPTION_PGP && item.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
+			if (item.getImageParams().width > 0) {
+				displayImageMessage(viewHolder,item);
+			} else {
+				displayOpenableMessage(viewHolder, item);
+			}
+		} else if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+			if (activity.hasPgp()) {
+				displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message));
 			} else {
-				displayTextMessage(viewHolder, item);
+				displayInfoMessage(viewHolder,
+						activity.getString(R.string.install_openkeychain));
+				if (viewHolder != null) {
+					viewHolder.message_box
+							.setOnClickListener(new OnClickListener() {
+
+								@Override
+								public void onClick(View v) {
+									activity.showInstallPgpDialog();
+								}
+							});
+				}
 			}
+		} else if (item.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
+			displayDecryptionFailed(viewHolder);
+		} else {
+			displayTextMessage(viewHolder, item);
 		}
 
 		displayStatus(viewHolder, item);
@@ -524,6 +563,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
 		}
 	}
 
+	public void openDonwloadable(DownloadableFile file) {
+		if (!file.exists()) {
+			Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show();
+			return;
+		}
+		Intent openIntent = new Intent(Intent.ACTION_VIEW);
+		openIntent.setDataAndType(Uri.fromFile(file), file.getMimeType());
+		PackageManager manager = activity.getPackageManager();
+		List<ResolveInfo> infos = manager.queryIntentActivities(openIntent, 0);
+		if (infos.size() > 0) {
+			getContext().startActivity(openIntent);
+		} else {
+			Toast.makeText(activity,R.string.no_application_found_to_open_file,Toast.LENGTH_SHORT).show();
+		}
+	}
+
 	public interface OnContactPictureClicked {
 		public void onContactPictureClicked(Message message);
 	}

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java πŸ”—

@@ -1,5 +1,6 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import java.net.URLConnection;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Iterator;
@@ -9,14 +10,15 @@ import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 
 import android.content.Intent;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
+import android.os.SystemClock;
 import android.util.Log;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Conversation;
 import eu.siacs.conversations.entities.Downloadable;
 import eu.siacs.conversations.entities.DownloadableFile;
+import eu.siacs.conversations.entities.DownloadablePlaceholder;
 import eu.siacs.conversations.entities.Message;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
@@ -29,9 +31,6 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class JingleConnection implements Downloadable {
 
-	private final String[] extensions = { "webp", "jpeg", "jpg", "png" };
-	private final String[] cryptoExtensions = { "pgp", "gpg", "otr" };
-
 	private JingleConnectionManager mJingleConnectionManager;
 	private XmppConnectionService mXmppConnectionService;
 
@@ -46,7 +45,7 @@ public class JingleConnection implements Downloadable {
 	private int ibbBlockSize = 4096;
 
 	private int mJingleStatus = -1;
-	private int mStatus = -1;
+	private int mStatus = Downloadable.STATUS_UNKNOWN;
 	private Message message;
 	private String sessionId;
 	private Account account;
@@ -62,6 +61,9 @@ public class JingleConnection implements Downloadable {
 	private String contentName;
 	private String contentCreator;
 
+	private int mProgress = 0;
+	private long mLastGuiRefresh = 0;
+
 	private boolean receivedCandidate = false;
 	private boolean sentCandidate = false;
 
@@ -74,7 +76,7 @@ public class JingleConnection implements Downloadable {
 		@Override
 		public void onIqPacketReceived(Account account, IqPacket packet) {
 			if (packet.getType() == IqPacket.TYPE_ERROR) {
-				cancel();
+				fail();
 			}
 		}
 	};
@@ -90,16 +92,14 @@ public class JingleConnection implements Downloadable {
 					JingleConnection.this.mXmppConnectionService
 							.getNotificationService().push(message);
 				}
-				BitmapFactory.Options options = new BitmapFactory.Options();
-				options.inJustDecodeBounds = true;
-				BitmapFactory.decodeFile(file.getAbsolutePath(), options);
-				int imageHeight = options.outHeight;
-				int imageWidth = options.outWidth;
-				message.setBody(Long.toString(file.getSize()) + '|'
-						+ imageWidth + '|' + imageHeight);
+				mXmppConnectionService.getFileBackend().updateFileParams(message);
 				mXmppConnectionService.databaseBackend.createMessage(message);
 				mXmppConnectionService.markMessage(message,
 						Message.STATUS_RECEIVED);
+			} else {
+				if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
+					file.delete();
+				}
 			}
 			Log.d(Config.LOGTAG,
 					"sucessfully transmitted file:" + file.getAbsolutePath());
@@ -114,7 +114,7 @@ public class JingleConnection implements Downloadable {
 		@Override
 		public void onFileTransferAborted() {
 			JingleConnection.this.sendCancel();
-			JingleConnection.this.cancel();
+			JingleConnection.this.fail();
 		}
 	};
 
@@ -161,14 +161,14 @@ public class JingleConnection implements Downloadable {
 			Reason reason = packet.getReason();
 			if (reason != null) {
 				if (reason.hasChild("cancel")) {
-					this.cancel();
+					this.fail();
 				} else if (reason.hasChild("success")) {
 					this.receiveSuccess();
 				} else {
-					this.cancel();
+					this.fail();
 				}
 			} else {
-				this.cancel();
+				this.fail();
 			}
 		} else if (packet.isAction("session-accept")) {
 			returnResult = receiveAccept(packet);
@@ -203,6 +203,8 @@ public class JingleConnection implements Downloadable {
 		this.contentCreator = "initiator";
 		this.contentName = this.mJingleConnectionManager.nextRandomId();
 		this.message = message;
+		this.message.setDownloadable(this);
+		this.mStatus = Downloadable.STATUS_UPLOADING;
 		this.account = message.getConversation().getAccount();
 		this.initiator = this.account.getJid();
 		this.responder = this.message.getCounterpart();
@@ -258,7 +260,6 @@ public class JingleConnection implements Downloadable {
 						packet.getFrom().toBareJid(), false);
 		this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
 		this.message.setStatus(Message.STATUS_RECEIVED);
-		this.message.setType(Message.TYPE_IMAGE);
 		this.mStatus = Downloadable.STATUS_OFFER;
 		this.message.setDownloadable(this);
         final Jid from = packet.getFrom();
@@ -278,75 +279,83 @@ public class JingleConnection implements Downloadable {
 			Element fileSize = fileOffer.findChild("size");
 			Element fileNameElement = fileOffer.findChild("name");
 			if (fileNameElement != null) {
-				boolean supportedFile = false;
 				String[] filename = fileNameElement.getContent()
 						.toLowerCase(Locale.US).split("\\.");
-				if (Arrays.asList(this.extensions).contains(
+				if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(
 						filename[filename.length - 1])) {
-					supportedFile = true;
-				} else if (Arrays.asList(this.cryptoExtensions).contains(
+					message.setType(Message.TYPE_IMAGE);
+				} else if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(
 						filename[filename.length - 1])) {
 					if (filename.length == 3) {
-						if (Arrays.asList(this.extensions).contains(
+						if (Arrays.asList(VALID_IMAGE_EXTENSIONS).contains(
 								filename[filename.length - 2])) {
-							supportedFile = true;
-							if (filename[filename.length - 1].equals("otr")) {
-								Log.d(Config.LOGTAG, "receiving otr file");
-								this.message
-										.setEncryption(Message.ENCRYPTION_OTR);
-							} else {
-								this.message
-										.setEncryption(Message.ENCRYPTION_PGP);
-							}
+							message.setType(Message.TYPE_IMAGE);
+						} else {
+							message.setType(Message.TYPE_FILE);
+						}
+						if (filename[filename.length - 1].equals("otr")) {
+							message.setEncryption(Message.ENCRYPTION_OTR);
+						} else {
+							message.setEncryption(Message.ENCRYPTION_PGP);
 						}
 					}
+				} else {
+					message.setType(Message.TYPE_FILE);
 				}
-				if (supportedFile) {
-					long size = Long.parseLong(fileSize.getContent());
-					message.setBody(Long.toString(size));
-					conversation.add(message);
-					mXmppConnectionService.updateConversationUi();
-					if (size <= this.mJingleConnectionManager
-							.getAutoAcceptFileSize()) {
-						Log.d(Config.LOGTAG, "auto accepting file from "
-								+ packet.getFrom());
-						this.acceptedAutomatically = true;
-						this.sendAccept();
-					} else {
-						message.markUnread();
-						Log.d(Config.LOGTAG,
-								"not auto accepting new file offer with size: "
-										+ size
-										+ " allowed size:"
-										+ this.mJingleConnectionManager
-												.getAutoAcceptFileSize());
-						this.mXmppConnectionService.getNotificationService()
-								.push(message);
-					}
-					this.file = this.mXmppConnectionService.getFileBackend()
-							.getFile(message, false);
-					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
-						byte[] key = conversation.getSymmetricKey();
-						if (key == null) {
-							this.sendCancel();
-							this.cancel();
-							return;
-						} else {
-							this.file.setKey(key);
+				if (message.getType() == Message.TYPE_FILE) {
+					String suffix = "";
+					if (!fileNameElement.getContent().isEmpty()) {
+						String parts[] = fileNameElement.getContent().split("/");
+						suffix = parts[parts.length - 1];
+						if (message.getEncryption() == Message.ENCRYPTION_OTR  && suffix.endsWith(".otr")) {
+							suffix = suffix.substring(0,suffix.length() - 4);
+						} else if (message.getEncryption() == Message.ENCRYPTION_PGP && (suffix.endsWith(".pgp") || suffix.endsWith(".gpg"))) {
+							suffix = suffix.substring(0,suffix.length() - 4);
 						}
 					}
-					this.file.setExpectedSize(size);
+					message.setRelativeFilePath(message.getUuid()+"_"+suffix);
+				}
+				long size = Long.parseLong(fileSize.getContent());
+				message.setBody(Long.toString(size));
+				conversation.add(message);
+				mXmppConnectionService.updateConversationUi();
+				if (size <= this.mJingleConnectionManager
+						.getAutoAcceptFileSize()) {
+					Log.d(Config.LOGTAG, "auto accepting file from "
+							+ packet.getFrom());
+					this.acceptedAutomatically = true;
+					this.sendAccept();
 				} else {
-					this.sendCancel();
-					this.cancel();
+					message.markUnread();
+					Log.d(Config.LOGTAG,
+							"not auto accepting new file offer with size: "
+									+ size
+									+ " allowed size:"
+									+ this.mJingleConnectionManager
+											.getAutoAcceptFileSize());
+					this.mXmppConnectionService.getNotificationService()
+							.push(message);
 				}
+				this.file = this.mXmppConnectionService.getFileBackend()
+						.getFile(message, false);
+				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+					byte[] key = conversation.getSymmetricKey();
+					if (key == null) {
+						this.sendCancel();
+						this.fail();
+						return;
+					} else {
+						this.file.setKey(key);
+					}
+				}
+				this.file.setExpectedSize(size);
 			} else {
 				this.sendCancel();
-				this.cancel();
+				this.fail();
 			}
 		} else {
 			this.sendCancel();
-			this.cancel();
+			this.fail();
 		}
 	}
 
@@ -354,7 +363,7 @@ public class JingleConnection implements Downloadable {
 		this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
 		JinglePacket packet = this.bootstrapPacket("session-initiate");
 		Content content = new Content(this.contentCreator, this.contentName);
-		if (message.getType() == Message.TYPE_IMAGE) {
+		if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 			content.setTransportId(this.transportId);
 			this.file = this.mXmppConnectionService.getFileBackend().getFile(
 					message, false);
@@ -485,7 +494,7 @@ public class JingleConnection implements Downloadable {
 					} else {
 						Log.d(Config.LOGTAG, "activated connection not found");
 						this.sendCancel();
-						this.cancel();
+						this.fail();
 					}
 				}
 				return true;
@@ -532,7 +541,7 @@ public class JingleConnection implements Downloadable {
 		this.transport = connection;
 		if (connection == null) {
 			Log.d(Config.LOGTAG, "could not find suitable candidate");
-			this.disconnect();
+			this.disconnectSocks5Connections();
 			if (this.initiator.equals(account.getJid())) {
 				this.sendFallbackToIbb();
 			}
@@ -623,7 +632,7 @@ public class JingleConnection implements Downloadable {
 		reason.addChild("success");
 		packet.setReason(reason);
 		this.sendJinglePacket(packet);
-		this.disconnect();
+		this.disconnectSocks5Connections();
 		this.mJingleStatus = JINGLE_STATUS_FINISHED;
 		this.message.setStatus(Message.STATUS_RECEIVED);
 		this.message.setDownloadable(null);
@@ -654,8 +663,7 @@ public class JingleConnection implements Downloadable {
 			}
 		}
 		this.transportId = packet.getJingleContent().getTransportId();
-		this.transport = new JingleInbandTransport(this.account,
-				this.responder, this.transportId, this.ibbBlockSize);
+		this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
 		this.transport.receive(file, onFileTransmissionSatusChanged);
 		JinglePacket answer = bootstrapPacket("transport-accept");
 		Content content = new Content("initiator", "a-file-offer");
@@ -677,8 +685,7 @@ public class JingleConnection implements Downloadable {
 					this.ibbBlockSize = bs;
 				}
 			}
-			this.transport = new JingleInbandTransport(this.account,
-					this.responder, this.transportId, this.ibbBlockSize);
+			this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
 			this.transport.connect(new OnTransportConnected() {
 
 				@Override
@@ -702,20 +709,51 @@ public class JingleConnection implements Downloadable {
 		this.mJingleStatus = JINGLE_STATUS_FINISHED;
 		this.mXmppConnectionService.markMessage(this.message,
 				Message.STATUS_SEND);
-		this.disconnect();
+		this.disconnectSocks5Connections();
+		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+			this.transport.disconnect();
+		}
+		this.message.setDownloadable(null);
 		this.mJingleConnectionManager.finishConnection(this);
 	}
 
 	public void cancel() {
-		this.mJingleStatus = JINGLE_STATUS_CANCELED;
-		this.disconnect();
+		this.disconnectSocks5Connections();
+		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+			this.transport.disconnect();
+		}
+		this.sendCancel();
+		this.mJingleConnectionManager.finishConnection(this);
+		if (this.responder.equals(account.getJid())) {
+			this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED));
+			if (this.file!=null) {
+				file.delete();
+			}
+			this.mXmppConnectionService.updateConversationUi();
+		} else {
+			this.mXmppConnectionService.markMessage(this.message,
+					Message.STATUS_SEND_FAILED);
+			this.message.setDownloadable(null);
+		}
+	}
+
+	private void fail() {
+		this.mJingleStatus = JINGLE_STATUS_FAILED;
+		this.disconnectSocks5Connections();
+		if (this.transport != null && this.transport instanceof JingleInbandTransport) {
+			this.transport.disconnect();
+		}
 		if (this.message != null) {
 			if (this.responder.equals(account.getJid())) {
-				this.mStatus = Downloadable.STATUS_FAILED;
+				this.message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_FAILED));
+				if (this.file!=null) {
+					file.delete();
+				}
 				this.mXmppConnectionService.updateConversationUi();
 			} else {
 				this.mXmppConnectionService.markMessage(this.message,
 						Message.STATUS_SEND_FAILED);
+				this.message.setDownloadable(null);
 			}
 		}
 		this.mJingleConnectionManager.finishConnection(this);
@@ -764,7 +802,7 @@ public class JingleConnection implements Downloadable {
 		});
 	}
 
-	private void disconnect() {
+	private void disconnectSocks5Connections() {
 		Iterator<Entry<String, JingleSocks5Transport>> it = this.connections
 				.entrySet().iterator();
 		while (it.hasNext()) {
@@ -856,6 +894,14 @@ public class JingleConnection implements Downloadable {
 		return null;
 	}
 
+	public void updateProgress(int i) {
+		this.mProgress = i;
+		if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
+			this.mLastGuiRefresh = SystemClock.elapsedRealtime();
+			mXmppConnectionService.updateConversationUi();
+		}
+	}
+
 	interface OnProxyActivated {
 		public void success();
 
@@ -900,4 +946,29 @@ public class JingleConnection implements Downloadable {
 			return 0;
 		}
 	}
+
+	@Override
+	public int getProgress() {
+		return this.mProgress;
+	}
+
+	@Override
+	public String getMimeType() {
+		if (this.message.getType() == Message.TYPE_FILE) {
+			String mime = null;
+			String path = this.message.getRelativeFilePath();
+			if (path != null && !this.message.getRelativeFilePath().isEmpty()) {
+				mime = URLConnection.guessContentTypeFromName(this.message.getRelativeFilePath());
+				if (mime!=null) {
+					return  mime;
+				} else {
+					return "";
+				}
+			} else {
+				return "";
+			}
+		} else {
+			return "image/webp";
+		}
+	}
 }

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java πŸ”—

@@ -75,6 +75,10 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
 	public void getPrimaryCandidate(Account account,
 			final OnPrimaryCandidateFound listener) {
+		if (Config.NO_PROXY_LOOKUP) {
+			listener.onPrimaryCandidateFound(false, null);
+			return;
+		}
 		if (!this.primaryCandidates.containsKey(account.getJid().toBareJid())) {
 			String xmlns = "http://jabber.org/protocol/bytestreams";
 			final String proxy = account.getXmppConnection()

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java πŸ”—

@@ -8,6 +8,7 @@ import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 
 import android.util.Base64;
+
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.DownloadableFile;
 import eu.siacs.conversations.utils.CryptoHelper;
@@ -27,11 +28,15 @@ public class JingleInbandTransport extends JingleTransport {
 
 	private boolean established = false;
 
+	private boolean connected = true;
+
 	private DownloadableFile file;
+	private JingleConnection connection;
 
 	private InputStream fileInputStream = null;
-	private OutputStream fileOutputStream;
-	private long remainingSize;
+	private OutputStream fileOutputStream = null;
+	private long remainingSize = 0;
+	private long fileSize = 0;
 	private MessageDigest digest;
 
 	private OnFileTransmissionStatusChanged onFileTransmissionStatusChanged;
@@ -39,16 +44,16 @@ public class JingleInbandTransport extends JingleTransport {
 	private OnIqPacketReceived onAckReceived = new OnIqPacketReceived() {
 		@Override
 		public void onIqPacketReceived(Account account, IqPacket packet) {
-			if (packet.getType() == IqPacket.TYPE_RESULT) {
+			if (connected && packet.getType() == IqPacket.TYPE_RESULT) {
 				sendNextBlock();
 			}
 		}
 	};
 
-	public JingleInbandTransport(final Account account, final Jid counterpart,
-			final String sid, final int blocksize) {
-		this.account = account;
-		this.counterpart = counterpart;
+	public JingleInbandTransport(final JingleConnection connection, final String sid, final int blocksize) {
+		this.connection = connection;
+		this.account = connection.getAccount();
+		this.counterpart = connection.getCounterPart();
 		this.blockSize = blocksize;
 		this.bufferSize = blocksize / 4;
 		this.sessionId = sid;
@@ -61,7 +66,7 @@ public class JingleInbandTransport extends JingleTransport {
 		open.setAttribute("sid", this.sessionId);
 		open.setAttribute("stanza", "iq");
 		open.setAttribute("block-size", Integer.toString(this.blockSize));
-
+		this.connected = true;
 		this.account.getXmppConnection().sendIqPacket(iq,
 				new OnIqPacketReceived() {
 
@@ -92,7 +97,7 @@ public class JingleInbandTransport extends JingleTransport {
 				callback.onFileTransferAborted();
 				return;
 			}
-			this.remainingSize = file.getExpectedSize();
+			this.remainingSize = this.fileSize = file.getExpectedSize();
 		} catch (final NoSuchAlgorithmException | IOException e) {
 			callback.onFileTransferAborted();
 		}
@@ -104,6 +109,8 @@ public class JingleInbandTransport extends JingleTransport {
 		this.onFileTransmissionStatusChanged = callback;
 		this.file = file;
 		try {
+			this.remainingSize = this.file.getSize();
+			this.fileSize = this.remainingSize;
 			this.digest = MessageDigest.getInstance("SHA-1");
 			this.digest.reset();
 			fileInputStream = this.file.createInputStream();
@@ -111,12 +118,33 @@ public class JingleInbandTransport extends JingleTransport {
 				callback.onFileTransferAborted();
 				return;
 			}
-			this.sendNextBlock();
+			if (this.connected) {
+				this.sendNextBlock();
+			}
 		} catch (NoSuchAlgorithmException e) {
 			callback.onFileTransferAborted();
 		}
 	}
 
+	@Override
+	public void disconnect() {
+		this.connected = false;
+		if (this.fileOutputStream != null) {
+			try {
+				this.fileOutputStream.close();
+			} catch (IOException e) {
+
+			}
+		}
+		if (this.fileInputStream != null) {
+			try {
+				this.fileInputStream.close();
+			} catch (IOException e) {
+
+			}
+		}
+	}
+
 	private void sendNextBlock() {
 		byte[] buffer = new byte[this.bufferSize];
 		try {
@@ -126,6 +154,7 @@ public class JingleInbandTransport extends JingleTransport {
 				fileInputStream.close();
 				this.onFileTransmissionStatusChanged.onFileTransmitted(file);
 			} else {
+				this.remainingSize -= count;
 				this.digest.update(buffer);
 				String base64 = Base64.encodeToString(buffer, Base64.NO_WRAP);
 				IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
@@ -140,6 +169,7 @@ public class JingleInbandTransport extends JingleTransport {
 				this.account.getXmppConnection().sendIqPacket(iq,
 						this.onAckReceived);
 				this.seq++;
+				connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
 			}
 		} catch (IOException e) {
 			this.onFileTransmissionStatusChanged.onFileTransferAborted();
@@ -155,6 +185,7 @@ public class JingleInbandTransport extends JingleTransport {
 			}
 			this.remainingSize -= buffer.length;
 
+
 			this.fileOutputStream.write(buffer);
 
 			this.digest.update(buffer);
@@ -163,6 +194,8 @@ public class JingleInbandTransport extends JingleTransport {
 				fileOutputStream.flush();
 				fileOutputStream.close();
 				this.onFileTransmissionStatusChanged.onFileTransmitted(file);
+			} else {
+				connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
 			}
 		} catch (IOException e) {
 			this.onFileTransmissionStatusChanged.onFileTransferAborted();
@@ -173,13 +206,14 @@ public class JingleInbandTransport extends JingleTransport {
 		if (payload.getName().equals("open")) {
 			if (!established) {
 				established = true;
+				connected = true;
 				this.account.getXmppConnection().sendIqPacket(
 						packet.generateRespone(IqPacket.TYPE_RESULT), null);
 			} else {
 				this.account.getXmppConnection().sendIqPacket(
 						packet.generateRespone(IqPacket.TYPE_ERROR), null);
 			}
-		} else if (payload.getName().equals("data")) {
+		} else if (connected && payload.getName().equals("data")) {
 			this.receiveNextBlock(payload.getContent());
 			this.account.getXmppConnection().sendIqPacket(
 					packet.generateRespone(IqPacket.TYPE_RESULT), null);

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java πŸ”—

@@ -15,6 +15,7 @@ import eu.siacs.conversations.utils.CryptoHelper;
 
 public class JingleSocks5Transport extends JingleTransport {
 	private JingleCandidate candidate;
+	private JingleConnection connection;
 	private String destination;
 	private OutputStream outputStream;
 	private InputStream inputStream;
@@ -25,6 +26,7 @@ public class JingleSocks5Transport extends JingleTransport {
 	public JingleSocks5Transport(JingleConnection jingleConnection,
 			JingleCandidate candidate) {
 		this.candidate = candidate;
+		this.connection = jingleConnection;
 		try {
 			MessageDigest mDigest = MessageDigest.getInstance("SHA-1");
 			StringBuilder destBuilder = new StringBuilder();
@@ -102,11 +104,15 @@ public class JingleSocks5Transport extends JingleTransport {
 						callback.onFileTransferAborted();
 						return;
 					}
+					long size = file.getSize();
+					long transmitted = 0;
 					int count;
 					byte[] buffer = new byte[8192];
 					while ((count = fileInputStream.read(buffer)) > 0) {
 						outputStream.write(buffer, 0, count);
 						digest.update(buffer, 0, count);
+						transmitted += count;
+						connection.updateProgress((int) ((((double) transmitted) / size) * 100));
 					}
 					outputStream.flush();
 					file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
@@ -151,6 +157,7 @@ public class JingleSocks5Transport extends JingleTransport {
 						callback.onFileTransferAborted();
 						return;
 					}
+					double size = file.getExpectedSize();
 					long remainingSize = file.getExpectedSize();
 					byte[] buffer = new byte[8192];
 					int count = buffer.length;
@@ -164,6 +171,7 @@ public class JingleSocks5Transport extends JingleTransport {
 							digest.update(buffer, 0, count);
 							remainingSize -= count;
 						}
+						connection.updateProgress((int) (((size - remainingSize) / size) * 100));
 					}
 					fileOutputStream.flush();
 					fileOutputStream.close();
@@ -189,6 +197,20 @@ public class JingleSocks5Transport extends JingleTransport {
 	}
 
 	public void disconnect() {
+		if (this.outputStream != null) {
+			try {
+				this.outputStream.close();
+			} catch (IOException e) {
+
+			}
+		}
+		if (this.inputStream != null) {
+			try {
+				this.inputStream.close();
+			} catch (IOException e) {
+
+			}
+		}
 		if (this.socket != null) {
 			try {
 				this.socket.close();

src/main/res/menu/attachment_choices.xml πŸ”—

@@ -9,7 +9,6 @@
         android:title="@string/attach_take_picture"/>
     <item
         android:id="@+id/attach_record_voice"
-        android:title="@string/attach_record_voice"
-        android:visible="false"/>
+        android:title="@string/choose_file"/>
 
 </menu>

src/main/res/menu/message_context.xml πŸ”—

@@ -16,5 +16,8 @@
     <item
         android:id="@+id/download_image"
         android:title="@string/download_image"/>
+    <item
+        android:id="@+id/cancel_transmission"
+        android:title="@string/cancel_transmission" />
 
 </menu>

src/main/res/values/strings.xml πŸ”—

@@ -58,7 +58,7 @@
     <string name="add_contact">Add contact</string>
     <string name="send_failed">delivery failed</string>
     <string name="send_rejected">rejected</string>
-    <string name="receiving_image">Receiving image file. Please wait…</string>
+    <string name="receiving_image">Receiving image file (%1$d%%)</string>
     <string name="preparing_image">Preparing image for transmission</string>
     <string name="action_clear_history">Clear history</string>
     <string name="clear_conversation_history">Clear Conversation History</string>
@@ -332,4 +332,16 @@
     <string name="touch_to_disable">Touch to disable foreground service</string>
     <string name="pref_keep_foreground_service">Keep service in foreground</string>
     <string name="pref_keep_foreground_service_summary">Prevents the operating system from killing your connection</string>
+    <string name="choose_file">Choose file</string>
+    <string name="receiving_file">Receiving %1$s file (%2$d%% completed)</string>
+    <string name="download_file">Download %s file</string>
+    <string name="open_file">Open %s file</string>
+    <string name="sending_file">sending (%1$d%% completed)</string>
+    <string name="preparing_file">Preparing file for transmission</string>
+    <string name="file_offered_for_download">File offered for download</string>
+    <string name="file">%s file</string>
+    <string name="cancel_transmission">Cancel transmission</string>
+    <string name="file_transmission_failed">file transmission failed</string>
+    <string name="file_deleted">The file has been deleted</string>
+    <string name="no_application_found_to_open_file">No application found to open file</string>
 </resources>