diff --git a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/conversation/AttachmentControllerIntegrationTest.java b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java similarity index 89% rename from briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/conversation/AttachmentControllerIntegrationTest.java rename to briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java index d639230b8..eea2427f3 100644 --- a/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/conversation/AttachmentControllerIntegrationTest.java +++ b/briar-android/src/androidTestOfficial/java/org/briarproject/briar/android/attachment/AttachmentRetrieverIntegrationTest.java @@ -1,4 +1,4 @@ -package org.briarproject.briar.android.conversation; +package org.briarproject.briar.android.attachment; import android.content.res.AssetManager; import android.support.test.InstrumentationRegistry; @@ -21,7 +21,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) -public class AttachmentControllerIntegrationTest { +public class AttachmentRetrieverIntegrationTest { private static final String smallKitten = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg/160px-Kitten_in_Rizal_Park%2C_Manila.jpg"; @@ -47,15 +47,15 @@ public class AttachmentControllerIntegrationTest { ); private final MessageId msgId = new MessageId(getRandomId()); - private final AttachmentController controller = - new AttachmentController(null, dimensions); + private final AttachmentRetriever retriever = + new AttachmentRetriever(null, dimensions); @Test public void testSmallJpegImage() throws Exception { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(smallKitten); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(msgId, item.getMessageId()); assertEquals(160, item.getWidth()); assertEquals(240, item.getHeight()); @@ -71,7 +71,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(originalKitten); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(msgId, item.getMessageId()); assertEquals(1728, item.getWidth()); assertEquals(2592, item.getHeight()); @@ -87,7 +87,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/png"); InputStream is = getUrlInputStream(pngKitten); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(msgId, item.getMessageId()); assertEquals(737, item.getWidth()); assertEquals(510, item.getHeight()); @@ -103,7 +103,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(uberGif); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(1, item.getWidth()); assertEquals(1, item.getHeight()); assertEquals(dimensions.minHeight, item.getThumbnailWidth()); @@ -118,7 +118,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(lottaPixel); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(64250, item.getWidth()); assertEquals(64250, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); @@ -133,7 +133,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(imageIoCrash); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(1184, item.getWidth()); assertEquals(448, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); @@ -148,7 +148,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(gimpCrash); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(1, item.getWidth()); assertEquals(1, item.getHeight()); assertEquals(dimensions.minHeight, item.getThumbnailWidth()); @@ -163,7 +163,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(optiPngAfl); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(32, item.getWidth()); assertEquals(32, item.getHeight()); assertEquals(dimensions.minHeight, item.getThumbnailWidth()); @@ -178,7 +178,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getUrlInputStream(librawError); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertTrue(item.hasError()); } @@ -187,7 +187,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("animated.gif"); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(65535, item.getWidth()); assertEquals(65535, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); @@ -202,7 +202,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("animated2.gif"); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(10000, item.getWidth()); assertEquals(10000, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); @@ -217,7 +217,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/gif"); InputStream is = getAssetInputStream("error_large.gif"); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(16384, item.getWidth()); assertEquals(16384, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); @@ -232,7 +232,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("error_high.jpg"); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(1, item.getWidth()); assertEquals(10000, item.getHeight()); assertEquals(dimensions.minWidth, item.getThumbnailWidth()); @@ -247,7 +247,7 @@ public class AttachmentControllerIntegrationTest { AttachmentHeader h = new AttachmentHeader(msgId, "image/jpeg"); InputStream is = getAssetInputStream("error_wide.jpg"); Attachment a = new Attachment(is); - AttachmentItem item = controller.getAttachmentItem(h, a, true); + AttachmentItem item = retriever.getAttachmentItem(h, a, true); assertEquals(1920, item.getWidth()); assertEquals(1, item.getHeight()); assertEquals(dimensions.maxWidth, item.getThumbnailWidth()); 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 new file mode 100644 index 000000000..d9e77d1d8 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreationTask.java @@ -0,0 +1,116 @@ +package org.briarproject.briar.android.attachment; + +import android.content.ContentResolver; +import android.net.Uri; +import android.support.annotation.Nullable; + +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.briar.api.messaging.AttachmentHeader; +import org.briarproject.briar.api.messaging.MessagingManager; +import org.jsoup.UnsupportedMimeTypeException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +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.IoUtils.tryToClose; +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; + +@NotNullByDefault +class AttachmentCreationTask { + + private static Logger LOG = + getLogger(AttachmentCreationTask.class.getName()); + + private final MessagingManager messagingManager; + private final ContentResolver contentResolver; + private final GroupId groupId; + private final Collection uris; + private final boolean needsSize; + @Nullable + private volatile AttachmentCreator attachmentCreator; + + private volatile boolean canceled = false; + + AttachmentCreationTask(MessagingManager messagingManager, + ContentResolver contentResolver, + AttachmentCreator attachmentCreator, GroupId groupId, + Collection uris, boolean needsSize) { + this.messagingManager = messagingManager; + this.contentResolver = contentResolver; + this.groupId = groupId; + this.uris = uris; + this.needsSize = needsSize; + this.attachmentCreator = attachmentCreator; + } + + public void cancel() { + canceled = true; + attachmentCreator = null; + } + + @IoExecutor + public void storeAttachments() { + for (Uri uri: uris) processUri(uri); + AttachmentCreator attachmentCreator = this.attachmentCreator; + if (!canceled && attachmentCreator != null) + attachmentCreator.onAttachmentCreationFinished(); + this.attachmentCreator = null; + } + + @IoExecutor + private void processUri(Uri uri) { + if (canceled) return; + try { + AttachmentHeader h = storeAttachment(uri); + AttachmentCreator attachmentCreator = this.attachmentCreator; + if (attachmentCreator != null) { + attachmentCreator.onAttachmentHeaderReceived(uri, h, needsSize); + } + } catch (DbException | IOException e) { + logException(LOG, WARNING, e); + AttachmentCreator attachmentCreator = this.attachmentCreator; + if (attachmentCreator != null) { + attachmentCreator.onAttachmentError(uri, e); + } + canceled = true; + } + } + + @IoExecutor + private AttachmentHeader storeAttachment(Uri uri) + throws IOException, DbException { + long start = now(); + String contentType = contentResolver.getType(uri); + if (contentType == null) throw new IOException("null content type"); + if (!isValidMimeType(contentType)) { + String uriString = uri.toString(); + throw new UnsupportedMimeTypeException("", contentType, uriString); + } + InputStream is = contentResolver.openInputStream(uri); + if (is == null) throw new IOException(); + long timestamp = System.currentTimeMillis(); + AttachmentHeader h = messagingManager + .addLocalAttachment(groupId, timestamp, contentType, is); + tryToClose(is, LOG, WARNING); + logDuration(LOG, "Storing attachment", start); + return h; + } + + private boolean isValidMimeType(String mimeType) { + for (String supportedType : IMAGE_MIME_TYPES) { + if (supportedType.equals(mimeType)) return true; + } + return false; + } + +} 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 new file mode 100644 index 000000000..c22d46024 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentCreator.java @@ -0,0 +1,218 @@ +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 uris = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList itemResults = + new CopyOnWriteArrayList<>(); + + private final MutableLiveData result = + new MutableLiveData<>(); + @Nullable + private AttachmentCreationTask task; + + public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor, + MessagingManager messagingManager, AttachmentRetriever retriever) { + this.app = app; + this.ioExecutor = ioExecutor; + this.messagingManager = messagingManager; + this.retriever = retriever; + } + + @UiThread + public LiveData storeAttachments( + LiveData groupId, Collection newUris) { + if (task != 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()); + }); + return result; + } + + /** + * This should be only called after configuration changes. + * In this case we should not create new attachments. + * They are already being created and returned by the existing LiveData. + */ + @UiThread + public LiveData getLiveAttachments() { + if (task == 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(h, a, needsSize); + if (item.hasError()) throw new IOException(); + AttachmentItemResult itemResult = + new AttachmentItemResult(uri, item); + itemResults.add(itemResult); + 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); + result.postValue(getResult(false)); + // expect to receive a cancel from the UI + } + + @IoExecutor + void onAttachmentCreationFinished() { + result.postValue(getResult(true)); + } + + @UiThread + public List getAttachmentHeadersForSending() { + List 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; + } + + /** + * Marks the attachments as sent and adds the items to the cache for display + * + * @param id The MessageId of the sent message. + */ + @UiThread + public void onAttachmentsSent(MessageId id) { + List 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(); + } + + /** + * 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(); + } + + @UiThread + private void resetState() { + task = null; + uris.clear(); + itemResults.clear(); + result.setValue(null); + } + + @UiThread + public void deleteUnsentAttachments() { + // Make a copy for the IoExecutor as we clear the itemResults soon + List 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 items = new ArrayList<>(itemResults); + return new AttachmentResult(items, finished); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentDimensions.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java similarity index 75% rename from briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentDimensions.java rename to briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java index 081a6480b..9c60101ee 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentDimensions.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentDimensions.java @@ -1,11 +1,16 @@ -package org.briarproject.briar.android.conversation; +package org.briarproject.briar.android.attachment; import android.content.res.Resources; import android.support.annotation.VisibleForTesting; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; -class AttachmentDimensions { +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +public class AttachmentDimensions { final int defaultSize; final int minWidth, maxWidth; @@ -21,7 +26,7 @@ class AttachmentDimensions { this.maxHeight = maxHeight; } - static AttachmentDimensions getAttachmentDimensions(Resources res) { + public static AttachmentDimensions getAttachmentDimensions(Resources res) { int defaultSize = res.getDimensionPixelSize(R.dimen.message_bubble_image_default); int minWidth = res.getDimensionPixelSize( @@ -33,7 +38,7 @@ class AttachmentDimensions { int maxHeight = res.getDimensionPixelSize( R.dimen.message_bubble_image_max_height); return new AttachmentDimensions(defaultSize, minWidth, maxWidth, - minHeight, minHeight); + minHeight, maxHeight); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java similarity index 71% rename from briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java rename to briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java index 8c109676b..8caafac7c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItem.java @@ -1,4 +1,4 @@ -package org.briarproject.briar.android.conversation; +package org.briarproject.briar.android.attachment; import android.os.Parcel; import android.os.Parcelable; @@ -6,18 +6,21 @@ import android.support.annotation.Nullable; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.concurrent.atomic.AtomicLong; import javax.annotation.concurrent.Immutable; +import static java.util.Objects.requireNonNull; + @Immutable @NotNullByDefault public class AttachmentItem implements Parcelable { - private final MessageId messageId; + private final AttachmentHeader header; private final int width, height; - private final String mimeType, extension; + private final String extension; private final int thumbnailWidth, thumbnailHeight; private final boolean hasError; private final long instanceId; @@ -37,13 +40,12 @@ public class AttachmentItem implements Parcelable { private static final AtomicLong NEXT_INSTANCE_ID = new AtomicLong(0); - AttachmentItem(MessageId messageId, int width, int height, String mimeType, + AttachmentItem(AttachmentHeader header, int width, int height, String extension, int thumbnailWidth, int thumbnailHeight, boolean hasError) { - this.messageId = messageId; + this.header = header; this.width = width; this.height = height; - this.mimeType = mimeType; this.extension = extension; this.thumbnailWidth = thumbnailWidth; this.thumbnailHeight = thumbnailHeight; @@ -54,19 +56,24 @@ public class AttachmentItem implements Parcelable { protected AttachmentItem(Parcel in) { byte[] messageIdByte = new byte[MessageId.LENGTH]; in.readByteArray(messageIdByte); - messageId = new MessageId(messageIdByte); + MessageId messageId = new MessageId(messageIdByte); width = in.readInt(); height = in.readInt(); - mimeType = in.readString(); - extension = in.readString(); + String mimeType = requireNonNull(in.readString()); + extension = requireNonNull(in.readString()); thumbnailWidth = in.readInt(); thumbnailHeight = in.readInt(); hasError = in.readByte() != 0; instanceId = in.readLong(); + header = new AttachmentHeader(messageId, mimeType); + } + + AttachmentHeader getHeader() { + return header; } public MessageId getMessageId() { - return messageId; + return header.getMessageId(); } int getWidth() { @@ -77,27 +84,27 @@ public class AttachmentItem implements Parcelable { return height; } - String getMimeType() { - return mimeType; + public String getMimeType() { + return header.getContentType(); } - String getExtension() { + public String getExtension() { return extension; } - int getThumbnailWidth() { + public int getThumbnailWidth() { return thumbnailWidth; } - int getThumbnailHeight() { + public int getThumbnailHeight() { return thumbnailHeight; } - boolean hasError() { + public boolean hasError() { return hasError; } - String getTransitionName() { + public String getTransitionName() { return String.valueOf(instanceId); } @@ -108,10 +115,10 @@ public class AttachmentItem implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeByteArray(messageId.getBytes()); + dest.writeByteArray(header.getMessageId().getBytes()); dest.writeInt(width); dest.writeInt(height); - dest.writeString(mimeType); + dest.writeString(header.getContentType()); dest.writeString(extension); dest.writeInt(thumbnailWidth); dest.writeInt(thumbnailHeight); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItemResult.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItemResult.java new file mode 100644 index 000000000..1254a851d --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentItemResult.java @@ -0,0 +1,50 @@ +package org.briarproject.briar.android.attachment; + +import android.net.Uri; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +public class AttachmentItemResult { + + private final Uri uri; + @Nullable + private final AttachmentItem item; + @Nullable + private final String errorMsg; + + AttachmentItemResult(Uri uri, AttachmentItem item) { + this.uri = uri; + this.item = item; + this.errorMsg = null; + } + + AttachmentItemResult(Uri uri, @Nullable String errorMsg) { + this.uri = uri; + this.item = null; + this.errorMsg = errorMsg; + } + + public Uri getUri() { + return uri; + } + + @Nullable + public AttachmentItem getItem() { + return item; + } + + public boolean hasError() { + return item == null; + } + + @Nullable + public String getErrorMsg() { + return errorMsg; + } + +} 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 new file mode 100644 index 000000000..f69f6085a --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentManager.java @@ -0,0 +1,22 @@ +package org.briarproject.briar.android.attachment; + +import android.arch.lifecycle.LiveData; +import android.net.Uri; +import android.support.annotation.UiThread; + +import org.briarproject.briar.api.messaging.AttachmentHeader; + +import java.util.Collection; +import java.util.List; + +@UiThread +public interface AttachmentManager { + + LiveData storeAttachments(Collection uri, + boolean restart); + + List getAttachmentHeadersForSending(); + + void cancel(); + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentResult.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentResult.java new file mode 100644 index 000000000..776d2ab59 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentResult.java @@ -0,0 +1,30 @@ +package org.briarproject.briar.android.attachment; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.Collection; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +public class AttachmentResult { + + private final Collection itemResults; + private final boolean finished; + + public AttachmentResult(Collection itemResults, + boolean finished) { + this.itemResults = itemResults; + this.finished = finished; + } + + public Collection getItemResults() { + return itemResults; + } + + public boolean isFinished() { + return finished; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java similarity index 85% rename from briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java rename to briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java index 11833f2ed..d3fb1188b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/AttachmentRetriever.java @@ -1,8 +1,9 @@ -package org.briarproject.briar.android.conversation; +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; @@ -13,7 +14,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor; 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.conversation.ImageHelper.DecodeResult; +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; @@ -42,10 +43,10 @@ import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; @NotNullByDefault -class AttachmentController { +public class AttachmentRetriever { private static final Logger LOG = - getLogger(AttachmentController.class.getName()); + getLogger(AttachmentRetriever.class.getName()); private static final int READ_LIMIT = 1024 * 8192; private final MessagingManager messagingManager; @@ -57,7 +58,8 @@ class AttachmentController { private final Map> attachmentCache = new ConcurrentHashMap<>(); - AttachmentController(MessagingManager messagingManager, + @VisibleForTesting + AttachmentRetriever(MessagingManager messagingManager, AttachmentDimensions dimensions, ImageHelper imageHelper) { this.messagingManager = messagingManager; this.imageHelper = imageHelper; @@ -68,7 +70,7 @@ class AttachmentController { maxHeight = dimensions.maxHeight; } - AttachmentController(MessagingManager messagingManager, + public AttachmentRetriever(MessagingManager messagingManager, AttachmentDimensions dimensions) { this(messagingManager, dimensions, new ImageHelper() { @Override @@ -91,17 +93,17 @@ class AttachmentController { }); } - void put(MessageId messageId, List attachments) { + public void cachePut(MessageId messageId, List attachments) { attachmentCache.put(messageId, attachments); } @Nullable - List get(MessageId messageId) { + public List cacheGet(MessageId messageId) { return attachmentCache.get(messageId); } @DatabaseExecutor - List> getMessageAttachments( + public List> getMessageAttachments( List headers) throws DbException { long start = now(); List> attachments = @@ -110,16 +112,21 @@ class AttachmentController { Attachment a = messagingManager.getAttachment(h.getMessageId()); attachments.add(new Pair<>(h, a)); } - logDuration(LOG, "Loading attachment", start); + logDuration(LOG, "Loading attachments", start); return attachments; } + @DatabaseExecutor + Attachment getMessageAttachment(AttachmentHeader h) throws DbException { + return messagingManager.getAttachment(h.getMessageId()); + } + /** * Creates {@link AttachmentItem}s from the passed headers and Attachments. *

