diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
index 8aa02d42af75f09ebd92eb8e9d7f88a7c50bb8ce..8d905dcc11ca17df8f4e9301e55393af7891c78c 100644
--- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
+++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/BriarUiTestComponent.java
@@ -4,6 +4,7 @@ import org.briarproject.bramble.BrambleAndroidModule;
 import org.briarproject.bramble.BrambleCoreModule;
 import org.briarproject.bramble.account.BriarAccountModule;
 import org.briarproject.briar.BriarCoreModule;
+import org.briarproject.briar.android.attachment.AttachmentModule;
 import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
 
 import javax.inject.Singleton;
@@ -13,6 +14,7 @@ import dagger.Component;
 @Singleton
 @Component(modules = {
 		AppModule.class,
+		AttachmentModule.class,
 		BriarCoreModule.class,
 		BrambleAndroidModule.class,
 		BriarAccountModule.class,
diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java
index 9881c74fb0117262106ad3a1de27e614c465adcd..29a0ecab53201278f6a537fd38a7c296746a67d0 100644
--- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java
+++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java
@@ -47,8 +47,10 @@ public class AttachmentRetrieverIntegrationTest {
 	);
 	private final MessageId msgId = new MessageId(getRandomId());
 
+	private final ImageHelper imageHelper = new ImageHelperImpl();
 	private final AttachmentRetriever retriever =
-			new AttachmentRetriever(null, dimensions);
+			new AttachmentRetrieverImpl(null, dimensions, imageHelper,
+					new ImageSizeCalculator(imageHelper));
 
 	@Test
 	public void testSmallJpegImage() throws Exception {
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
index f71811826229f56bb4e99d1de65b20fe16d982f4..8fd44813c6bfeac16714d1c23fe3b13db614ab3a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
@@ -30,6 +30,7 @@ import org.briarproject.bramble.api.system.LocationUtils;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
 import org.briarproject.briar.BriarCoreEagerSingletons;
 import org.briarproject.briar.BriarCoreModule;
+import org.briarproject.briar.android.attachment.AttachmentModule;
 import org.briarproject.briar.android.conversation.glide.BriarModelLoader;
 import org.briarproject.briar.android.login.SignInReminderReceiver;
 import org.briarproject.briar.android.reporting.BriarReportSender;
@@ -68,7 +69,8 @@ import dagger.Component;
 		BriarCoreModule.class,
 		BrambleAndroidModule.class,
 		BriarAccountModule.class,
-		AppModule.class
+		AppModule.class,
+		AttachmentModule.class
 })
 public interface AndroidComponent
 		extends BrambleCoreEagerSingletons, BrambleAndroidEagerSingletons,
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreationTask.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreationTask.java
index d9e77d1d81ce81d134dbc4137b653af8a0dcc133..8c55972a9b6567223247ba7729b3684958d13700 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreationTask.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreationTask.java
@@ -1,6 +1,8 @@
 package org.briarproject.briar.android.attachment;
 
 import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory.Options;
 import android.net.Uri;
 import android.support.annotation.Nullable;
 
@@ -12,11 +14,17 @@ import org.briarproject.briar.api.messaging.AttachmentHeader;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.jsoup.UnsupportedMimeTypeException;
 
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
 import java.util.logging.Logger;
 
+import static android.graphics.Bitmap.CompressFormat.JPEG;
+import static android.graphics.BitmapFactory.decodeStream;
+import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
 import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -24,6 +32,7 @@ import static org.briarproject.bramble.util.LogUtils.logDuration;
 import static org.briarproject.bramble.util.LogUtils.logException;
 import static org.briarproject.bramble.util.LogUtils.now;
 import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES;
+import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
 
 @NotNullByDefault
 class AttachmentCreationTask {
@@ -31,8 +40,11 @@ class AttachmentCreationTask {
 	private static Logger LOG =
 			getLogger(AttachmentCreationTask.class.getName());
 
+	private static final int MAX_ATTACHMENT_DIMENSION = 1000;
+
 	private final MessagingManager messagingManager;
 	private final ContentResolver contentResolver;
+	private final ImageSizeCalculator imageSizeCalculator;
 	private final GroupId groupId;
 	private final Collection<Uri> uris;
 	private final boolean needsSize;
@@ -43,24 +55,26 @@ class AttachmentCreationTask {
 
 	AttachmentCreationTask(MessagingManager messagingManager,
 			ContentResolver contentResolver,
-			AttachmentCreator attachmentCreator, GroupId groupId,
-			Collection<Uri> uris, boolean needsSize) {
+			AttachmentCreator attachmentCreator,
+			ImageSizeCalculator imageSizeCalculator,
+			GroupId groupId, Collection<Uri> uris, boolean needsSize) {
 		this.messagingManager = messagingManager;
 		this.contentResolver = contentResolver;
+		this.imageSizeCalculator = imageSizeCalculator;
 		this.groupId = groupId;
 		this.uris = uris;
 		this.needsSize = needsSize;
 		this.attachmentCreator = attachmentCreator;
 	}
 
-	public void cancel() {
+	void cancel() {
 		canceled = true;
 		attachmentCreator = null;
 	}
 
 	@IoExecutor
-	public void storeAttachments() {
-		for (Uri uri: uris) processUri(uri);
+	void storeAttachments() {
+		for (Uri uri : uris) processUri(uri);
 		AttachmentCreator attachmentCreator = this.attachmentCreator;
 		if (!canceled && attachmentCreator != null)
 			attachmentCreator.onAttachmentCreationFinished();
@@ -98,6 +112,8 @@ class AttachmentCreationTask {
 		}
 		InputStream is = contentResolver.openInputStream(uri);
 		if (is == null) throw new IOException();
+		is = compressImage(is, contentType);
+		contentType = "image/jpeg";
 		long timestamp = System.currentTimeMillis();
 		AttachmentHeader h = messagingManager
 				.addLocalAttachment(groupId, timestamp, contentType, is);
@@ -113,4 +129,48 @@ class AttachmentCreationTask {
 		return false;
 	}
 
+	private InputStream compressImage(InputStream is, String contentType)
+			throws IOException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		try {
+			Bitmap bitmap = createBitmap(is, contentType);
+			for (int quality = 100; quality >= 0; quality -= 10) {
+				if (!bitmap.compress(JPEG, quality, out))
+					throw new IOException();
+				if (out.size() <= MAX_IMAGE_SIZE) {
+					if (LOG.isLoggable(INFO)) {
+						LOG.info("Compressed image to "
+								+ out.size() + " bytes, quality " + quality);
+					}
+					return new ByteArrayInputStream(out.toByteArray());
+				}
+				out.reset();
+			}
+			throw new IOException();
+		} finally {
+			tryToClose(is, LOG, WARNING);
+		}
+	}
+
+	private Bitmap createBitmap(InputStream is, String contentType)
+			throws IOException {
+		is = new BufferedInputStream(is);
+		Size size = imageSizeCalculator.getSize(is, contentType);
+		if (size.error) throw new IOException();
+		if (LOG.isLoggable(INFO))
+			LOG.info("Original image size: " + size.width + "x" + size.height);
+		int dimension = Math.max(size.width, size.height);
+		int inSampleSize = 1;
+		while (dimension > MAX_ATTACHMENT_DIMENSION) {
+			inSampleSize *= 2;
+			dimension /= 2;
+		}
+		if (LOG.isLoggable(INFO))
+			LOG.info("Scaling attachment by factor of " + inSampleSize);
+		Options options = new Options();
+		options.inSampleSize = inSampleSize;
+		Bitmap bitmap = decodeStream(is, null, options);
+		if (bitmap == null) throw new IOException();
+		return bitmap;
+	}
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreator.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreator.java
index 9d6433100c31441599b6a64e6c3fb94ef3cdc9f7..05916b20c0f56db669ce6fe09ca003b0403587f8 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreator.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreator.java
@@ -1,85 +1,24 @@
 package org.briarproject.briar.android.attachment;
 
-
-import android.app.Application;
 import android.arch.lifecycle.LiveData;
-import android.arch.lifecycle.MutableLiveData;
 import android.net.Uri;
-import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 
-import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.lifecycle.IoExecutor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.R;
-import org.briarproject.briar.api.messaging.Attachment;
 import org.briarproject.briar.api.messaging.AttachmentHeader;
-import org.briarproject.briar.api.messaging.FileTooBigException;
-import org.briarproject.briar.api.messaging.MessagingManager;
-import org.jsoup.UnsupportedMimeTypeException;
 
-import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import static java.util.logging.Level.WARNING;
-import static java.util.logging.Logger.getLogger;
-import static org.briarproject.bramble.util.LogUtils.logException;
-import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
-import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
 
 @NotNullByDefault
-public class AttachmentCreator {
-
-	private static Logger LOG = getLogger(AttachmentCreator.class.getName());
-
-	private final Application app;
-	@IoExecutor
-	private final Executor ioExecutor;
-	private final MessagingManager messagingManager;
-	private final AttachmentRetriever retriever;
-
-	private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
-	private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
-			new CopyOnWriteArrayList<>();
-
-	@Nullable
-	private AttachmentCreationTask task;
-
-	@Nullable
-	private volatile MutableLiveData<AttachmentResult> result;
-
-	public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
-			MessagingManager messagingManager, AttachmentRetriever retriever) {
-		this.app = app;
-		this.ioExecutor = ioExecutor;
-		this.messagingManager = messagingManager;
-		this.retriever = retriever;
-	}
+public interface AttachmentCreator {
 
 	@UiThread
-	public LiveData<AttachmentResult> storeAttachments(
-			LiveData<GroupId> groupId, Collection<Uri> newUris) {
-		if (task != null || result != null || !uris.isEmpty())
-			throw new IllegalStateException();
-		uris.addAll(newUris);
-		observeForeverOnce(groupId, id -> {
-			if (id == null) throw new IllegalStateException();
-			boolean needsSize = uris.size() == 1;
-			task = new AttachmentCreationTask(messagingManager,
-					app.getContentResolver(), this, id, uris, needsSize);
-			ioExecutor.execute(() -> task.storeAttachments());
-		});
-		MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
-		this.result = result;
-		return result;
-	}
+	LiveData<AttachmentResult> storeAttachments(LiveData<GroupId> groupId,
+			Collection<Uri> newUris);
 
 	/**
 	 * This should be only called after configuration changes.
@@ -87,72 +26,10 @@ public class AttachmentCreator {
 	 * They are already being created and returned by the existing LiveData.
 	 */
 	@UiThread
-	public LiveData<AttachmentResult> getLiveAttachments() {
-		MutableLiveData<AttachmentResult> result = this.result;
-		if (task == null || result == null || uris.isEmpty())
-			throw new IllegalStateException();
-		// A task is already running. It will update the result LiveData.
-		// So nothing more to do here.
-		return result;
-	}
-
-	@IoExecutor
-	void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
-			boolean needsSize) {
-		// get and cache AttachmentItem for ImagePreview
-		try {
-			Attachment a = retriever.getMessageAttachment(h);
-			AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
-			if (item.hasError()) throw new IOException();
-			AttachmentItemResult itemResult =
-					new AttachmentItemResult(uri, item);
-			itemResults.add(itemResult);
-			MutableLiveData<AttachmentResult> result = this.result;
-			if (result != null) result.postValue(getResult(false));
-		} catch (IOException | DbException e) {
-			logException(LOG, WARNING, e);
-			onAttachmentError(uri, e);
-		}
-	}
-
-	@IoExecutor
-	void onAttachmentError(Uri uri, Throwable t) {
-		// get error message
-		String errorMsg;
-		if (t instanceof UnsupportedMimeTypeException) {
-			String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
-			errorMsg = app.getString(
-					R.string.image_attach_error_invalid_mime_type, mimeType);
-		} else if (t instanceof FileTooBigException) {
-			int mb = MAX_IMAGE_SIZE / 1024 / 1024;
-			errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
-		} else {
-			errorMsg = null; // generic error
-		}
-		AttachmentItemResult itemResult =
-				new AttachmentItemResult(uri, errorMsg);
-		itemResults.add(itemResult);
-		MutableLiveData<AttachmentResult> result = this.result;
-		if (result != null) result.postValue(getResult(false));
-		// expect to receive a cancel from the UI
-	}
-
-	@IoExecutor
-	void onAttachmentCreationFinished() {
-		MutableLiveData<AttachmentResult> result = this.result;
-		if (result != null) result.postValue(getResult(true));
-	}
+	LiveData<AttachmentResult> getLiveAttachments();
 
 	@UiThread
-	public List<AttachmentHeader> getAttachmentHeadersForSending() {
-		List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
-		for (AttachmentItemResult itemResult : itemResults) {
-			// check if we are trying to send attachment items with errors
-			if (itemResult.getItem() == null) throw new IllegalStateException();
-			headers.add(itemResult.getItem().getHeader());
-		}
-		return headers;
-	}
+	List<AttachmentHeader> getAttachmentHeadersForSending();
 
 	/**
 	 * Marks the attachments as sent and adds the items to the cache for display
@@ -160,70 +37,24 @@ public class AttachmentCreator {
 	 * @param id The MessageId of the sent message.
 	 */
 	@UiThread
-	public void onAttachmentsSent(MessageId id) {
-		List<AttachmentItem> items = new ArrayList<>(itemResults.size());
-		for (AttachmentItemResult itemResult : itemResults) {
-			// check if we are trying to send attachment items with errors
-			if (itemResult.getItem() == null) throw new IllegalStateException();
-			items.add(itemResult.getItem());
-		}
-		retriever.cachePut(id, items);
-		resetState();
-	}
+	void onAttachmentsSent(MessageId id);
 
 	/**
 	 * Needs to be called when created attachments will not be sent anymore.
 	 */
 	@UiThread
-	public void cancel() {
-		if (task == null) throw new AssertionError();
-		task.cancel();
-		deleteUnsentAttachments();
-		resetState();
-	}
+	void cancel();
 
 	@UiThread
-	private void resetState() {
-		task = null;
-		uris.clear();
-		itemResults.clear();
-		MutableLiveData<AttachmentResult> result = this.result;
-		if (result != null) {
-			result.setValue(null);
-			this.result = null;
-		}
-	}
+	void deleteUnsentAttachments();
 
-	@UiThread
-	public void deleteUnsentAttachments() {
-		// Make a copy for the IoExecutor as we clear the itemResults soon
-		List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
-		for (AttachmentItemResult itemResult : itemResults) {
-			// check if we are trying to send attachment items with errors
-			if (itemResult.getItem() != null)
-				headers.add(itemResult.getItem().getHeader());
-		}
-		ioExecutor.execute(() -> {
-			for (AttachmentHeader header : headers) {
-				try {
-					messagingManager.removeAttachment(header);
-				} catch (DbException e) {
-					logException(LOG, WARNING, e);
-				}
-			}
-		});
-	}
+	@IoExecutor
+	void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
+			boolean needsSize);
 
-	private AttachmentResult getResult(boolean finished) {
-		// Make a copy of the list,
-		// because our copy will continue to change in the background.
-		// (As it's a CopyOnWriteArrayList,
-		//  the code that receives the result can safely do simple things
-		//  like iterating over the list,
-		//  but anything that involves calling more than one list method
-		//  is still unsafe.)
-		Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
-		return new AttachmentResult(items, finished);
-	}
+	@IoExecutor
+	void onAttachmentError(Uri uri, Throwable t);
 
-}
+	@IoExecutor
+	void onAttachmentCreationFinished();
+}
\ No newline at end of file
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..53687f48bcfad5d68128e8fc9c2e56113a96896a
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreatorImpl.java
@@ -0,0 +1,233 @@
+package org.briarproject.briar.android.attachment;
+
+
+import android.app.Application;
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MutableLiveData;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.annotation.UiThread;
+
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.R;
+import org.briarproject.briar.api.messaging.Attachment;
+import org.briarproject.briar.api.messaging.AttachmentHeader;
+import org.briarproject.briar.api.messaging.FileTooBigException;
+import org.briarproject.briar.api.messaging.MessagingManager;
+import org.jsoup.UnsupportedMimeTypeException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
+import static org.briarproject.briar.api.messaging.MessagingConstants.MAX_IMAGE_SIZE;
+
+@NotNullByDefault
+class AttachmentCreatorImpl implements AttachmentCreator {
+
+	private static Logger LOG =
+			getLogger(AttachmentCreatorImpl.class.getName());
+
+	private final Application app;
+	@IoExecutor
+	private final Executor ioExecutor;
+	private final MessagingManager messagingManager;
+	private final AttachmentRetriever retriever;
+	private final ImageSizeCalculator imageSizeCalculator;
+
+	private final CopyOnWriteArrayList<Uri> uris = new CopyOnWriteArrayList<>();
+	private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
+			new CopyOnWriteArrayList<>();
+
+	@Nullable
+	private AttachmentCreationTask task;
+
+	@Nullable
+	private volatile MutableLiveData<AttachmentResult> result;
+
+	@Inject
+	AttachmentCreatorImpl(Application app, @IoExecutor Executor ioExecutor,
+			MessagingManager messagingManager, AttachmentRetriever retriever,
+			ImageSizeCalculator imageSizeCalculator) {
+		this.app = app;
+		this.ioExecutor = ioExecutor;
+		this.messagingManager = messagingManager;
+		this.retriever = retriever;
+		this.imageSizeCalculator = imageSizeCalculator;
+	}
+
+	@Override
+	@UiThread
+	public LiveData<AttachmentResult> storeAttachments(
+			LiveData<GroupId> groupId, Collection<Uri> newUris) {
+		if (task != null || result != null || !uris.isEmpty())
+			throw new IllegalStateException();
+		MutableLiveData<AttachmentResult> result = new MutableLiveData<>();
+		this.result = result;
+		uris.addAll(newUris);
+		observeForeverOnce(groupId, id -> {
+			if (id == null) throw new IllegalStateException();
+			boolean needsSize = uris.size() == 1;
+			task = new AttachmentCreationTask(messagingManager,
+					app.getContentResolver(), this, imageSizeCalculator, id,
+					uris, needsSize);
+			ioExecutor.execute(() -> task.storeAttachments());
+		});
+		return result;
+	}
+
+	@Override
+	@UiThread
+	public LiveData<AttachmentResult> getLiveAttachments() {
+		MutableLiveData<AttachmentResult> result = this.result;
+		if (task == null || result == null || uris.isEmpty())
+			throw new IllegalStateException();
+		// A task is already running. It will update the result LiveData.
+		// So nothing more to do here.
+		return result;
+	}
+
+	@Override
+	@IoExecutor
+	public void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
+			boolean needsSize) {
+		// get and cache AttachmentItem for ImagePreview
+		try {
+			Attachment a = retriever.getMessageAttachment(h);
+			AttachmentItem item = retriever.getAttachmentItem(a, needsSize);
+			if (item.hasError()) throw new IOException();
+			AttachmentItemResult itemResult =
+					new AttachmentItemResult(uri, item);
+			itemResults.add(itemResult);
+			MutableLiveData<AttachmentResult> result = this.result;
+			if (result != null) result.postValue(getResult(false));
+		} catch (IOException | DbException e) {
+			logException(LOG, WARNING, e);
+			onAttachmentError(uri, e);
+		}
+	}
+
+	@Override
+	@IoExecutor
+	public void onAttachmentError(Uri uri, Throwable t) {
+		// get error message
+		String errorMsg;
+		if (t instanceof UnsupportedMimeTypeException) {
+			String mimeType = ((UnsupportedMimeTypeException) t).getMimeType();
+			errorMsg = app.getString(
+					R.string.image_attach_error_invalid_mime_type, mimeType);
+		} else if (t instanceof FileTooBigException) {
+			int mb = MAX_IMAGE_SIZE / 1024 / 1024;
+			errorMsg = app.getString(R.string.image_attach_error_too_big, mb);
+		} else {
+			errorMsg = null; // generic error
+		}
+		AttachmentItemResult itemResult =
+				new AttachmentItemResult(uri, errorMsg);
+		itemResults.add(itemResult);
+		MutableLiveData<AttachmentResult> result = this.result;
+		if (result != null) result.postValue(getResult(false));
+		// expect to receive a cancel from the UI
+	}
+
+	@Override
+	@IoExecutor
+	public void onAttachmentCreationFinished() {
+		MutableLiveData<AttachmentResult> result = this.result;
+		if (result != null) result.postValue(getResult(true));
+	}
+
+	@Override
+	@UiThread
+	public List<AttachmentHeader> getAttachmentHeadersForSending() {
+		List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
+		for (AttachmentItemResult itemResult : itemResults) {
+			// check if we are trying to send attachment items with errors
+			if (itemResult.getItem() == null) throw new IllegalStateException();
+			headers.add(itemResult.getItem().getHeader());
+		}
+		return headers;
+	}
+
+	@Override
+	@UiThread
+	public void onAttachmentsSent(MessageId id) {
+		List<AttachmentItem> items = new ArrayList<>(itemResults.size());
+		for (AttachmentItemResult itemResult : itemResults) {
+			// check if we are trying to send attachment items with errors
+			if (itemResult.getItem() == null) throw new IllegalStateException();
+			items.add(itemResult.getItem());
+		}
+		retriever.cachePut(id, items);
+		resetState();
+	}
+
+	@Override
+	@UiThread
+	public void cancel() {
+		if (task == null) throw new AssertionError();
+		task.cancel();
+		deleteUnsentAttachments();
+		resetState();
+	}
+
+	@UiThread
+	private void resetState() {
+		task = null;
+		uris.clear();
+		itemResults.clear();
+		MutableLiveData<AttachmentResult> result = this.result;
+		if (result != null) {
+			result.setValue(null);
+			this.result = null;
+		}
+	}
+
+	@Override
+	@UiThread
+	public void deleteUnsentAttachments() {
+		// Make a copy for the IoExecutor as we clear the itemResults soon
+		List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
+		for (AttachmentItemResult itemResult : itemResults) {
+			// check if we are trying to send attachment items with errors
+			if (itemResult.getItem() != null)
+				headers.add(itemResult.getItem().getHeader());
+		}
+		ioExecutor.execute(() -> {
+			for (AttachmentHeader header : headers) {
+				try {
+					messagingManager.removeAttachment(header);
+				} catch (DbException e) {
+					logException(LOG, WARNING, e);
+				}
+			}
+		});
+	}
+
+	private AttachmentResult getResult(boolean finished) {
+		// Make a copy of the list,
+		// because our copy will continue to change in the background.
+		// (As it's a CopyOnWriteArrayList,
+		//  the code that receives the result can safely do simple things
+		//  like iterating over the list,
+		//  but anything that involves calling more than one list method
+		//  is still unsafe.)
+		Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
+		return new AttachmentResult(items, finished);
+	}
+
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java
index 9c60101ee923f4314efb41f5650f03c76f40332a..1fda393939cdfb0693911cdad6e15d6e1a5444c5 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java
@@ -10,7 +10,7 @@ import javax.annotation.concurrent.Immutable;
 
 @Immutable
 @NotNullByDefault
-public class AttachmentDimensions {
+class AttachmentDimensions {
 
 	final int defaultSize;
 	final int minWidth, maxWidth;
@@ -26,7 +26,7 @@ public class AttachmentDimensions {
 		this.maxHeight = maxHeight;
 	}
 
-	public static AttachmentDimensions getAttachmentDimensions(Resources res) {
+	static AttachmentDimensions getAttachmentDimensions(Resources res) {
 		int defaultSize =
 				res.getDimensionPixelSize(R.dimen.message_bubble_image_default);
 		int minWidth = res.getDimensionPixelSize(
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentManager.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentManager.java
index f69f6085ae98782088a6e7172bd71ef6f1f463ca..208d1de71611b566b38bfe1c11ef38941a08374a 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentManager.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentManager.java
@@ -4,12 +4,14 @@ import android.arch.lifecycle.LiveData;
 import android.net.Uri;
 import android.support.annotation.UiThread;
 
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.briar.api.messaging.AttachmentHeader;
 
 import java.util.Collection;
 import java.util.List;
 
 @UiThread
+@NotNullByDefault
 public interface AttachmentManager {
 
 	LiveData<AttachmentResult> storeAttachments(Collection<Uri> uri,
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentModule.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..57ae44f565f8eddf9c777d779c5fd655e103015c
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentModule.java
@@ -0,0 +1,43 @@
+package org.briarproject.briar.android.attachment;
+
+import android.app.Application;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
+
+@Module
+public class AttachmentModule {
+
+	@Provides
+	ImageHelper provideImageHelper(ImageHelperImpl imageHelper) {
+		return imageHelper;
+	}
+
+	@Provides
+	ImageSizeCalculator provideImageSizeCalculator(ImageHelper imageHelper) {
+		return new ImageSizeCalculator(imageHelper);
+	}
+
+	@Provides
+	AttachmentDimensions provideAttachmentDimensions(Application app) {
+		return getAttachmentDimensions(app.getResources());
+	}
+
+	@Provides
+	@Singleton
+	AttachmentRetriever provideAttachmentRetriever(
+			AttachmentRetrieverImpl attachmentRetriever) {
+		return attachmentRetriever;
+	}
+
+	@Provides
+	@Singleton
+	AttachmentCreator provideAttachmentCreator(
+			AttachmentCreatorImpl attachmentCreator) {
+		return attachmentCreator;
+	}
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java
index 31ba87e4a01aea8404e367ff533a9a527c7806e3..93bbcfba377e1b31d973cda44f6bbfcd06d5c1df 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java
@@ -1,241 +1,29 @@
 package org.briarproject.briar.android.attachment;
 
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapFactory.Options;
 import android.support.annotation.Nullable;
-import android.support.annotation.VisibleForTesting;
-import android.support.media.ExifInterface;
-import android.webkit.MimeTypeMap;
-
-import com.bumptech.glide.util.MarkEnforcingInputStream;
 
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.MessageId;
-import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
 import org.briarproject.briar.api.messaging.Attachment;
 import org.briarproject.briar.api.messaging.AttachmentHeader;
-import org.briarproject.briar.api.messaging.MessagingManager;
 
-import java.io.BufferedInputStream;
-import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.logging.Logger;
-
-import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
-import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
-import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
-import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
-import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
-import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
-import static android.support.media.ExifInterface.TAG_ORIENTATION;
-import static java.util.logging.Level.WARNING;
-import static java.util.logging.Logger.getLogger;
-import static org.briarproject.bramble.util.IoUtils.tryToClose;
-import static org.briarproject.bramble.util.LogUtils.logException;
 
 @NotNullByDefault
-public class AttachmentRetriever {
-
-	private static final Logger LOG =
-			getLogger(AttachmentRetriever.class.getName());
-	private static final int READ_LIMIT = 1024 * 8192;
-
-	private final MessagingManager messagingManager;
-	private final ImageHelper imageHelper;
-	private final int defaultSize;
-	private final int minWidth, maxWidth;
-	private final int minHeight, maxHeight;
-
-	private final Map<MessageId, List<AttachmentItem>> attachmentCache =
-			new ConcurrentHashMap<>();
-
-	@VisibleForTesting
-	AttachmentRetriever(MessagingManager messagingManager,
-			AttachmentDimensions dimensions, ImageHelper imageHelper) {
-		this.messagingManager = messagingManager;
-		this.imageHelper = imageHelper;
-		defaultSize = dimensions.defaultSize;
-		minWidth = dimensions.minWidth;
-		maxWidth = dimensions.maxWidth;
-		minHeight = dimensions.minHeight;
-		maxHeight = dimensions.maxHeight;
-	}
+public interface AttachmentRetriever {
 
-	public AttachmentRetriever(MessagingManager messagingManager,
-			AttachmentDimensions dimensions) {
-		this(messagingManager, dimensions, new ImageHelper() {
-			@Override
-			public DecodeResult decodeStream(InputStream is) {
-				Options options = new Options();
-				options.inJustDecodeBounds = true;
-				BitmapFactory.decodeStream(is, null, options);
-				String mimeType = options.outMimeType;
-				if (mimeType == null) mimeType = "";
-				return new DecodeResult(options.outWidth, options.outHeight,
-						mimeType);
-			}
-
-			@Nullable
-			@Override
-			public String getExtensionFromMimeType(String mimeType) {
-				MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
-				return mimeTypeMap.getExtensionFromMimeType(mimeType);
-			}
-		});
-	}
-
-	public void cachePut(MessageId messageId,
-			List<AttachmentItem> attachments) {
-		attachmentCache.put(messageId, attachments);
-	}
+	void cachePut(MessageId messageId, List<AttachmentItem> attachments);
 
 	@Nullable
-	public List<AttachmentItem> cacheGet(MessageId messageId) {
-		return attachmentCache.get(messageId);
-	}
+	List<AttachmentItem> cacheGet(MessageId messageId);
 
-	public Attachment getMessageAttachment(AttachmentHeader h)
-			throws DbException {
-		return messagingManager.getAttachment(h);
-	}
+	Attachment getMessageAttachment(AttachmentHeader h) throws DbException;
 
 	/**
 	 * Creates an {@link AttachmentItem} from the {@link Attachment}'s
 	 * {@link InputStream} which will be closed when this method returns.
 	 */
-	public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
-		AttachmentHeader h = a.getHeader();
-		if (!needsSize) {
-			String extension =
-					imageHelper.getExtensionFromMimeType(h.getContentType());
-			boolean hasError = false;
-			if (extension == null) {
-				extension = "";
-				hasError = true;
-			}
-			return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
-		}
-
-		Size size = new Size();
-		InputStream is = new MarkEnforcingInputStream(
-				new BufferedInputStream(a.getStream()));
-		is.mark(READ_LIMIT);
-		try {
-			// use exif to get size
-			if (h.getContentType().equals("image/jpeg")) {
-				size = getSizeFromExif(is);
-			}
-		} catch (IOException e) {
-			logException(LOG, WARNING, e);
-		}
-		try {
-			// use BitmapFactory to get size
-			if (size.error) {
-				is.reset();
-				// need to mark again to re-add read limit
-				is.mark(READ_LIMIT);
-				size = getSizeFromBitmap(is);
-			}
-		} catch (IOException e) {
-			logException(LOG, WARNING, e);
-		} finally {
-			tryToClose(is, LOG, WARNING);
-		}
-
-		// calculate thumbnail size
-		Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
-		if (!size.error) {
-			thumbnailSize =
-					getThumbnailSize(size.width, size.height, size.mimeType);
-		}
-		// get file extension
-		String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
-		boolean hasError = extension == null || size.error;
-		if (!h.getContentType().equals(size.mimeType)) {
-			if (LOG.isLoggable(WARNING)) {
-				LOG.warning("Header has different mime type (" +
-						h.getContentType() + ") than image (" + size.mimeType +
-						").");
-			}
-			hasError = true;
-		}
-		if (extension == null) extension = "";
-		return new AttachmentItem(h, size.width, size.height, extension,
-				thumbnailSize.width, thumbnailSize.height, hasError);
-	}
-
-	/**
-	 * Gets the size of a JPEG {@link InputStream} if EXIF info is available.
-	 */
-	private Size getSizeFromExif(InputStream is) throws IOException {
-		ExifInterface exif = new ExifInterface(is);
-		// these can return 0 independent of default value
-		int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
-		int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
-		if (width == 0 || height == 0) return new Size();
-		int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
-		if (orientation == ORIENTATION_ROTATE_90 ||
-				orientation == ORIENTATION_ROTATE_270 ||
-				orientation == ORIENTATION_TRANSVERSE ||
-				orientation == ORIENTATION_TRANSPOSE) {
-			//noinspection SuspiciousNameCombination
-			return new Size(height, width, "image/jpeg");
-		}
-		return new Size(width, height, "image/jpeg");
-	}
-
-	/**
-	 * Gets the size of any image {@link InputStream}.
-	 */
-	private Size getSizeFromBitmap(InputStream is) {
-		DecodeResult result = imageHelper.decodeStream(is);
-		if (result.width < 1 || result.height < 1) return new Size();
-		return new Size(result.width, result.height, result.mimeType);
-	}
-
-	private Size getThumbnailSize(int width, int height, String mimeType) {
-		float widthPercentage = maxWidth / (float) width;
-		float heightPercentage = maxHeight / (float) height;
-		float scaleFactor = Math.min(widthPercentage, heightPercentage);
-		if (scaleFactor > 1) scaleFactor = 1f;
-		int thumbnailWidth = (int) (width * scaleFactor);
-		int thumbnailHeight = (int) (height * scaleFactor);
-		if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
-			widthPercentage = minWidth / (float) width;
-			heightPercentage = minHeight / (float) height;
-			scaleFactor = Math.max(widthPercentage, heightPercentage);
-			thumbnailWidth = (int) (width * scaleFactor);
-			thumbnailHeight = (int) (height * scaleFactor);
-			if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
-			if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
-		}
-		return new Size(thumbnailWidth, thumbnailHeight, mimeType);
-	}
-
-	private static class Size {
-
-		private final int width;
-		private final int height;
-		private final String mimeType;
-		private final boolean error;
-
-		private Size(int width, int height, String mimeType) {
-			this.width = width;
-			this.height = height;
-			this.mimeType = mimeType;
-			this.error = false;
-		}
-
-		private Size() {
-			this.width = 0;
-			this.height = 0;
-			this.mimeType = "";
-			this.error = true;
-		}
-	}
-
+	AttachmentItem getAttachmentItem(Attachment a, boolean needsSize);
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..2bef6e5192caa62d42306c46756941383e4618b6
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetrieverImpl.java
@@ -0,0 +1,129 @@
+package org.briarproject.briar.android.attachment;
+
+import android.support.annotation.Nullable;
+
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.messaging.Attachment;
+import org.briarproject.briar.api.messaging.AttachmentHeader;
+import org.briarproject.briar.api.messaging.MessagingManager;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+
+@NotNullByDefault
+class AttachmentRetrieverImpl implements AttachmentRetriever {
+
+	private static final Logger LOG =
+			getLogger(AttachmentRetrieverImpl.class.getName());
+
+	private final MessagingManager messagingManager;
+	private final ImageHelper imageHelper;
+	private final ImageSizeCalculator imageSizeCalculator;
+	private final int defaultSize;
+	private final int minWidth, maxWidth;
+	private final int minHeight, maxHeight;
+
+	private final Map<MessageId, List<AttachmentItem>> attachmentCache =
+			new ConcurrentHashMap<>();
+
+	@Inject
+	AttachmentRetrieverImpl(MessagingManager messagingManager,
+			AttachmentDimensions dimensions, ImageHelper imageHelper,
+			ImageSizeCalculator imageSizeCalculator) {
+		this.messagingManager = messagingManager;
+		this.imageHelper = imageHelper;
+		this.imageSizeCalculator = imageSizeCalculator;
+		defaultSize = dimensions.defaultSize;
+		minWidth = dimensions.minWidth;
+		maxWidth = dimensions.maxWidth;
+		minHeight = dimensions.minHeight;
+		maxHeight = dimensions.maxHeight;
+	}
+
+	@Override
+	public void cachePut(MessageId messageId,
+			List<AttachmentItem> attachments) {
+		attachmentCache.put(messageId, attachments);
+	}
+
+	@Override
+	@Nullable
+	public List<AttachmentItem> cacheGet(MessageId messageId) {
+		return attachmentCache.get(messageId);
+	}
+
+	@Override
+	public Attachment getMessageAttachment(AttachmentHeader h)
+			throws DbException {
+		return messagingManager.getAttachment(h);
+	}
+
+	@Override
+	public AttachmentItem getAttachmentItem(Attachment a, boolean needsSize) {
+		AttachmentHeader h = a.getHeader();
+		if (!needsSize) {
+			String extension =
+					imageHelper.getExtensionFromMimeType(h.getContentType());
+			boolean hasError = false;
+			if (extension == null) {
+				extension = "";
+				hasError = true;
+			}
+			return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError);
+		}
+
+		InputStream is = new BufferedInputStream(a.getStream());
+		Size size = imageSizeCalculator.getSize(is, h.getContentType());
+
+		// calculate thumbnail size
+		Size thumbnailSize = new Size(defaultSize, defaultSize, size.mimeType);
+		if (!size.error) {
+			thumbnailSize =
+					getThumbnailSize(size.width, size.height, size.mimeType);
+		}
+		// get file extension
+		String extension = imageHelper.getExtensionFromMimeType(size.mimeType);
+		boolean hasError = extension == null || size.error;
+		if (!h.getContentType().equals(size.mimeType)) {
+			if (LOG.isLoggable(WARNING)) {
+				LOG.warning("Header has different mime type (" +
+						h.getContentType() + ") than image (" + size.mimeType +
+						").");
+			}
+			hasError = true;
+		}
+		if (extension == null) extension = "";
+		return new AttachmentItem(h, size.width, size.height, extension,
+				thumbnailSize.width, thumbnailSize.height, hasError);
+	}
+
+	private Size getThumbnailSize(int width, int height, String mimeType) {
+		float widthPercentage = maxWidth / (float) width;
+		float heightPercentage = maxHeight / (float) height;
+		float scaleFactor = Math.min(widthPercentage, heightPercentage);
+		if (scaleFactor > 1) scaleFactor = 1f;
+		int thumbnailWidth = (int) (width * scaleFactor);
+		int thumbnailHeight = (int) (height * scaleFactor);
+		if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) {
+			widthPercentage = minWidth / (float) width;
+			heightPercentage = minHeight / (float) height;
+			scaleFactor = Math.max(widthPercentage, heightPercentage);
+			thumbnailWidth = (int) (width * scaleFactor);
+			thumbnailHeight = (int) (height * scaleFactor);
+			if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth;
+			if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight;
+		}
+		return new Size(thumbnailWidth, thumbnailHeight, mimeType);
+	}
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java
index 1264d65110692100f1310c25a8970cb09d1af36c..e1ea0776d6f7f2c18111cc61157c00b2acbe316d 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java
@@ -7,7 +7,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import java.io.InputStream;
 
 @NotNullByDefault
-interface ImageHelper {
+public interface ImageHelper {
 
 	DecodeResult decodeStream(InputStream is);
 
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelperImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelperImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..86a2ed39af90a74f4e7f46206ec4f17c62b9b4f3
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelperImpl.java
@@ -0,0 +1,39 @@
+package org.briarproject.briar.android.attachment;
+
+import android.graphics.BitmapFactory;
+import android.support.annotation.Nullable;
+import android.webkit.MimeTypeMap;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import java.io.InputStream;
+
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+@Immutable
+@NotNullByDefault
+class ImageHelperImpl implements ImageHelper {
+
+	@Inject
+	ImageHelperImpl() {
+	}
+
+	@Override
+	public DecodeResult decodeStream(InputStream is) {
+		BitmapFactory.Options options = new BitmapFactory.Options();
+		options.inJustDecodeBounds = true;
+		BitmapFactory.decodeStream(is, null, options);
+		String mimeType = options.outMimeType;
+		if (mimeType == null) mimeType = "";
+		return new DecodeResult(options.outWidth, options.outHeight,
+				mimeType);
+	}
+
+	@Nullable
+	@Override
+	public String getExtensionFromMimeType(String mimeType) {
+		MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+		return mimeTypeMap.getExtensionFromMimeType(mimeType);
+	}
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageSizeCalculator.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageSizeCalculator.java
new file mode 100644
index 0000000000000000000000000000000000000000..41b3094cbce48ab222a904c350d9f2b3e87fb6a8
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageSizeCalculator.java
@@ -0,0 +1,94 @@
+package org.briarproject.briar.android.attachment;
+
+import android.support.media.ExifInterface;
+
+import com.bumptech.glide.util.MarkEnforcingInputStream;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+
+import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270;
+import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90;
+import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE;
+import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE;
+import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH;
+import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH;
+import static android.support.media.ExifInterface.TAG_ORIENTATION;
+import static java.util.logging.Level.WARNING;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.bramble.util.LogUtils.logException;
+
+@NotNullByDefault
+class ImageSizeCalculator {
+
+	private static final Logger LOG =
+			getLogger(ImageSizeCalculator.class.getName());
+
+	private static final int READ_LIMIT = 1024 * 8192;
+
+	private final ImageHelper imageHelper;
+
+	ImageSizeCalculator(ImageHelper imageHelper) {
+		this.imageHelper = imageHelper;
+	}
+
+	Size getSize(InputStream is, String contentType) {
+		Size size = new Size();
+		is = new MarkEnforcingInputStream(is);
+		is.mark(READ_LIMIT);
+		if (contentType.equals("image/jpeg")) {
+			try {
+				// use exif to get size
+				size = getSizeFromExif(is);
+				is.reset();
+			} catch (IOException e) {
+				logException(LOG, WARNING, e);
+			}
+		}
+		if (size.error) {
+			// need to mark again to re-add read limit
+			is.mark(READ_LIMIT);
+			try {
+				// use BitmapFactory to get size
+				size = getSizeFromBitmap(is);
+				is.reset();
+			} catch (IOException e) {
+				logException(LOG, WARNING, e);
+			}
+		}
+		return size;
+	}
+
+	/**
+	 * Gets the size of a JPEG {@link InputStream} if EXIF info is available.
+	 */
+	private Size getSizeFromExif(InputStream is) throws IOException {
+		ExifInterface exif = new ExifInterface(is);
+		// these can return 0 independent of default value
+		int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0);
+		int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0);
+		if (width == 0 || height == 0) return new Size();
+		int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0);
+		if (orientation == ORIENTATION_ROTATE_90 ||
+				orientation == ORIENTATION_ROTATE_270 ||
+				orientation == ORIENTATION_TRANSVERSE ||
+				orientation == ORIENTATION_TRANSPOSE) {
+			//noinspection SuspiciousNameCombination
+			return new Size(height, width, "image/jpeg");
+		}
+		return new Size(width, height, "image/jpeg");
+	}
+
+	/**
+	 * Gets the size of any image {@link InputStream}.
+	 */
+	private Size getSizeFromBitmap(InputStream is) {
+		DecodeResult result = imageHelper.decodeStream(is);
+		if (result.width < 1 || result.height < 1) return new Size();
+		return new Size(result.width, result.height, result.mimeType);
+	}
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/Size.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/Size.java
new file mode 100644
index 0000000000000000000000000000000000000000..22871e1a55b8fbfa1631d2ba52eb8eb0da4401db
--- /dev/null
+++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/Size.java
@@ -0,0 +1,23 @@
+package org.briarproject.briar.android.attachment;
+
+class Size {
+
+	final int width;
+	final int height;
+	final String mimeType;
+	final boolean error;
+
+	Size(int width, int height, String mimeType) {
+		this.width = width;
+		this.height = height;
+		this.mimeType = mimeType;
+		this.error = false;
+	}
+
+	Size() {
+		this.width = 0;
+		this.height = 0;
+		this.mimeType = "";
+		this.error = true;
+	}
+}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java
index fc565d3a26e4e89e6f05a218c8f4782623e64e11..cb1c4128aaf7af1e843387cf341f9d95627d505c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java
@@ -18,7 +18,6 @@ import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.NoSuchContactException;
 import org.briarproject.bramble.api.db.TransactionManager;
 import org.briarproject.bramble.api.identity.AuthorId;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.settings.Settings;
 import org.briarproject.bramble.api.settings.SettingsManager;
@@ -51,7 +50,6 @@ import static java.util.logging.Logger.getLogger;
 import static org.briarproject.bramble.util.LogUtils.logDuration;
 import static org.briarproject.bramble.util.LogUtils.logException;
 import static org.briarproject.bramble.util.LogUtils.now;
-import static org.briarproject.briar.android.attachment.AttachmentDimensions.getAttachmentDimensions;
 import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
 import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
 
@@ -101,10 +99,13 @@ public class ConversationViewModel extends AndroidViewModel
 	@Inject
 	ConversationViewModel(Application application,
 			@DatabaseExecutor Executor dbExecutor,
-			@IoExecutor Executor ioExecutor, TransactionManager db,
-			MessagingManager messagingManager, ContactManager contactManager,
+			TransactionManager db,
+			MessagingManager messagingManager,
+			ContactManager contactManager,
 			SettingsManager settingsManager,
-			PrivateMessageFactory privateMessageFactory) {
+			PrivateMessageFactory privateMessageFactory,
+			AttachmentRetriever attachmentRetriever,
+			AttachmentCreator attachmentCreator) {
 		super(application);
 		this.dbExecutor = dbExecutor;
 		this.db = db;
@@ -112,10 +113,8 @@ public class ConversationViewModel extends AndroidViewModel
 		this.contactManager = contactManager;
 		this.settingsManager = settingsManager;
 		this.privateMessageFactory = privateMessageFactory;
-		this.attachmentRetriever = new AttachmentRetriever(messagingManager,
-				getAttachmentDimensions(application.getResources()));
-		this.attachmentCreator = new AttachmentCreator(getApplication(),
-				ioExecutor, messagingManager, attachmentRetriever);
+		this.attachmentRetriever = attachmentRetriever;
+		this.attachmentCreator = attachmentCreator;
 		messagingGroupId = Transformations
 				.map(contact, c -> messagingManager.getContactGroup(c).getId());
 		contactDeleted.setValue(false);
diff --git a/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java
index 67670e8465ac63e6b86ab815f60dc9f93199da5a..4558c2ff8dd0e067070e88340d282e16c54b0c1c 100644
--- a/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java
+++ b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java
@@ -2,11 +2,11 @@ package org.briarproject.briar.android.attachment;
 
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.test.BrambleMockTestCase;
-import org.briarproject.briar.android.attachment.ImageHelper.DecodeResult;
 import org.briarproject.briar.api.messaging.Attachment;
 import org.briarproject.briar.api.messaging.AttachmentHeader;
 import org.briarproject.briar.api.messaging.MessagingManager;
 import org.jmock.Expectations;
+import org.jmock.lib.legacy.ClassImposteriser;
 import org.junit.Test;
 
 import java.io.ByteArrayInputStream;
@@ -24,14 +24,18 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
 			100, 50, 200, 75, 300
 	);
 	private final MessageId msgId = new MessageId(getRandomId());
-	private final MessagingManager messagingManager =
-			context.mock(MessagingManager.class);
 	private final ImageHelper imageHelper = context.mock(ImageHelper.class);
-	private final AttachmentRetriever retriever = new AttachmentRetriever(
-			messagingManager,
-			dimensions,
-			imageHelper
-	);
+	private final ImageSizeCalculator imageSizeCalculator;
+	private final AttachmentRetriever retriever;
+
+	public AttachmentRetrieverTest() {
+		context.setImposteriser(ClassImposteriser.INSTANCE);
+		MessagingManager messagingManager =
+				context.mock(MessagingManager.class);
+		imageSizeCalculator = context.mock(ImageSizeCalculator.class);
+		retriever = new AttachmentRetrieverImpl(messagingManager, dimensions,
+				imageHelper, imageSizeCalculator);
+	}
 
 	@Test
 	public void testNoSize() {
@@ -69,8 +73,9 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
 		Attachment attachment = getAttachment(mimeType);
 
 		context.checking(new Expectations() {{
-			oneOf(imageHelper).decodeStream(with(any(InputStream.class)));
-			will(returnValue(new DecodeResult(160, 240, mimeType)));
+			oneOf(imageSizeCalculator).getSize(with(any(InputStream.class)),
+					with(mimeType));
+			will(returnValue(new Size(160, 240, mimeType)));
 			oneOf(imageHelper).getExtensionFromMimeType(mimeType);
 			will(returnValue("jpg"));
 		}});
@@ -92,8 +97,9 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
 		Attachment attachment = getAttachment(mimeType);
 
 		context.checking(new Expectations() {{
-			oneOf(imageHelper).decodeStream(with(any(InputStream.class)));
-			will(returnValue(new DecodeResult(1728, 2592, mimeType)));
+			oneOf(imageSizeCalculator).getSize(with(any(InputStream.class)),
+					with(mimeType));
+			will(returnValue(new Size(1728, 2592, mimeType)));
 			oneOf(imageHelper).getExtensionFromMimeType(mimeType);
 			will(returnValue("jpg"));
 		}});
@@ -108,11 +114,13 @@ public class AttachmentRetrieverTest extends BrambleMockTestCase {
 
 	@Test
 	public void testMalformedError() {
-		Attachment attachment = getAttachment("image/jpeg");
+		String mimeType = "image/jpeg";
+		Attachment attachment = getAttachment(mimeType);
 
 		context.checking(new Expectations() {{
-			oneOf(imageHelper).decodeStream(with(any(InputStream.class)));
-			will(returnValue(new DecodeResult(0, 0, "")));
+			oneOf(imageSizeCalculator).getSize(with(any(InputStream.class)),
+					with(mimeType));
+			will(returnValue(new Size()));
 			oneOf(imageHelper).getExtensionFromMimeType("");
 			will(returnValue(null));
 		}});