Commit 11eefaed authored by Torsten Grote's avatar Torsten Grote

Refactor attachment creation

parent bb5a6c02
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());
......
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.List;
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 List<Uri> uris;
private final boolean needsSize;
@Nullable
private AttachmentCreator attachmentCreator;
private volatile boolean canceled = false;
AttachmentCreationTask(MessagingManager messagingManager,
ContentResolver contentResolver,
AttachmentCreator attachmentCreator, GroupId groupId,
List<Uri> 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);
if (!canceled && attachmentCreator != null)
attachmentCreator.onAttachmentCreationFinished();
attachmentCreator = null;
}
@IoExecutor
private void processUri(Uri uri) {
if (canceled) return;
try {
AttachmentHeader h = storeAttachment(uri);
if (attachmentCreator != null) {
attachmentCreator
.onAttachmentHeaderReceived(uri, h, needsSize);
}
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
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))
throw new UnsupportedMimeTypeException("", contentType,
uri.toString());
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(@Nullable String mimeType) {
if (mimeType == null) return false;
for (String supportedType : IMAGE_MIME_TYPES) {
if (supportedType.equals(mimeType)) return true;
}
return false;
}
}
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.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
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.LogUtils.logException;
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 controller;
private final Map<Uri, AttachmentItem> unsentItems =
new ConcurrentHashMap<>();
private final Map<Uri, MutableLiveData<AttachmentItemResult>>
liveDataResult = new ConcurrentHashMap<>();
@Nullable
private MutableLiveData<Boolean> liveDataFinished = null;
@Nullable
private AttachmentCreationTask task;
public AttachmentCreator(Application app, @IoExecutor Executor ioExecutor,
MessagingManager messagingManager,
AttachmentRetriever controller) {
this.app = app;
this.ioExecutor = ioExecutor;
this.messagingManager = messagingManager;
this.controller = controller;
}
@UiThread
public AttachmentResult storeAttachments(GroupId groupId,
Collection<Uri> uris) {
if (task != null && !isStoring()) throw new AssertionError();
List<LiveData<AttachmentItemResult>> itemResults = new ArrayList<>();
List<Uri> urisToStore = new ArrayList<>();
for (Uri uri : uris) {
MutableLiveData<AttachmentItemResult> liveData =
new MutableLiveData<>();
itemResults.add(liveData);
liveDataResult.put(uri, liveData);
if (unsentItems.containsKey(uri)) {
// This can happen due to configuration changes.
// So don't create a new attachment, if we have one already.
AttachmentItem item = requireNonNull(unsentItems.get(uri));
AttachmentItemResult result =
new AttachmentItemResult(uri, item);
liveData.setValue(result);
} else {
urisToStore.add(uri);
}
}
boolean needsSize = uris.size() == 1;
task = new AttachmentCreationTask(messagingManager,
app.getContentResolver(), this, groupId, urisToStore,
needsSize);
ioExecutor.execute(() -> task.storeAttachments());
liveDataFinished = new MutableLiveData<>();
return new AttachmentResult(itemResults, liveDataFinished);
}
@IoExecutor
void onAttachmentHeaderReceived(Uri uri, AttachmentHeader h,
boolean needsSize) {
// get and cache AttachmentItem for ImagePreview
try {
Attachment a = controller.getMessageAttachment(h);
AttachmentItem item = controller.getAttachmentItem(h, a, needsSize);
if (item.hasError()) throw new IOException();
unsentItems.put(uri, item);
MutableLiveData<AttachmentItemResult> result =
liveDataResult.get(uri);
if (result != null) { // might have been cleared on UiThread
result.postValue(new AttachmentItemResult(uri, item));
}
} catch (IOException | DbException e) {
logException(LOG, WARNING, e);
onAttachmentError(uri, e);
}
}
@IoExecutor
void onAttachmentError(Uri uri, Throwable t) {
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
}
MutableLiveData<AttachmentItemResult> result = liveDataResult.get(uri);
if (result != null)
result.postValue(new AttachmentItemResult(errorMsg));
// expect to receive a cancel from the UI
}
@IoExecutor
void onAttachmentCreationFinished() {
if (liveDataFinished != null) liveDataFinished.postValue(true);
}
@UiThread
public List<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers =
new ArrayList<>(unsentItems.values().size());
for (AttachmentItem item : unsentItems.values()) {
headers.add(item.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.
*/
public void onAttachmentsSent(MessageId id) {
controller.cachePut(id, new ArrayList<>(unsentItems.values()));
resetState();
}
@UiThread
public void cancel() {
if (task == null) throw new AssertionError();
task.cancel();
// let observers know that they can remove themselves
for (MutableLiveData<AttachmentItemResult> liveData : liveDataResult
.values()) {
if (liveData.getValue() == null) {
liveData.setValue(null);
}
}
if (liveDataFinished != null) liveDataFinished.setValue(false);
deleteUnsentAttachments();
resetState();
}
@UiThread
private void resetState() {
task = null;
liveDataResult.clear();
liveDataFinished = null;
unsentItems.clear();
}
@UiThread
public void deleteUnsentAttachments() {
List<AttachmentItem> itemsToDelete =
new ArrayList<>(unsentItems.values());
ioExecutor.execute(() -> {
for (AttachmentItem item : itemsToDelete) {
try {
messagingManager.removeAttachment(item.getHeader());
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private boolean isStoring() {
return liveDataFinished != null;
}
}
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);
}
}
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.os.Parcel;
import android.os.Parcelable;
......@@ -12,6 +12,8 @@ import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.concurrent.Immutable;
import static java.util.Objects.requireNonNull;
@Immutable
@NotNullByDefault
public class AttachmentItem implements Parcelable {
......@@ -57,8 +59,8 @@ public class AttachmentItem implements Parcelable {
MessageId messageId = new MessageId(messageIdByte);
width = in.readInt();
height = in.readInt();
String 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;
......@@ -82,27 +84,27 @@ public class AttachmentItem implements Parcelable {
return height;
}
String getMimeType() {
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);
}
......
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.net.Uri;
......@@ -9,7 +9,7 @@ import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentResult {
public class AttachmentItemResult {
@Nullable
private final Uri uri;
......@@ -18,13 +18,13 @@ public class AttachmentResult {
@Nullable
private final String errorMsg;
public AttachmentResult(Uri uri, AttachmentItem item) {
public AttachmentItemResult(Uri uri, AttachmentItem item) {
this.uri = uri;
this.item = item;
this.errorMsg = null;
}
public AttachmentResult(@Nullable String errorMsg) {
public AttachmentItemResult(@Nullable String errorMsg) {
this.uri = null;
this.item = null;
this.errorMsg = errorMsg;
......
package org.briarproject.briar.android.attachment;
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{
AttachmentResult storeAttachments(Collection<Uri> uri);
List<AttachmentHeader> getAttachmentHeadersForSending();
void cancel();
}
package org.briarproject.briar.android.attachment;
import android.arch.lifecycle.LiveData;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.util.Collection;
import javax.annotation.concurrent.Immutable;
@Immutable
@NotNullByDefault
public class AttachmentResult {
private final Collection<LiveData<AttachmentItemResult>> itemResults;
private final LiveData<Boolean> finished;
public AttachmentResult(
Collection<LiveData<AttachmentItemResult>> itemResults,
LiveData<Boolean> finished) {
this.itemResults = itemResults;
this.finished = finished;
}
public Collection<LiveData<AttachmentItemResult>> getItemResults() {
return itemResults;
}
public LiveData<Boolean> getFinished() {