* Note: This closes the {@link Attachment}'s {@link InputStream}. */ - List getAttachmentItems( + public List getAttachmentItems( List> attachments) { boolean needsSize = attachments.size() == 1; List items = new ArrayList<>(attachments.size()); @@ -137,17 +144,15 @@ class AttachmentController { */ AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a, boolean needsSize) { - MessageId messageId = h.getMessageId(); if (!needsSize) { - String mimeType = h.getContentType(); - String extension = imageHelper.getExtensionFromMimeType(mimeType); + String extension = + imageHelper.getExtensionFromMimeType(h.getContentType()); boolean hasError = false; if (extension == null) { extension = ""; hasError = true; } - return new AttachmentItem(messageId, 0, 0, mimeType, extension, 0, - 0, hasError); + return new AttachmentItem(h, 0, 0, extension, 0, 0, hasError); } Size size = new Size(); @@ -185,10 +190,17 @@ class AttachmentController { // 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(messageId, size.width, size.height, - size.mimeType, extension, thumbnailSize.width, - thumbnailSize.height, hasError); + return new AttachmentItem(h, size.width, size.height, extension, + thumbnailSize.width, thumbnailSize.height, hasError); } /** diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageHelper.java b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java similarity index 90% rename from briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageHelper.java rename to briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java index 203a45222..1264d6511 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageHelper.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/attachment/ImageHelper.java @@ -1,4 +1,4 @@ -package org.briarproject.briar.android.conversation; +package org.briarproject.briar.android.attachment; import android.support.annotation.Nullable; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index 17e596cb6..3fcdddaa0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.blog; -import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -21,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; @@ -121,7 +121,8 @@ public class ReblogFragment extends BaseFragment implements SendListener { } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { ui.input.hideSoftKeyboard(); feedController.repeatPost(item, text, new UiExceptionHandler(this) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java index 5b68499cd..1ea824b79 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.blog; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.KeyEvent; @@ -27,6 +26,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogPost; import org.briarproject.briar.api.blog.BlogPostFactory; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.security.GeneralSecurityException; import java.util.List; @@ -120,7 +120,8 @@ public class WriteBlogPostActivity extends BriarActivity } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { if (isNullOrEmpty(text)) throw new AssertionError(); // hide publish button, show progress bar diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index c844f030c..dfbddcd7a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProviders; import android.content.DialogInterface; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -53,6 +52,8 @@ import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.attachment.AttachmentItem; +import org.briarproject.briar.android.attachment.AttachmentRetriever; import org.briarproject.briar.android.blog.BlogActivity; import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; @@ -184,7 +185,7 @@ public class ConversationActivity extends BriarActivity loadMessages(); }; - private AttachmentController attachmentController; + private AttachmentRetriever attachmentRetriever; private ConversationViewModel viewModel; private ConversationVisitor visitor; private ConversationAdapter adapter; @@ -219,7 +220,7 @@ public class ConversationActivity extends BriarActivity viewModel = ViewModelProviders.of(this, viewModelFactory) .get(ConversationViewModel.class); - attachmentController = viewModel.getAttachmentController(); + attachmentRetriever = viewModel.getAttachmentRetriever(); setContentView(R.layout.activity_conversation); @@ -242,7 +243,7 @@ public class ConversationActivity extends BriarActivity requireNonNull(deleted); if (deleted) finish(); }); - viewModel.getAddedPrivateMessage().observe(this, + viewModel.getAddedPrivateMessage().observeEvent(this, this::onAddedPrivateMessage); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); @@ -264,7 +265,7 @@ public class ConversationActivity extends BriarActivity if (FEATURE_FLAG_IMAGE_ATTACHMENTS) { ImagePreview imagePreview = findViewById(R.id.imagePreview); sendController = new TextAttachmentController(textInputView, - imagePreview, this, this); + imagePreview, this, this, viewModel); observeOnce(viewModel.hasImageSupport(), this, hasSupport -> { if (hasSupport != null && hasSupport) { // remove cast when removing FEATURE_FLAG_IMAGE_ATTACHMENTS @@ -457,13 +458,13 @@ public class ConversationActivity extends BriarActivity // If the message has a single image, load its size - for multiple // images we use a grid so the size is fixed if (h.getAttachmentHeaders().size() == 1) { - List items = attachmentController.get(id); + List items = attachmentRetriever.cacheGet(id); if (items == null) { LOG.info("Eagerly loading image size for latest message"); - items = attachmentController.getAttachmentItems( - attachmentController.getMessageAttachments( + items = attachmentRetriever.getAttachmentItems( + attachmentRetriever.getMessageAttachments( h.getAttachmentHeaders())); - attachmentController.put(id, items); + attachmentRetriever.cachePut(id, items); } } } @@ -545,10 +546,10 @@ public class ConversationActivity extends BriarActivity runOnDbThread(() -> { try { List> attachments = - attachmentController.getMessageAttachments(headers); + attachmentRetriever.getMessageAttachments(headers); // TODO move getting the items off to IoExecutor, if size == 1 List items = - attachmentController.getAttachmentItems(attachments); + attachmentRetriever.getAttachmentItems(attachments); displayMessageAttachments(messageId, items); } catch (DbException e) { logException(LOG, WARNING, e); @@ -559,7 +560,7 @@ public class ConversationActivity extends BriarActivity private void displayMessageAttachments(MessageId m, List items) { runOnUiThreadUnlessDestroyed(() -> { - attachmentController.put(m, items); + attachmentRetriever.cachePut(m, items); Pair pair = adapter.getMessageItem(m); if (pair != null) { @@ -658,12 +659,13 @@ public class ConversationActivity extends BriarActivity } @Override - public void onSendClick(@Nullable String text, List imageUris) { - if (isNullOrEmpty(text) && imageUris.isEmpty()) + public void onSendClick(@Nullable String text, + List attachmentHeaders) { + if (isNullOrEmpty(text) && attachmentHeaders.isEmpty()) throw new AssertionError(); long timestamp = System.currentTimeMillis(); timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); - viewModel.sendMessage(text, imageUris, timestamp); + viewModel.sendMessage(text, attachmentHeaders, timestamp); textInputView.clearText(); } @@ -676,7 +678,6 @@ public class ConversationActivity extends BriarActivity private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) { if (h == null) return; addConversationItem(h.accept(visitor)); - viewModel.onAddedPrivateMessageSeen(); } private void askToRemoveContact() { @@ -903,7 +904,7 @@ public class ConversationActivity extends BriarActivity @Override public List getAttachmentItems(MessageId m, List headers) { - List attachments = attachmentController.get(m); + List attachments = attachmentRetriever.cacheGet(m); if (attachments == null) { loadMessageAttachments(m, headers); return emptyList(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java index d7c445fae..d961ff1e2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationListener.java @@ -4,6 +4,7 @@ import android.support.annotation.UiThread; import android.view.View; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.android.attachment.AttachmentItem; @UiThread @NotNullByDefault diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java index 1732e3ab8..6b853932a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java @@ -3,6 +3,7 @@ package org.briarproject.briar.android.conversation; import android.support.annotation.LayoutRes; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.api.messaging.PrivateMessageHeader; import java.util.List; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index 94c80639a..6524129b4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.attachment.AttachmentItem; import static android.support.constraint.ConstraintSet.WRAP_CONTENT; import static android.support.v4.content.ContextCompat.getColor; 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 ba1457e7c..a1f4428b9 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 @@ -5,13 +5,11 @@ import android.arch.lifecycle.AndroidViewModel; import android.arch.lifecycle.LiveData; import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.Transformations; -import android.content.ContentResolver; import android.net.Uri; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; @@ -21,25 +19,26 @@ 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; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.android.attachment.AttachmentCreator; +import org.briarproject.briar.android.attachment.AttachmentManager; +import org.briarproject.briar.android.attachment.AttachmentResult; +import org.briarproject.briar.android.attachment.AttachmentRetriever; import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent; -import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.messaging.PrivateMessageHeader; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; @@ -50,16 +49,16 @@ import javax.inject.Inject; import static java.util.Objects.requireNonNull; 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.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; -import static org.briarproject.briar.android.conversation.AttachmentDimensions.getAttachmentDimensions; +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; @NotNullByDefault -public class ConversationViewModel extends AndroidViewModel { +public class ConversationViewModel extends AndroidViewModel + implements AttachmentManager { private static Logger LOG = getLogger(ConversationViewModel.class.getName()); @@ -77,7 +76,8 @@ public class ConversationViewModel extends AndroidViewModel { private final ContactManager contactManager; private final SettingsManager settingsManager; private final PrivateMessageFactory privateMessageFactory; - private final AttachmentController attachmentController; + private final AttachmentRetriever attachmentRetriever; + private final AttachmentCreator attachmentCreator; @Nullable private ContactId contactId = null; @@ -86,6 +86,7 @@ public class ConversationViewModel extends AndroidViewModel { Transformations.map(contact, c -> c.getAuthor().getId()); private final LiveData contactName = Transformations.map(contact, UiUtils::getContactDisplayName); + private final LiveData messagingGroupId; private final MutableLiveData imageSupport = new MutableLiveData<>(); private final MutableLiveEvent showImageOnboarding = @@ -96,15 +97,14 @@ public class ConversationViewModel extends AndroidViewModel { new MutableLiveData<>(); private final MutableLiveData contactDeleted = new MutableLiveData<>(); - private final MutableLiveData messagingGroupId = - new MutableLiveData<>(); - private final MutableLiveData addedHeader = - new MutableLiveData<>(); + private final MutableLiveEvent addedHeader = + new MutableLiveEvent<>(); @Inject ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, - @CryptoExecutor Executor cryptoExecutor, TransactionManager db, + @CryptoExecutor Executor cryptoExecutor, + @IoExecutor Executor ioExecutor, TransactionManager db, MessagingManager messagingManager, ContactManager contactManager, SettingsManager settingsManager, PrivateMessageFactory privateMessageFactory) { @@ -116,11 +116,21 @@ public class ConversationViewModel extends AndroidViewModel { this.contactManager = contactManager; this.settingsManager = settingsManager; this.privateMessageFactory = privateMessageFactory; - this.attachmentController = new AttachmentController(messagingManager, + this.attachmentRetriever = new AttachmentRetriever(messagingManager, getAttachmentDimensions(application.getResources())); + this.attachmentCreator = new AttachmentCreator(getApplication(), + ioExecutor, messagingManager, attachmentRetriever); + messagingGroupId = Transformations + .map(contact, c -> messagingManager.getContactGroup(c).getId()); contactDeleted.setValue(false); } + @Override + protected void onCleared() { + super.onCleared(); + attachmentCreator.deleteUnsentAttachments(); + } + /** * Setting the {@link ContactId} automatically triggers loading of other * data. @@ -176,25 +186,37 @@ public class ConversationViewModel extends AndroidViewModel { }); } - void sendMessage(@Nullable String text, List uris, long timestamp) { - if (messagingGroupId.getValue() == null) loadGroupId(); + void sendMessage(@Nullable String text, + List attachmentHeaders, long timestamp) { + // messagingGroupId is loaded with the contact observeForeverOnce(messagingGroupId, groupId -> { - if (groupId == null) return; - // calls through to creating and storing the message - storeAttachments(groupId, text, uris, timestamp); + if (groupId == null) throw new IllegalStateException(); + createMessage(groupId, text, attachmentHeaders, timestamp); }); } - private void loadGroupId() { - if (contactId == null) throw new IllegalStateException(); - dbExecutor.execute(() -> { - try { - messagingGroupId.postValue( - messagingManager.getConversationId(contactId)); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); + @Override + @UiThread + public LiveData storeAttachments(Collection uris, + boolean restart) { + if (restart) { + return attachmentCreator.getLiveAttachments(); + } else { + // messagingGroupId is loaded with the contact + return attachmentCreator.storeAttachments(messagingGroupId, uris); + } + } + + @Override + @UiThread + public List getAttachmentHeadersForSending() { + return attachmentCreator.getAttachmentHeadersForSending(); + } + + @Override + @UiThread + public void cancel() { + attachmentCreator.cancel(); } @DatabaseExecutor @@ -252,58 +274,8 @@ public class ConversationViewModel extends AndroidViewModel { }); } - private void storeAttachments(GroupId groupId, @Nullable String text, - List uris, long timestamp) { - dbExecutor.execute(() -> { - long start = now(); - List attachments = new ArrayList<>(); - List items = new ArrayList<>(); - boolean needsSize = uris.size() == 1; - for (Uri uri : uris) { - Pair pair = - createAttachmentHeader(groupId, uri, timestamp, - needsSize); - if (pair == null) continue; - attachments.add(pair.getFirst()); - items.add(pair.getSecond()); - } - logDuration(LOG, "Storing attachments", start); - createMessage(groupId, text, attachments, items, timestamp); - }); - } - - @Nullable - @DatabaseExecutor - private Pair createAttachmentHeader( - GroupId groupId, Uri uri, long timestamp, boolean needsSize) { - InputStream is = null; - try { - ContentResolver contentResolver = - getApplication().getContentResolver(); - is = contentResolver.openInputStream(uri); - if (is == null) throw new IOException(); - String contentType = contentResolver.getType(uri); - if (contentType == null) throw new IOException("null content type"); - AttachmentHeader h = messagingManager - .addLocalAttachment(groupId, timestamp, contentType, is); - is.close(); - // re-open stream to get AttachmentItem - is = contentResolver.openInputStream(uri); - if (is == null) throw new IOException(); - AttachmentItem item = attachmentController - .getAttachmentItem(h, new Attachment(is), needsSize); - return new Pair<>(h, item); - } catch (DbException | IOException e) { - logException(LOG, WARNING, e); - return null; - } finally { - if (is != null) tryToClose(is, LOG, WARNING); - } - } - private void createMessage(GroupId groupId, @Nullable String text, - List attachments, List aItems, - long timestamp) { + List attachments, long timestamp) { cryptoExecutor.execute(() -> { try { // TODO remove when text can be null in the backend @@ -311,7 +283,6 @@ public class ConversationViewModel extends AndroidViewModel { PrivateMessage pm = privateMessageFactory .createPrivateMessage(groupId, timestamp, msgText, attachments); - attachmentController.put(pm.getMessage().getId(), aItems); storeMessage(pm, msgText, attachments); } catch (FormatException e) { throw new RuntimeException(e); @@ -321,6 +292,7 @@ public class ConversationViewModel extends AndroidViewModel { private void storeMessage(PrivateMessage m, @Nullable String text, List attachments) { + attachmentCreator.onAttachmentsSent(m.getMessage().getId()); dbExecutor.execute(() -> { try { long start = now(); @@ -332,20 +304,15 @@ public class ConversationViewModel extends AndroidViewModel { message.getTimestamp(), true, true, false, false, text != null, attachments); // TODO add text to cache when available here - addedHeader.postValue(h); + addedHeader.postEvent(h); } catch (DbException e) { logException(LOG, WARNING, e); } }); } - @UiThread - void onAddedPrivateMessageSeen() { - addedHeader.setValue(null); - } - - AttachmentController getAttachmentController() { - return attachmentController; + AttachmentRetriever getAttachmentRetriever() { + return attachmentRetriever; } LiveData getContact() { @@ -380,7 +347,7 @@ public class ConversationViewModel extends AndroidViewModel { return contactDeleted; } - LiveData getAddedPrivateMessage() { + LiveEvent getAddedPrivateMessage() { return addedHeader; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index 87ec94f07..48f39e666 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -7,6 +7,7 @@ import android.support.annotation.UiThread; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.api.blog.BlogInvitationRequest; import org.briarproject.briar.api.blog.BlogInvitationResponse; import org.briarproject.briar.api.conversation.ConversationMessageVisitor; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index aebb3069e..1e1f3bf36 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -31,6 +31,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.util.BriarSnackbarBuilder; import org.briarproject.briar.android.view.PullDownLayout; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java index 8c9e15d30..f97a5f233 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageAdapter.java @@ -12,6 +12,7 @@ import android.view.WindowManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.conversation.glide.Radii; import java.util.ArrayList; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java index 1d1b0d1fd..684e216a8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageFragment.java @@ -21,6 +21,7 @@ import com.github.chrisbanes.photoview.PhotoView; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.conversation.glide.GlideApp; import javax.annotation.ParametersAreNonnullByDefault; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java index f76e3c2ec..4019bb4f4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewHolder.java @@ -11,6 +11,7 @@ import com.bumptech.glide.load.Transformation; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; import org.briarproject.briar.android.conversation.glide.GlideApp; import org.briarproject.briar.android.conversation.glide.Radii; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java index c8c9451c9..33f29d3e3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageViewModel.java @@ -13,6 +13,7 @@ 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.MessageId; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.android.viewmodel.LiveEvent; import org.briarproject.briar.android.viewmodel.MutableLiveEvent; import org.briarproject.briar.api.messaging.Attachment; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java index ac239f4a4..1394f8035 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java @@ -10,7 +10,7 @@ import org.briarproject.bramble.api.db.DatabaseExecutor; 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.conversation.AttachmentItem; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.api.messaging.MessagingManager; import java.io.InputStream; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java index 87f55a170..69a920ae9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java @@ -2,7 +2,7 @@ package org.briarproject.briar.android.conversation.glide; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.android.conversation.AttachmentItem; +import org.briarproject.briar.android.attachment.AttachmentItem; import org.briarproject.briar.api.messaging.MessagingManager; import java.util.concurrent.Executor; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java index a1a66ab33..a477f2f44 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java @@ -10,7 +10,7 @@ import com.bumptech.glide.module.AppGlideModule; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.android.BriarApplication; -import org.briarproject.briar.android.conversation.AttachmentItem; +import org.briarproject.briar.android.attachment.AttachmentItem; import java.io.InputStream; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java index b4e132392..dd5008f67 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java @@ -8,7 +8,7 @@ import com.bumptech.glide.signature.ObjectKey; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.android.BriarApplication; -import org.briarproject.briar.android.conversation.AttachmentItem; +import org.briarproject.briar.android.attachment.AttachmentItem; import java.io.InputStream; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java index 7d077e50e..07ed62227 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java @@ -6,7 +6,7 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.android.BriarApplication; -import org.briarproject.briar.android.conversation.AttachmentItem; +import org.briarproject.briar.android.attachment.AttachmentItem; import java.io.InputStream; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java index dee769cf3..70478b884 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.introduction; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; @@ -26,6 +25,7 @@ import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.api.introduction.IntroductionManager; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; import java.util.logging.Logger; @@ -193,7 +193,8 @@ public class IntroductionMessageFragment extends BaseFragment } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { // disable button to prevent accidental double invitations ui.message.setReady(false); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java index 57c299fa4..eaff51eb4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.sharing; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.StringRes; @@ -19,6 +18,7 @@ import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.view.LargeTextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; @@ -83,7 +83,8 @@ public abstract class BaseMessageFragment extends BaseFragment } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { // disable button to prevent accidental double actions sendController.setReady(false); message.hideSoftKeyboard(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index f74626b67..80456c453 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.threaded; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.CallSuper; @@ -34,6 +33,7 @@ import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.UnreadMessageButton; import org.briarproject.briar.api.client.NamedGroup; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.Collection; import java.util.List; @@ -341,7 +341,8 @@ public abstract class ThreadListActivity imageUris) { + public void onSendClick(@Nullable String text, + List headers) { if (isNullOrEmpty(text)) throw new AssertionError(); I replyItem = adapter.getHighlightedItem(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java index f4158a495..7bdb7d93f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.view; import android.content.Context; -import android.net.Uri; import android.support.annotation.Nullable; import android.support.constraint.ConstraintLayout; import android.support.v7.widget.RecyclerView; @@ -10,11 +9,13 @@ import android.view.LayoutInflater; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.attachment.AttachmentItemResult; import java.util.Collection; import static android.content.Context.LAYOUT_INFLATER_SERVICE; import static android.support.v4.content.ContextCompat.getColor; +import static android.support.v7.widget.RecyclerView.NO_POSITION; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static java.util.Objects.requireNonNull; @@ -60,34 +61,28 @@ public class ImagePreview extends ConstraintLayout { this.listener = listener; } - void showPreview(Collection imageUris) { + void showPreview(Collection items) { if (listener == null) throw new IllegalStateException(); - if (imageUris.size() == 1) { + if (items.size() == 1) { LayoutParams params = (LayoutParams) imageList.getLayoutParams(); params.width = MATCH_PARENT; imageList.setLayoutParams(params); } setVisibility(VISIBLE); - imageList.setAdapter(new ImagePreviewAdapter(imageUris, listener)); + ImagePreviewAdapter adapter = new ImagePreviewAdapter(items); + imageList.setAdapter(adapter); } - void removeUri(Uri uri) { + void loadPreviewImage(AttachmentItemResult result) { ImagePreviewAdapter adapter = - (ImagePreviewAdapter) imageList.getAdapter(); - requireNonNull(adapter).removeUri(uri); + ((ImagePreviewAdapter) imageList.getAdapter()); + int pos = requireNonNull(adapter).loadItemPreview(result); + if (pos != NO_POSITION) { + imageList.smoothScrollToPosition(pos); + } } interface ImagePreviewListener { - - void onPreviewLoaded(); - - /** - * Called when Glide can't load a preview image. - * - * Warning: Glide may call this multiple times. - */ - void onUriError(Uri uri); - void onCancel(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java index a6508e4eb..5e7bf3253 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.view; -import android.net.Uri; import android.support.annotation.LayoutRes; import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; @@ -9,25 +8,24 @@ import android.view.ViewGroup; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; -import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; +import org.briarproject.briar.android.attachment.AttachmentItemResult; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import static android.support.v7.widget.RecyclerView.NO_POSITION; import static java.util.Objects.requireNonNull; @NotNullByDefault class ImagePreviewAdapter extends Adapter { - private final List items; - private final ImagePreviewListener listener; + private final List items; @LayoutRes private final int layout; - ImagePreviewAdapter(Collection items, ImagePreviewListener listener) { + ImagePreviewAdapter(Collection items) { this.items = new ArrayList<>(items); - this.listener = listener; this.layout = items.size() == 1 ? R.layout.list_item_image_preview_single : R.layout.list_item_image_preview; @@ -38,7 +36,7 @@ class ImagePreviewAdapter extends Adapter { int type) { View v = LayoutInflater.from(viewGroup.getContext()) .inflate(layout, viewGroup, false); - return new ImagePreviewViewHolder(v, requireNonNull(listener)); + return new ImagePreviewViewHolder(v); } @Override @@ -52,11 +50,17 @@ class ImagePreviewAdapter extends Adapter { return items.size(); } - void removeUri(Uri uri) { - int pos = items.indexOf(uri); - if (pos == -1) return; - items.remove(uri); - notifyItemRemoved(pos); + int loadItemPreview(AttachmentItemResult result) { + ImagePreviewItem newItem = new ImagePreviewItem(result.getUri()); + int pos = items.indexOf(newItem); + if (pos == NO_POSITION) throw new AssertionError(); + ImagePreviewItem item = items.get(pos); + if (item.getItem() == null) { + item.setItem(requireNonNull(result.getItem())); + notifyItemChanged(pos, item); + return pos; + } + return NO_POSITION; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java new file mode 100644 index 000000000..5a4f24648 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java @@ -0,0 +1,53 @@ +package org.briarproject.briar.android.view; + +import android.net.Uri; +import android.support.annotation.Nullable; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.android.attachment.AttachmentItem; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@NotNullByDefault +class ImagePreviewItem { + + private final Uri uri; + @Nullable + private AttachmentItem item; + + ImagePreviewItem(Uri uri) { + this.uri = uri; + this.item = null; + } + + static List fromUris(Collection uris) { + List items = new ArrayList<>(uris.size()); + for (Uri uri : uris) { + items.add(new ImagePreviewItem(uri)); + } + return items; + } + + public void setItem(AttachmentItem item) { + this.item = item; + } + + @Nullable + public AttachmentItem getItem() { + return item; + } + + @Override + public boolean equals(@Nullable Object o) { + return o instanceof ImagePreviewItem && + uri.equals(((ImagePreviewItem) o).uri); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java index c16ce14ca..856eb30bb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.view; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView.ViewHolder; @@ -17,9 +16,9 @@ import com.bumptech.glide.request.target.Target; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.conversation.glide.GlideApp; -import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; import static android.view.View.INVISIBLE; +import static android.view.View.VISIBLE; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; @@ -30,45 +29,47 @@ class ImagePreviewViewHolder extends ViewHolder { @DrawableRes private static final int ERROR_RES = R.drawable.ic_image_broken; - private final ImagePreviewListener listener; - private final ImageView imageView; private final ProgressBar progressBar; - ImagePreviewViewHolder(View v, ImagePreviewListener listener) { + ImagePreviewViewHolder(View v) { super(v); - this.listener = listener; this.imageView = v.findViewById(R.id.imageView); this.progressBar = v.findViewById(R.id.progressBar); } - void bind(Uri uri) { - GlideApp.with(imageView) - .load(uri) - .diskCacheStrategy(NONE) - .error(ERROR_RES) - .downsample(FIT_CENTER) - .transition(withCrossFade()) - .addListener(new RequestListener() { - @Override - public boolean onLoadFailed(@Nullable GlideException e, - Object model, Target target, - boolean isFirstResource) { - progressBar.setVisibility(INVISIBLE); - listener.onUriError(uri); - return false; - } + void bind(ImagePreviewItem item) { + if (item.getItem() == null) { + progressBar.setVisibility(VISIBLE); + GlideApp.with(imageView) + .clear(imageView); + } else { + GlideApp.with(imageView) + .load(item.getItem()) + .diskCacheStrategy(NONE) + .error(ERROR_RES) + .downsample(FIT_CENTER) + .transition(withCrossFade()) + .addListener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, + Object model, Target target, + boolean isFirstResource) { + progressBar.setVisibility(INVISIBLE); + return false; + } - @Override - public boolean onResourceReady(Drawable resource, - Object model, Target target, - DataSource dataSource, boolean isFirstResource) { - progressBar.setVisibility(INVISIBLE); - listener.onPreviewLoaded(); - return false; - } - }) - .into(imageView); + @Override + public boolean onResourceReady(Drawable resource, + Object model, Target target, + DataSource dataSource, + boolean isFirstResource) { + progressBar.setVisibility(INVISIBLE); + return false; + } + }) + .into(imageView); + } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java index 3a73ba695..34a097846 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java @@ -1,6 +1,9 @@ package org.briarproject.briar.android.view; import android.app.Activity; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Observer; import android.content.ClipData; import android.content.Context; import android.content.Intent; @@ -15,26 +18,32 @@ import android.widget.Toast; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.attachment.AttachmentItemResult; +import org.briarproject.briar.android.attachment.AttachmentManager; +import org.briarproject.briar.android.attachment.AttachmentResult; import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt; import uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.PromptStateChangeListener; +import static android.arch.lifecycle.Lifecycle.State.DESTROYED; import static android.content.Intent.ACTION_GET_CONTENT; import static android.content.Intent.ACTION_OPEN_DOCUMENT; import static android.content.Intent.CATEGORY_OPENABLE; import static android.content.Intent.EXTRA_ALLOW_MULTIPLE; +import static android.content.Intent.EXTRA_MIME_TYPES; import static android.os.Build.VERSION.SDK_INT; import static android.support.v4.content.ContextCompat.getColor; import static android.support.v4.view.AbsSavedState.EMPTY_STATE; import static android.view.View.GONE; import static android.widget.Toast.LENGTH_LONG; -import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute; +import static org.briarproject.briar.api.messaging.MessagingConstants.IMAGE_MIME_TYPES; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_DISMISSED; import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.STATE_FINISHED; @@ -46,17 +55,19 @@ public class TextAttachmentController extends TextSendController private final ImagePreview imagePreview; private final AttachImageListener imageListener; private final CompositeSendButton sendButton; + private final AttachmentManager attachmentManager; - private CharSequence textHint; - private List imageUris = emptyList(); - private int previewsLoaded = 0; - private boolean loadingPreviews = false; + private final List imageUris = new ArrayList<>(); + private final CharSequence textHint; + private boolean loadingUris = false; public TextAttachmentController(TextInputView v, ImagePreview imagePreview, - SendListener listener, AttachImageListener imageListener) { + SendListener listener, AttachImageListener imageListener, + AttachmentManager attachmentManager) { super(v, listener, false); this.imageListener = imageListener; this.imagePreview = imagePreview; + this.attachmentManager = attachmentManager; this.imagePreview.setImagePreviewListener(this); sendButton = (CompositeSendButton) compositeSendButton; @@ -67,10 +78,10 @@ public class TextAttachmentController extends TextSendController @Override protected void updateViewState() { - textInput.setEnabled(ready && !loadingPreviews); - boolean sendEnabled = ready && !loadingPreviews && + textInput.setEnabled(ready && !loadingUris); + boolean sendEnabled = ready && !loadingUris && (!textIsEmpty || canSendEmptyText()); - if (loadingPreviews) { + if (loadingUris) { sendButton.showProgress(true); } else if (imageUris.isEmpty()) { sendButton.showProgress(false); @@ -84,7 +95,9 @@ public class TextAttachmentController extends TextSendController @Override public void onSendEvent() { if (canSend()) { - listener.onSendClick(textInput.getText(), imageUris); + if (loadingUris) throw new AssertionError(); + listener.onSendClick(textInput.getText(), + attachmentManager.getAttachmentHeadersForSending()); reset(); } } @@ -110,36 +123,95 @@ public class TextAttachmentController extends TextSendController builder.show(); return; } - Intent intent = new Intent(SDK_INT >= 19 ? - ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT); - intent.addCategory(CATEGORY_OPENABLE); - intent.setType("image/*"); - if (SDK_INT >= 18) intent.putExtra(EXTRA_ALLOW_MULTIPLE, true); - requireNonNull(imageListener).onAttachImage(intent); - } - - public void onImageReceived(@Nullable Intent resultData) { - if (resultData == null) return; - if (resultData.getData() != null) { - imageUris = new ArrayList<>(1); - imageUris.add(resultData.getData()); - onNewUris(); - } else if (SDK_INT >= 18 && resultData.getClipData() != null) { - ClipData clipData = resultData.getClipData(); - imageUris = new ArrayList<>(clipData.getItemCount()); - for (int i = 0; i < clipData.getItemCount(); i++) { - imageUris.add(clipData.getItemAt(i).getUri()); - } - onNewUris(); + Intent intent = getAttachFileIntent(); + if (imageListener.getLifecycle().getCurrentState() != DESTROYED) { + requireNonNull(imageListener).onAttachImage(intent); } } - private void onNewUris() { + private Intent getAttachFileIntent() { + Intent intent = new Intent(SDK_INT >= 19 ? + ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT); + intent.setType("image/*"); + intent.addCategory(CATEGORY_OPENABLE); + if (SDK_INT >= 19) intent.putExtra(EXTRA_MIME_TYPES, IMAGE_MIME_TYPES); + if (SDK_INT >= 18) intent.putExtra(EXTRA_ALLOW_MULTIPLE, true); + return intent; + } + + /** + * This is called with the result Intent + * returned by the Activity started with {@link #getAttachFileIntent()}. + *

+ * This method must be called at most once per call to + * {@link AttachImageListener#onAttachImage(Intent)}. + * Normally, this is true if called from + * {@link Activity#onActivityResult(int, int, Intent)} since this is called + * at most once per call to {@link Activity#startActivityForResult(Intent, int)}. + */ + public void onImageReceived(@Nullable Intent resultData) { + if (resultData == null) return; + if (loadingUris || !imageUris.isEmpty()) throw new AssertionError(); + if (resultData.getData() != null) { + imageUris.add(resultData.getData()); + onNewUris(false); + } else if (SDK_INT >= 18 && resultData.getClipData() != null) { + ClipData clipData = resultData.getClipData(); + for (int i = 0; i < clipData.getItemCount(); i++) { + imageUris.add(clipData.getItemAt(i).getUri()); + } + onNewUris(false); + } + } + + private void onNewUris(boolean restart) { if (imageUris.isEmpty()) return; - loadingPreviews = true; + if (loadingUris) throw new AssertionError(); + loadingUris = true; updateViewState(); textInput.setHint(R.string.image_caption_hint); - imagePreview.showPreview(imageUris); + List items = ImagePreviewItem.fromUris(imageUris); + imagePreview.showPreview(items); + // store attachments and show preview when successful + LiveData result = + attachmentManager.storeAttachments(imageUris, restart); + result.observe(imageListener, new Observer() { + @Override + public void onChanged(@Nullable AttachmentResult attachmentResult) { + if (attachmentResult == null) { + // The fresh LiveData was deliberately set to null. + // This means that we can stop observing it. + result.removeObserver(this); + } else { + boolean noError = onNewAttachmentItemResults( + attachmentResult.getItemResults()); + if (noError && attachmentResult.isFinished()) { + onAllAttachmentsCreated(); + result.removeObserver(this); + } + } + } + }); + } + + private boolean onNewAttachmentItemResults( + Collection itemResults) { + if (!loadingUris) throw new AssertionError(); + for (AttachmentItemResult result : itemResults) { + if (result.hasError()) { + onError(result.getErrorMsg()); + return false; + } else { + imagePreview.loadPreviewImage(result); + } + } + return true; + } + + private void onAllAttachmentsCreated() { + if (!loadingUris) throw new AssertionError(); + loadingUris = false; + updateViewState(); } private void reset() { @@ -148,10 +220,9 @@ public class TextAttachmentController extends TextSendController // hide image layout imagePreview.setVisibility(GONE); // reset image URIs - imageUris = emptyList(); - // no preview has been loaded - previewsLoaded = 0; - loadingPreviews = false; + imageUris.clear(); + // definitely not loading anymore + loadingUris = false; // show the image button again, so images can get attached updateViewState(); } @@ -168,45 +239,29 @@ public class TextAttachmentController extends TextSendController @Nullable public Parcelable onRestoreInstanceState(Parcelable inState) { SavedState state = (SavedState) inState; - imageUris = requireNonNull(state.imageUris); - onNewUris(); + if (!imageUris.isEmpty()) throw new AssertionError(); + if (state.imageUris != null) imageUris.addAll(state.imageUris); + onNewUris(true); return state.getSuperState(); } - @Override - public void onPreviewLoaded() { - previewsLoaded++; - checkAllPreviewsLoaded(); - } - - @Override - public void onUriError(Uri uri) { - boolean removed = imageUris.remove(uri); - if (!removed) { - // we have removed this Uri already, do not remove it again - return; + @UiThread + private void onError(@Nullable String errorMsg) { + if (errorMsg == null) { + errorMsg = imagePreview.getContext() + .getString(R.string.image_attach_error); } - imagePreview.removeUri(uri); - if (imageUris.isEmpty()) onCancel(); - Toast.makeText(textInput.getContext(), R.string.image_attach_error, - LENGTH_LONG).show(); - checkAllPreviewsLoaded(); + Toast.makeText(textInput.getContext(), errorMsg, LENGTH_LONG).show(); + onCancel(); } @Override public void onCancel() { textInput.clearText(); + attachmentManager.cancel(); reset(); } - private void checkAllPreviewsLoaded() { - if (previewsLoaded == imageUris.size()) { - loadingPreviews = false; - // all previews were loaded - updateViewState(); - } - } - public void showImageOnboarding(Activity activity, Runnable onOnboardingSeen) { PromptStateChangeListener listener = (prompt, state) -> { @@ -261,7 +316,7 @@ public class TextAttachmentController extends TextSendController }; } - public interface AttachImageListener { + public interface AttachImageListener extends LifecycleOwner { void onAttachImage(Intent intent); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java index f0a864afe..725d23baa 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.view; -import android.net.Uri; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.UiThread; @@ -10,6 +9,7 @@ import android.view.View; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.view.EmojiTextInputView.TextInputListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; @@ -85,7 +85,7 @@ public class TextSendController implements TextInputListener { } public interface SendListener { - void onSendClick(@Nullable String text, List imageUris); + void onSendClick(@Nullable String text, List headers); } } diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 144581e5d..a80cae6e3 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -129,7 +129,9 @@ Type message Add a caption (optional) Attach image - Could not attach image + Could not attach image(s) + Image too big. Limit is %d MB. + Image format unsupported: %s Change contact name Contact name Change diff --git a/briar-android/src/test/java/org/briarproject/briar/android/conversation/AttachmentControllerTest.java b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java similarity index 83% rename from briar-android/src/test/java/org/briarproject/briar/android/conversation/AttachmentControllerTest.java rename to briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java index d66b69697..f0f38bdb1 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/conversation/AttachmentControllerTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/attachment/AttachmentRetrieverTest.java @@ -1,8 +1,8 @@ -package org.briarproject.briar.android.conversation; +package org.briarproject.briar.android.attachment; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.test.BrambleMockTestCase; -import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult; +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; @@ -19,7 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -public class AttachmentControllerTest extends BrambleMockTestCase { +public class AttachmentRetrieverTest extends BrambleMockTestCase { private final AttachmentDimensions dimensions = new AttachmentDimensions( 100, 50, 200, 75, 300 @@ -32,8 +32,8 @@ public class AttachmentControllerTest extends BrambleMockTestCase { private final MessagingManager messagingManager = context.mock(MessagingManager.class); private final ImageHelper imageHelper = context.mock(ImageHelper.class); - private final AttachmentController controller = - new AttachmentController( + private final AttachmentRetriever controller = + new AttachmentRetriever( messagingManager, dimensions, imageHelper @@ -94,23 +94,6 @@ public class AttachmentControllerTest extends BrambleMockTestCase { assertFalse(item.hasError()); } - @Test - public void testImageHealsWrongMimeType() { - AttachmentHeader h = getAttachmentHeader("image/png"); - - context.checking(new Expectations() {{ - oneOf(imageHelper).decodeStream(with(any(InputStream.class))); - will(returnValue(new DecodeResult(160, 240, "image/jpeg"))); - oneOf(imageHelper).getExtensionFromMimeType("image/jpeg"); - will(returnValue("jpg")); - }}); - - AttachmentItem item = controller.getAttachmentItem(h, attachment, true); - assertEquals("image/jpeg", item.getMimeType()); - assertEquals("jpg", item.getExtension()); - assertFalse(item.hasError()); - } - @Test public void testBigJpegImage() { String mimeType = "image/jpeg"; diff --git a/briar-android/src/test/java/org/briarproject/briar/android/conversation/MarkEnforcingInputStreamTest.java b/briar-android/src/test/java/org/briarproject/briar/android/attachment/MarkEnforcingInputStreamTest.java similarity index 97% rename from briar-android/src/test/java/org/briarproject/briar/android/conversation/MarkEnforcingInputStreamTest.java rename to briar-android/src/test/java/org/briarproject/briar/android/attachment/MarkEnforcingInputStreamTest.java index 45a30a499..14defdf4d 100644 --- a/briar-android/src/test/java/org/briarproject/briar/android/conversation/MarkEnforcingInputStreamTest.java +++ b/briar-android/src/test/java/org/briarproject/briar/android/attachment/MarkEnforcingInputStreamTest.java @@ -1,4 +1,4 @@ -package org.briarproject.briar.android.conversation; +package org.briarproject.briar.android.attachment; import com.bumptech.glide.util.MarkEnforcingInputStream; diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java index 685e2ea3a..970401211 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java @@ -25,4 +25,15 @@ public class AttachmentHeader { return contentType; } + @Override + public boolean equals(Object o) { + return o instanceof AttachmentHeader && + messageId.equals(((AttachmentHeader) o).messageId); + } + + @Override + public int hashCode() { + return messageId.hashCode(); + } + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/FileTooBigException.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/FileTooBigException.java new file mode 100644 index 000000000..f7d9c7c04 --- /dev/null +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/FileTooBigException.java @@ -0,0 +1,6 @@ +package org.briarproject.briar.api.messaging; + +import java.io.IOException; + +public class FileTooBigException extends IOException { +} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java index 1efdd748d..c1be55bdf 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java @@ -8,4 +8,20 @@ public interface MessagingConstants { * The maximum length of a private message's text in UTF-8 bytes. */ int MAX_PRIVATE_MESSAGE_TEXT_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; + + /** + * The supported mime types for image attachments. + */ + String[] IMAGE_MIME_TYPES = { + "image/jpeg", + "image/png", + "image/gif", + }; + + /** + * The maximum allowed size of image attachments. + * TODO: Different limit for GIFs? + */ + int MAX_IMAGE_SIZE = MAX_MESSAGE_BODY_LENGTH; // 6 * 1024 * 1024; + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java index a4f91af5c..d069e337b 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java @@ -37,10 +37,17 @@ public interface MessagingManager extends ConversationClient { /** * Stores a local attachment message. + * + * @throws FileTooBigException */ AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, String contentType, InputStream is) throws DbException, IOException; + /** + * Removes an unsent attachment. + */ + void removeAttachment(AttachmentHeader header) throws DbException; + /** * Returns the ID of the contact with the given private conversation. */ diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java index 44073b377..366eb768c 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java @@ -18,14 +18,17 @@ import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group.Visibility; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageFactory; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; +import org.briarproject.bramble.util.IoUtils; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.conversation.ConversationMessageHeader; 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.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessageHeader; @@ -33,18 +36,18 @@ import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent; import org.briarproject.briar.client.ConversationClientImpl; import java.io.ByteArrayInputStream; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Map; -import java.util.Random; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static java.util.Collections.emptyList; -import static org.briarproject.bramble.util.StringUtils.fromHexString; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; @Immutable @@ -55,15 +58,18 @@ class MessagingManagerImpl extends ConversationClientImpl private final ClientVersioningManager clientVersioningManager; private final ContactGroupFactory contactGroupFactory; + private final MessageFactory messageFactory; @Inject MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper, ClientVersioningManager clientVersioningManager, MetadataParser metadataParser, MessageTracker messageTracker, - ContactGroupFactory contactGroupFactory) { + ContactGroupFactory contactGroupFactory, + MessageFactory messageFactory) { super(db, clientHelper, metadataParser, messageTracker); this.clientVersioningManager = clientVersioningManager; this.contactGroupFactory = contactGroupFactory; + this.messageFactory = messageFactory; } @Override @@ -158,12 +164,29 @@ class MessagingManagerImpl extends ConversationClientImpl @Override public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, - String contentType, InputStream is) throws IOException { + String contentType, InputStream is) + throws DbException, IOException { // TODO add real implementation - if (is.available() == 0) throw new IOException(); - byte[] b = new byte[MessageId.LENGTH]; - new Random().nextBytes(b); - return new AttachmentHeader(new MessageId(b), "image/png"); + byte[] body = new byte[MAX_MESSAGE_BODY_LENGTH]; + try { + IoUtils.read(is, body); + } catch (EOFException ignored) { + } + if (is.available() > 0) throw new FileTooBigException(); + is.close(); + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + Message m = messageFactory.createMessage(groupId, timestamp, body); + clientHelper.addLocalMessage(m, new BdfDictionary(), false); + return new AttachmentHeader(m.getId(), contentType); + } + + @Override + public void removeAttachment(AttachmentHeader header) throws DbException { + db.transaction(false, + txn -> db.removeMessage(txn, header.getMessageId())); } private ContactId getContactId(Transaction txn, GroupId g) @@ -242,12 +265,10 @@ class MessagingManagerImpl extends ConversationClientImpl } @Override - public Attachment getAttachment(MessageId m) { + public Attachment getAttachment(MessageId mId) throws DbException { // TODO add real implementation - byte[] bytes = fromHexString("89504E470D0A1A0A0000000D49484452" + - "000000010000000108060000001F15C4" + - "890000000A49444154789C6300010000" + - "0500010D0A2DB40000000049454E44AE426082"); + Message m = clientHelper.getMessage(mId); + byte[] bytes = m.getBody(); return new Attachment(new ByteArrayInputStream(bytes)); }