From 4796902b9cbc109fbe50e3aaa44dd38a54b49db8 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Fri, 23 Nov 2018 18:58:32 -0200 Subject: [PATCH] [android] store attachments and actually attach them to sent messages --- .../conversation/AttachmentController.java | 23 ++- .../conversation/ConversationActivity.java | 64 +------ .../conversation/ConversationViewModel.java | 156 +++++++++++++++++- .../briar/android/util/UiUtils.java | 18 ++ .../briar/api/messaging/MessagingManager.java | 4 +- .../briar/messaging/MessagingManagerImpl.java | 4 +- 6 files changed, 207 insertions(+), 62 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java index 2d85e26687..b1a37456f5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java @@ -90,10 +90,16 @@ class AttachmentController { 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<Pair<AttachmentHeader, Attachment>> attachments) { List<AttachmentItem> items = new ArrayList<>(attachments.size()); for (Pair<AttachmentHeader, Attachment> a : attachments) { + a.getSecond().getStream().mark(Integer.MAX_VALUE); AttachmentItem item = getAttachmentItem(a.getFirst(), a.getSecond()); items.add(item); @@ -101,12 +107,18 @@ class AttachmentController { 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(); Size size = new Size(); InputStream is = a.getStream(); - is.mark(Integer.MAX_VALUE); try { // use exif to get size if (h.getContentType().equals("image/jpeg")) { @@ -178,6 +190,13 @@ class AttachmentController { 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) { float widthPercentage = maxWidth / (float) width; float heightPercentage = maxHeight / (float) height; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 3d3611f906..9f48b56283 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -29,7 +29,6 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; -import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; @@ -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.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.bramble.api.sync.event.MessagesAckedEvent; import org.briarproject.bramble.api.sync.event.MessagesSentEvent; @@ -83,7 +81,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager; 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 org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; @@ -196,8 +193,6 @@ public class ConversationActivity extends BriarActivity ViewModelProvider.Factory viewModelFactory; private volatile ContactId contactId; - @Nullable - private volatile GroupId messagingGroupId; private final Observer<String> contactNameObserver = name -> { requireNonNull(name); @@ -245,6 +240,8 @@ public class ConversationActivity extends BriarActivity requireNonNull(deleted); if (deleted) finish(); }); + viewModel.getAddedPrivateMessage() + .observe(this, this::onAddedPrivateMessage); setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarStatus, getBulbTransitionName(contactId)); @@ -600,16 +597,11 @@ public class ConversationActivity extends BriarActivity @Override public void onSendClick(@Nullable String text, List<Uri> imageUris) { - if (!imageUris.isEmpty()) { - Toast.makeText(this, "Not yet implemented.", LENGTH_LONG).show(); - textInputView.clearText(); - return; - } - if (isNullOrEmpty(text)) throw new AssertionError(); + if (isNullOrEmpty(text) && imageUris.isEmpty()) + throw new AssertionError(); long timestamp = System.currentTimeMillis(); timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); - if (messagingGroupId == null) loadGroupId(text, timestamp); - else createMessage(text, timestamp); + viewModel.sendMessage(text, imageUris, timestamp); textInputView.clearText(); } @@ -619,48 +611,10 @@ public class ConversationActivity extends BriarActivity return item == null ? 0 : item.getTime() + 1; } - private void loadGroupId(String text, long timestamp) { - runOnDbThread(() -> { - try { - messagingGroupId = - 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 onAddedPrivateMessage(@Nullable PrivateMessageHeader h) { + if (h == null) return; + addConversationItem(h.accept(visitor)); + viewModel.onAddedPrivateMessageSeen(); } private void askToRemoveContact() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index 3a8e6e3640..b4e158253c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java @@ -5,19 +5,37 @@ 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; +import org.briarproject.bramble.api.crypto.CryptoExecutor; import org.briarproject.bramble.api.db.DatabaseExecutor; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.identity.AuthorId; 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.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.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.logging.Logger; @@ -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.logException; 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 public class ConversationViewModel extends AndroidViewModel { @@ -38,7 +58,11 @@ public class ConversationViewModel extends AndroidViewModel { @DatabaseExecutor private final Executor dbExecutor; + @CryptoExecutor + private final Executor cryptoExecutor; + private final MessagingManager messagingManager; private final ContactManager contactManager; + private final PrivateMessageFactory privateMessageFactory; private final AttachmentController attachmentController; @Nullable @@ -50,14 +74,24 @@ public class ConversationViewModel extends AndroidViewModel { Transformations.map(contact, UiUtils::getContactDisplayName); private final MutableLiveData<Boolean> contactDeleted = new MutableLiveData<>(); + private final MutableLiveData<GroupId> messagingGroupId = + new MutableLiveData<>(); + private final MutableLiveData<PrivateMessageHeader> addedHeader = + new MutableLiveData<>(); @Inject ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, - ContactManager contactManager, MessagingManager messagingManager) { + @CryptoExecutor Executor cryptoExecutor, + MessagingManager messagingManager, + ContactManager contactManager, + PrivateMessageFactory privateMessageFactory) { super(application); this.dbExecutor = dbExecutor; + this.cryptoExecutor = cryptoExecutor; + this.messagingManager = messagingManager; this.contactManager = contactManager; + this.privateMessageFactory = privateMessageFactory; this.attachmentController = new AttachmentController(messagingManager, application.getResources()); contactDeleted.setValue(false); @@ -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() { return attachmentController; } @@ -120,4 +270,8 @@ public class ConversationViewModel extends AndroidViewModel { return contactDeleted; } + LiveData<PrivateMessageHeader> getAddedPrivateMessage() { + return addedHeader; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index fcd7fcb146..b74e8a216e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -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); + } + }); + } + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java index d7f4997a7e..91b8d866c3 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java @@ -8,7 +8,7 @@ import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; -import java.nio.ByteBuffer; +import java.io.InputStream; @NotNullByDefault public interface MessagingManager extends ConversationClient { @@ -37,7 +37,7 @@ public interface MessagingManager extends ConversationClient { * Stores a local attachment message. */ 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. diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java index bc92e69520..d8c06c4aa1 100644 --- a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java @@ -32,7 +32,7 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent; import org.briarproject.briar.client.ConversationClientImpl; -import java.nio.ByteBuffer; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @@ -152,7 +152,7 @@ class MessagingManagerImpl extends ConversationClientImpl @Override public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, - String contentType, ByteBuffer data) { + String contentType, InputStream is) { // TODO add real implementation byte[] b = new byte[MessageId.LENGTH]; new Random().nextBytes(b); -- GitLab