Merge branch '1468-reject-unsupported-images' into 'master'

Reject unsupported images

Closes #1468

See merge request briar/briar!1038
This commit is contained in:
akwizgran
2019-06-17 16:39:26 +00:00
45 changed files with 947 additions and 351 deletions

View File

@@ -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());

View File

@@ -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<Uri> 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<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);
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;
}
}

View File

@@ -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<Uri> uris = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<AttachmentItemResult> itemResults =
new CopyOnWriteArrayList<>();
private final MutableLiveData<AttachmentResult> 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<AttachmentResult> storeAttachments(
LiveData<GroupId> groupId, Collection<Uri> 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<AttachmentResult> 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<AttachmentHeader> getAttachmentHeadersForSending() {
List<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
headers.add(itemResult.getItem().getHeader());
}
return headers;
}
/**
* 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<AttachmentItem> items = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() == null) throw new IllegalStateException();
items.add(itemResult.getItem());
}
retriever.cachePut(id, items);
resetState();
}
/**
* 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<AttachmentHeader> headers = new ArrayList<>(itemResults.size());
for (AttachmentItemResult itemResult : itemResults) {
// check if we are trying to send attachment items with errors
if (itemResult.getItem() != null)
headers.add(itemResult.getItem().getHeader());
}
ioExecutor.execute(() -> {
for (AttachmentHeader header : headers) {
try {
messagingManager.removeAttachment(header);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
}
});
}
private AttachmentResult getResult(boolean finished) {
// Make a copy of the list,
// because our copy will continue to change in the background.
// (As it's a CopyOnWriteArrayList,
// the code that receives the result can safely do simple things
// like iterating over the list,
// but anything that involves calling more than one list method
// is still unsafe.)
Collection<AttachmentItemResult> items = new ArrayList<>(itemResults);
return new AttachmentResult(items, finished);
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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<AttachmentResult> storeAttachments(Collection<Uri> uri,
boolean restart);
List<AttachmentHeader> getAttachmentHeadersForSending();
void cancel();
}

View File

@@ -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<AttachmentItemResult> itemResults;
private final boolean finished;
public AttachmentResult(Collection<AttachmentItemResult> itemResults,
boolean finished) {
this.itemResults = itemResults;
this.finished = finished;
}
public Collection<AttachmentItemResult> getItemResults() {
return itemResults;
}
public boolean isFinished() {
return finished;
}
}

View File

@@ -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<MessageId, List<AttachmentItem>> 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<AttachmentItem> attachments) {
public void cachePut(MessageId messageId, List<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Nullable
List<AttachmentItem> get(MessageId messageId) {
public List<AttachmentItem> cacheGet(MessageId messageId) {
return attachmentCache.get(messageId);
}
@DatabaseExecutor
List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
public List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException {
long start = now();
List<Pair<AttachmentHeader, Attachment>> 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.
* <p>
* Note: This closes the {@link Attachment}'s {@link InputStream}.
*/
List<AttachmentItem> getAttachmentItems(
public List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
boolean needsSize = attachments.size() == 1;
List<AttachmentItem> 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);
}
/**

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import android.support.annotation.Nullable;

View File

@@ -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<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
ui.input.hideSoftKeyboard();
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {

View File

@@ -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<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError();
// hide publish button, show progress bar

View File

@@ -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<AttachmentItem> items = attachmentController.get(id);
List<AttachmentItem> 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<Pair<AttachmentHeader, Attachment>> attachments =
attachmentController.getMessageAttachments(headers);
attachmentRetriever.getMessageAttachments(headers);
// TODO move getting the items off to IoExecutor, if size == 1
List<AttachmentItem> 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<AttachmentItem> items) {
runOnUiThreadUnlessDestroyed(() -> {
attachmentController.put(m, items);
attachmentRetriever.cachePut(m, items);
Pair<Integer, ConversationMessageItem> pair =
adapter.getMessageItem(m);
if (pair != null) {
@@ -658,12 +659,13 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text) && imageUris.isEmpty())
public void onSendClick(@Nullable String text,
List<AttachmentHeader> 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<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers) {
List<AttachmentItem> attachments = attachmentController.get(m);
List<AttachmentItem> attachments = attachmentRetriever.cacheGet(m);
if (attachments == null) {
loadMessageAttachments(m, headers);
return emptyList();

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<String> contactName =
Transformations.map(contact, UiUtils::getContactDisplayName);
private final LiveData<GroupId> messagingGroupId;
private final MutableLiveData<Boolean> imageSupport =
new MutableLiveData<>();
private final MutableLiveEvent<Boolean> showImageOnboarding =
@@ -96,15 +97,14 @@ public class ConversationViewModel extends AndroidViewModel {
new MutableLiveData<>();
private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>();
private final MutableLiveData<GroupId> messagingGroupId =
new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
private final MutableLiveEvent<PrivateMessageHeader> 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<Uri> uris, long timestamp) {
if (messagingGroupId.getValue() == null) loadGroupId();
void sendMessage(@Nullable String text,
List<AttachmentHeader> 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<AttachmentResult> storeAttachments(Collection<Uri> uris,
boolean restart) {
if (restart) {
return attachmentCreator.getLiveAttachments();
} else {
// messagingGroupId is loaded with the contact
return attachmentCreator.storeAttachments(messagingGroupId, uris);
}
}
@Override
@UiThread
public List<AttachmentHeader> 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<Uri> uris, long timestamp) {
dbExecutor.execute(() -> {
long start = now();
List<AttachmentHeader> attachments = new ArrayList<>();
List<AttachmentItem> items = new ArrayList<>();
boolean needsSize = uris.size() == 1;
for (Uri uri : uris) {
Pair<AttachmentHeader, AttachmentItem> 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<AttachmentHeader, AttachmentItem> 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<AttachmentHeader> attachments, List<AttachmentItem> aItems,
long timestamp) {
List<AttachmentHeader> 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<AttachmentHeader> 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<Contact> getContact() {
@@ -380,7 +347,7 @@ public class ConversationViewModel extends AndroidViewModel {
return contactDeleted;
}
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
LiveEvent<PrivateMessageHeader> getAddedPrivateMessage() {
return addedHeader;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
// disable button to prevent accidental double invitations
ui.message.setReady(false);

View File

@@ -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<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
// disable button to prevent accidental double actions
sendController.setReady(false);
message.hideSoftKeyboard();

View File

@@ -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<G extends NamedGroup, I extends ThreadI
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
public void onSendClick(@Nullable String text,
List<AttachmentHeader> headers) {
if (isNullOrEmpty(text)) throw new AssertionError();
I replyItem = adapter.getHighlightedItem();

View File

@@ -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<Uri> imageUris) {
void showPreview(Collection<ImagePreviewItem> 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();
}

View File

@@ -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<ImagePreviewViewHolder> {
private final List<Uri> items;
private final ImagePreviewListener listener;
private final List<ImagePreviewItem> items;
@LayoutRes
private final int layout;
ImagePreviewAdapter(Collection<Uri> items, ImagePreviewListener listener) {
ImagePreviewAdapter(Collection<ImagePreviewItem> 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<ImagePreviewViewHolder> {
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<ImagePreviewViewHolder> {
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;
}
}

View File

@@ -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<ImagePreviewItem> fromUris(Collection<Uri> uris) {
List<ImagePreviewItem> 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();
}
}

View File

@@ -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<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> 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<Drawable>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Drawable> target,
boolean isFirstResource) {
progressBar.setVisibility(INVISIBLE);
return false;
}
@Override
public boolean onResourceReady(Drawable resource,
Object model, Target<Drawable> target,
DataSource dataSource, boolean isFirstResource) {
progressBar.setVisibility(INVISIBLE);
listener.onPreviewLoaded();
return false;
}
})
.into(imageView);
@Override
public boolean onResourceReady(Drawable resource,
Object model, Target<Drawable> target,
DataSource dataSource,
boolean isFirstResource) {
progressBar.setVisibility(INVISIBLE);
return false;
}
})
.into(imageView);
}
}
}

View File

@@ -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<Uri> imageUris = emptyList();
private int previewsLoaded = 0;
private boolean loadingPreviews = false;
private final List<Uri> 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()}.
* <p>
* 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<ImagePreviewItem> items = ImagePreviewItem.fromUris(imageUris);
imagePreview.showPreview(items);
// store attachments and show preview when successful
LiveData<AttachmentResult> result =
attachmentManager.storeAttachments(imageUris, restart);
result.observe(imageListener, new Observer<AttachmentResult>() {
@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<AttachmentItemResult> 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);
}

View File

@@ -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<Uri> imageUris);
void onSendClick(@Nullable String text, List<AttachmentHeader> headers);
}
}

View File

@@ -129,7 +129,9 @@
<string name="message_hint">Type message</string>
<string name="image_caption_hint">Add a caption (optional)</string>
<string name="image_attach">Attach image</string>
<string name="image_attach_error">Could not attach image</string>
<string name="image_attach_error">Could not attach image(s)</string>
<string name="image_attach_error_too_big">Image too big. Limit is %d MB.</string>
<string name="image_attach_error_invalid_mime_type">Image format unsupported: %s</string>
<string name="set_contact_alias">Change contact name</string>
<string name="set_contact_alias_hint">Contact name</string>
<string name="set_alias_button">Change</string>

View File

@@ -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";

View File

@@ -1,4 +1,4 @@
package org.briarproject.briar.android.conversation;
package org.briarproject.briar.android.attachment;
import com.bumptech.glide.util.MarkEnforcingInputStream;

View File

@@ -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();
}
}

View File

@@ -0,0 +1,6 @@
package org.briarproject.briar.api.messaging;
import java.io.IOException;
public class FileTooBigException extends IOException {
}

View File

@@ -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;
}

View File

@@ -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.
*/

View File

@@ -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));
}