[android] store attachments and actually attach them to sent messages

parent cdf4f3a2
...@@ -90,10 +90,16 @@ class AttachmentController { ...@@ -90,10 +90,16 @@ class AttachmentController {
return attachments; return attachments;
} }
/**
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
* Note: This marks the {@link Attachment}'s {@link InputStream}
* and closes the streams.
*/
List<AttachmentItem> getAttachmentItems( List<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) { List<Pair<AttachmentHeader, Attachment>> attachments) {
List<AttachmentItem> items = new ArrayList<>(attachments.size()); List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> a : attachments) { for (Pair<AttachmentHeader, Attachment> a : attachments) {
a.getSecond().getStream().mark(Integer.MAX_VALUE);
AttachmentItem item = AttachmentItem item =
getAttachmentItem(a.getFirst(), a.getSecond()); getAttachmentItem(a.getFirst(), a.getSecond());
items.add(item); items.add(item);
...@@ -101,12 +107,18 @@ class AttachmentController { ...@@ -101,12 +107,18 @@ class AttachmentController {
return items; return items;
} }
private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) { /**
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
* {@link InputStream}.
* Note: Requires a resettable InputStream
* with the beginning already marked.
* The stream will be closed when this method returns.
*/
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) {
MessageId messageId = h.getMessageId(); MessageId messageId = h.getMessageId();
Size size = new Size(); Size size = new Size();
InputStream is = a.getStream(); InputStream is = a.getStream();
is.mark(Integer.MAX_VALUE);
try { try {
// use exif to get size // use exif to get size
if (h.getContentType().equals("image/jpeg")) { if (h.getContentType().equals("image/jpeg")) {
...@@ -178,6 +190,13 @@ class AttachmentController { ...@@ -178,6 +190,13 @@ class AttachmentController {
options.outMimeType); options.outMimeType);
} }
static String getContentTypeFromBitmap(InputStream is) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
return options.outMimeType;
}
private Size getThumbnailSize(int width, int height, String mimeType) { private Size getThumbnailSize(int width, int height, String mimeType) {
float widthPercentage = maxWidth / (float) width; float widthPercentage = maxWidth / (float) width;
float heightPercentage = maxHeight / (float) height; float heightPercentage = maxHeight / (float) height;
......
...@@ -29,7 +29,6 @@ import android.widget.ImageView; ...@@ -29,7 +29,6 @@ import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
...@@ -49,7 +48,6 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent; ...@@ -49,7 +48,6 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager; import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
...@@ -83,7 +81,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager; ...@@ -83,7 +81,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.Attachment; import org.briarproject.briar.api.messaging.Attachment;
import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.AttachmentHeader;
import org.briarproject.briar.api.messaging.MessagingManager; 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.PrivateMessageFactory;
import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
...@@ -196,8 +193,6 @@ public class ConversationActivity extends BriarActivity ...@@ -196,8 +193,6 @@ public class ConversationActivity extends BriarActivity
ViewModelProvider.Factory viewModelFactory; ViewModelProvider.Factory viewModelFactory;
private volatile ContactId contactId; private volatile ContactId contactId;
@Nullable
private volatile GroupId messagingGroupId;
private final Observer<String> contactNameObserver = name -> { private final Observer<String> contactNameObserver = name -> {
requireNonNull(name); requireNonNull(name);
...@@ -245,6 +240,8 @@ public class ConversationActivity extends BriarActivity ...@@ -245,6 +240,8 @@ public class ConversationActivity extends BriarActivity
requireNonNull(deleted); requireNonNull(deleted);
if (deleted) finish(); if (deleted) finish();
}); });
viewModel.getAddedPrivateMessage()
.observe(this, this::onAddedPrivateMessage);
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
setTransitionName(toolbarStatus, getBulbTransitionName(contactId)); setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
...@@ -600,16 +597,11 @@ public class ConversationActivity extends BriarActivity ...@@ -600,16 +597,11 @@ public class ConversationActivity extends BriarActivity
@Override @Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) { public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (!imageUris.isEmpty()) { if (isNullOrEmpty(text) && imageUris.isEmpty())
Toast.makeText(this, "Not yet implemented.", LENGTH_LONG).show(); throw new AssertionError();
textInputView.clearText();
return;
}
if (isNullOrEmpty(text)) throw new AssertionError();
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
if (messagingGroupId == null) loadGroupId(text, timestamp); viewModel.sendMessage(text, imageUris, timestamp);
else createMessage(text, timestamp);
textInputView.clearText(); textInputView.clearText();
} }
...@@ -619,48 +611,10 @@ public class ConversationActivity extends BriarActivity ...@@ -619,48 +611,10 @@ public class ConversationActivity extends BriarActivity
return item == null ? 0 : item.getTime() + 1; return item == null ? 0 : item.getTime() + 1;
} }
private void loadGroupId(String text, long timestamp) { private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
runOnDbThread(() -> { if (h == null) return;
try { addConversationItem(h.accept(visitor));
messagingGroupId = viewModel.onAddedPrivateMessageSeen();
messagingManager.getConversationId(contactId);
createMessage(text, timestamp);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void createMessage(String text, long timestamp) {
cryptoExecutor.execute(() -> {
try {
//noinspection ConstantConditions init in loadGroupId()
storeMessage(privateMessageFactory.createPrivateMessage(
messagingGroupId, timestamp, text, emptyList()), text);
} catch (FormatException e) {
throw new RuntimeException(e);
}
});
}
private void storeMessage(PrivateMessage m, String text) {
runOnDbThread(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, false, false, false,
true, emptyList());
textCache.put(message.getId(), text);
addConversationItem(h.accept(visitor));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
} }
private void askToRemoveContact() { private void askToRemoveContact() {
......
...@@ -5,19 +5,37 @@ import android.arch.lifecycle.AndroidViewModel; ...@@ -5,19 +5,37 @@ import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData; import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.MutableLiveData;
import android.arch.lifecycle.Transformations; import android.arch.lifecycle.Transformations;
import android.content.ContentResolver;
import android.net.Uri;
import android.support.annotation.Nullable; 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.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.NoSuchContactException;
import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.identity.AuthorId;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message;
import org.briarproject.briar.android.util.UiUtils; import org.briarproject.briar.android.util.UiUtils;
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.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.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.logging.Logger; import java.util.logging.Logger;
...@@ -29,6 +47,8 @@ import static java.util.logging.Logger.getLogger; ...@@ -29,6 +47,8 @@ import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now; import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.briar.android.conversation.AttachmentController.getContentTypeFromBitmap;
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
@NotNullByDefault @NotNullByDefault
public class ConversationViewModel extends AndroidViewModel { public class ConversationViewModel extends AndroidViewModel {
...@@ -38,7 +58,11 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -38,7 +58,11 @@ public class ConversationViewModel extends AndroidViewModel {
@DatabaseExecutor @DatabaseExecutor
private final Executor dbExecutor; private final Executor dbExecutor;
@CryptoExecutor
private final Executor cryptoExecutor;
private final MessagingManager messagingManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final PrivateMessageFactory privateMessageFactory;
private final AttachmentController attachmentController; private final AttachmentController attachmentController;
@Nullable @Nullable
...@@ -50,14 +74,24 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -50,14 +74,24 @@ public class ConversationViewModel extends AndroidViewModel {
Transformations.map(contact, UiUtils::getContactDisplayName); Transformations.map(contact, UiUtils::getContactDisplayName);
private final MutableLiveData<Boolean> contactDeleted = private final MutableLiveData<Boolean> contactDeleted =
new MutableLiveData<>(); new MutableLiveData<>();
private final MutableLiveData<GroupId> messagingGroupId =
new MutableLiveData<>();
private final MutableLiveData<PrivateMessageHeader> addedHeader =
new MutableLiveData<>();
@Inject @Inject
ConversationViewModel(Application application, ConversationViewModel(Application application,
@DatabaseExecutor Executor dbExecutor, @DatabaseExecutor Executor dbExecutor,
ContactManager contactManager, MessagingManager messagingManager) { @CryptoExecutor Executor cryptoExecutor,
MessagingManager messagingManager,
ContactManager contactManager,
PrivateMessageFactory privateMessageFactory) {
super(application); super(application);
this.dbExecutor = dbExecutor; this.dbExecutor = dbExecutor;
this.cryptoExecutor = cryptoExecutor;
this.messagingManager = messagingManager;
this.contactManager = contactManager; this.contactManager = contactManager;
this.privateMessageFactory = privateMessageFactory;
this.attachmentController = new AttachmentController(messagingManager, this.attachmentController = new AttachmentController(messagingManager,
application.getResources()); application.getResources());
contactDeleted.setValue(false); contactDeleted.setValue(false);
...@@ -100,6 +134,122 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -100,6 +134,122 @@ public class ConversationViewModel extends AndroidViewModel {
}); });
} }
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) {
if (messagingGroupId.getValue() == null) loadGroupId();
observeForeverOnce(messagingGroupId, groupId -> {
if (groupId == null) return;
// calls through to creating and storing the message
// (wouldn't Kotlin's co-routines be nice here?)
storeAttachments(groupId, text, uris, 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);
}
});
}
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<>();
for (Uri uri : uris) {
Pair<AttachmentHeader, AttachmentItem> pair =
createAttachmentHeader(groupId, uri, timestamp);
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) {
InputStream is = null;
try {
ContentResolver contentResolver =
getApplication().getContentResolver();
is = contentResolver.openInputStream(uri);
if (is == null) throw new IOException();
is = new BufferedInputStream(is); // adds support for reset()
is.mark(Integer.MAX_VALUE);
String contentType = contentResolver.getType(uri);
if (contentType == null) contentType = getContentTypeFromBitmap(is);
AttachmentHeader h = messagingManager
.addLocalAttachment(groupId, timestamp, contentType, is);
AttachmentItem item = attachmentController
.getAttachmentItem(h, new Attachment(is));
return new Pair<>(h, item);
} catch (DbException | IOException e) {
logException(LOG, WARNING, e);
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
}
}
private void createMessage(GroupId groupId, @Nullable String text,
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
long timestamp) {
cryptoExecutor.execute(() -> {
try {
// TODO remove when text can be null in the backend
String msgText = text == null ? "null" : text;
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);
}
});
}
private void storeMessage(PrivateMessage m, @Nullable String text,
List<AttachmentHeader> attachments) {
dbExecutor.execute(() -> {
try {
long start = now();
messagingManager.addLocalMessage(m);
logDuration(LOG, "Storing message", start);
Message message = m.getMessage();
PrivateMessageHeader h = new PrivateMessageHeader(
message.getId(), message.getGroupId(),
message.getTimestamp(), true, true, false, false,
text != null, attachments);
// TODO add text to cache when available here
addedHeader.postValue(h);
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
@UiThread
void onAddedPrivateMessageSeen() {
addedHeader.setValue(null);
}
AttachmentController getAttachmentController() { AttachmentController getAttachmentController() {
return attachmentController; return attachmentController;
} }
...@@ -120,4 +270,8 @@ public class ConversationViewModel extends AndroidViewModel { ...@@ -120,4 +270,8 @@ public class ConversationViewModel extends AndroidViewModel {
return contactDeleted; return contactDeleted;
} }
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
return addedHeader;
}
} }
...@@ -354,4 +354,22 @@ public class UiUtils { ...@@ -354,4 +354,22 @@ public class UiUtils {
}); });
} }
/**
* Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)},
* but without a {@link LifecycleOwner}.
*
* Warning: Do NOT call from objects that have a lifecycle.
*/
@MainThread
public static <T> void observeForeverOnce(LiveData<T> liveData,
Observer<T> observer) {
liveData.observeForever(new Observer<T>() {
@Override
public void onChanged(@Nullable T t) {
observer.onChanged(t);
liveData.removeObserver(this);
}
});
}
} }
...@@ -8,7 +8,7 @@ import org.briarproject.bramble.api.sync.GroupId; ...@@ -8,7 +8,7 @@ import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient;
import java.nio.ByteBuffer; import java.io.InputStream;
@NotNullByDefault @NotNullByDefault
public interface MessagingManager extends ConversationClient { public interface MessagingManager extends ConversationClient {
...@@ -37,7 +37,7 @@ public interface MessagingManager extends ConversationClient { ...@@ -37,7 +37,7 @@ public interface MessagingManager extends ConversationClient {
* Stores a local attachment message. * Stores a local attachment message.
*/ */
AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
String contentType, ByteBuffer data) throws DbException; String contentType, InputStream is) throws DbException;
/** /**
* Returns the ID of the contact with the given private conversation. * Returns the ID of the contact with the given private conversation.
......
...@@ -32,7 +32,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader; ...@@ -32,7 +32,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader;
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent; import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent;
import org.briarproject.briar.client.ConversationClientImpl; import org.briarproject.briar.client.ConversationClientImpl;
import java.nio.ByteBuffer; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
...@@ -152,7 +152,7 @@ class MessagingManagerImpl extends ConversationClientImpl ...@@ -152,7 +152,7 @@ class MessagingManagerImpl extends ConversationClientImpl
@Override @Override
public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
String contentType, ByteBuffer data) { String contentType, InputStream is) {
// TODO add real implementation // TODO add real implementation
byte[] b = new byte[MessageId.LENGTH]; byte[] b = new byte[MessageId.LENGTH];
new Random().nextBytes(b); new Random().nextBytes(b);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment