From 55f4600a69d360e93376025dfcba187eddb92e49 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 15 Feb 2019 09:13:36 -0200 Subject: [PATCH] [android] Create attachments before showing previews --- .../briar/android/blog/ReblogFragment.java | 5 +- .../android/blog/WriteBlogPostActivity.java | 5 +- .../conversation/AttachmentController.java | 55 ++++++++ .../conversation/ConversationActivity.java | 10 +- .../conversation/ConversationViewModel.java | 129 +++++++++--------- .../IntroductionMessageFragment.java | 5 +- .../android/sharing/BaseMessageFragment.java | 5 +- .../android/threaded/ThreadListActivity.java | 5 +- .../briar/android/view/ImagePreview.java | 16 +-- .../android/view/ImagePreviewAdapter.java | 18 +-- .../briar/android/view/ImagePreviewItem.java | 53 +++++++ .../android/view/ImagePreviewViewHolder.java | 8 +- .../view/TextAttachmentController.java | 47 +++++-- .../android/view/TextSendController.java | 4 +- briar-android/src/main/res/values/strings.xml | 2 +- .../briar/api/messaging/AttachmentHeader.java | 11 ++ .../api/messaging/MessagingConstants.java | 15 ++ .../briar/api/messaging/MessagingManager.java | 5 + .../briar/messaging/MessagingManagerImpl.java | 5 + 19 files changed, 291 insertions(+), 112 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index 17e596cb6..3fcdddaa0 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.blog; -import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -21,6 +20,7 @@ import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; @@ -121,7 +121,8 @@ public class ReblogFragment extends BaseFragment implements SendListener { } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { ui.input.hideSoftKeyboard(); feedController.repeatPost(item, text, new UiExceptionHandler(this) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java index 5b68499cd..1ea824b79 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.blog; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.KeyEvent; @@ -27,6 +26,7 @@ import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogPost; import org.briarproject.briar.api.blog.BlogPostFactory; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.security.GeneralSecurityException; import java.util.List; @@ -120,7 +120,8 @@ public class WriteBlogPostActivity extends BriarActivity } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { if (isNullOrEmpty(text)) throw new AssertionError(); // hide publish button, show progress bar diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java index 11833f2ed..a9c997dc2 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 @@ -1,8 +1,11 @@ package org.briarproject.briar.android.conversation; +import android.content.ContentResolver; import android.graphics.BitmapFactory; import android.graphics.BitmapFactory.Options; +import android.net.Uri; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.media.ExifInterface; import android.webkit.MimeTypeMap; @@ -12,6 +15,7 @@ import org.briarproject.bramble.api.Pair; 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.GroupId; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.android.conversation.ImageHelper.DecodeResult; import org.briarproject.briar.api.messaging.Attachment; @@ -25,6 +29,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270; @@ -54,6 +59,7 @@ class AttachmentController { private final int minWidth, maxWidth; private final int minHeight, maxHeight; + private final List unsent = new CopyOnWriteArrayList<>(); private final Map> attachmentCache = new ConcurrentHashMap<>(); @@ -114,6 +120,54 @@ class AttachmentController { return attachments; } + @DatabaseExecutor + AttachmentHeader createAttachmentHeader(ContentResolver contentResolver, + GroupId groupId, Uri uri) + throws IOException, DbException { + InputStream is = contentResolver.openInputStream(uri); + if (is == null) throw new IOException(); + String contentType = contentResolver.getType(uri); + if (contentType == null) throw new IOException("null content type"); + long timestamp = System.currentTimeMillis(); + AttachmentHeader h = messagingManager + .addLocalAttachment(groupId, timestamp, contentType, is); + tryToClose(is, LOG, WARNING); + unsent.add(h); + return h; + } + + @DatabaseExecutor + void deleteUnsentAttachments() { + for (AttachmentHeader h : unsent) { + try { + messagingManager.removeAttachment(h); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + } + } + + List getUnsentAttachments() { + return new ArrayList<>(unsent); + } + + void markAttachmentsSent() { + unsent.clear(); + } + + @DatabaseExecutor + AttachmentItem getAttachmentItem(ContentResolver contentResolver, Uri uri, + AttachmentHeader h, boolean needsSize) throws IOException { + InputStream is = null; + try { + is = contentResolver.openInputStream(uri); + if (is == null) throw new IOException(); + return getAttachmentItem(h, new Attachment(is), needsSize); + } finally { + if (is != null) tryToClose(is, LOG, WARNING); + } + } + /** * Creates {@link AttachmentItem}s from the passed headers and Attachments. *

@@ -135,6 +189,7 @@ class AttachmentController { * Creates an {@link AttachmentItem} from the {@link Attachment}'s * {@link InputStream} which will be closed when this method returns. */ + @VisibleForTesting AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a, boolean needsSize) { MessageId messageId = h.getMessageId(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index c844f030c..fac65e937 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -6,7 +6,6 @@ import android.arch.lifecycle.ViewModelProvider; import android.arch.lifecycle.ViewModelProviders; import android.content.DialogInterface; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.Nullable; @@ -264,7 +263,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 @@ -658,12 +657,13 @@ public class ConversationActivity extends BriarActivity } @Override - public void onSendClick(@Nullable String text, List imageUris) { - if (isNullOrEmpty(text) && imageUris.isEmpty()) + public void onSendClick(@Nullable String text, + List attachmentHeaders) { + if (isNullOrEmpty(text) && attachmentHeaders.isEmpty()) throw new AssertionError(); long timestamp = System.currentTimeMillis(); timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); - viewModel.sendMessage(text, imageUris, timestamp); + viewModel.sendMessage(text, attachmentHeaders, timestamp); textInputView.clearText(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationViewModel.java index ba1457e7c..9182540ea 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 @@ -11,7 +11,6 @@ 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; @@ -27,10 +26,11 @@ 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.system.AndroidExecutor; import org.briarproject.briar.android.util.UiUtils; +import org.briarproject.briar.android.view.TextAttachmentController.AttachmentManager; 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; @@ -38,10 +38,11 @@ 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.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -50,7 +51,6 @@ 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; @@ -59,7 +59,8 @@ import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_ 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()); @@ -73,6 +74,7 @@ public class ConversationViewModel extends AndroidViewModel { @CryptoExecutor private final Executor cryptoExecutor; private final TransactionManager db; + private final AndroidExecutor androidExecutor; private final MessagingManager messagingManager; private final ContactManager contactManager; private final SettingsManager settingsManager; @@ -100,17 +102,20 @@ public class ConversationViewModel extends AndroidViewModel { new MutableLiveData<>(); private final MutableLiveData addedHeader = new MutableLiveData<>(); + // TODO move to AttachmentController + private final Map unsentItems = new HashMap<>(); @Inject ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, @CryptoExecutor Executor cryptoExecutor, TransactionManager db, - MessagingManager messagingManager, ContactManager contactManager, - SettingsManager settingsManager, + AndroidExecutor androidExecutor, MessagingManager messagingManager, + ContactManager contactManager, SettingsManager settingsManager, PrivateMessageFactory privateMessageFactory) { super(application); this.dbExecutor = dbExecutor; this.cryptoExecutor = cryptoExecutor; + this.androidExecutor = androidExecutor; this.db = db; this.messagingManager = messagingManager; this.contactManager = contactManager; @@ -121,6 +126,12 @@ public class ConversationViewModel extends AndroidViewModel { contactDeleted.setValue(false); } + @Override + protected void onCleared() { + super.onCleared(); + attachmentController.deleteUnsentAttachments(); + } + /** * Setting the {@link ContactId} automatically triggers loading of other * data. @@ -176,15 +187,56 @@ public class ConversationViewModel extends AndroidViewModel { }); } - void sendMessage(@Nullable String text, List uris, long timestamp) { + void sendMessage(@Nullable String text, + List attachmentHeaders, long timestamp) { if (messagingGroupId.getValue() == null) loadGroupId(); observeForeverOnce(messagingGroupId, groupId -> { if (groupId == null) return; - // calls through to creating and storing the message - storeAttachments(groupId, text, uris, timestamp); + createMessage(groupId, text, attachmentHeaders, timestamp); }); } + @Override + public void storeAttachment(Uri uri, boolean needsSize, Runnable onSuccess, + Runnable onError) { + if (unsentItems.containsKey(uri)) { + // This can happen due to configuration (screen orientation) change. + // So don't create a new attachment, if we have one already. + androidExecutor.runOnUiThread(onSuccess); + return; + } + if (messagingGroupId.getValue() == null) loadGroupId(); + observeForeverOnce(messagingGroupId, groupId -> dbExecutor.execute(() + -> { + if (groupId == null) throw new IllegalStateException(); + long start = now(); + try { + ContentResolver contentResolver = + getApplication().getContentResolver(); + AttachmentHeader h = attachmentController + .createAttachmentHeader(contentResolver, groupId, uri); + unsentItems.put(uri, attachmentController + .getAttachmentItem(contentResolver, uri, h, needsSize)); + androidExecutor.runOnUiThread(onSuccess); + } catch (DbException | IOException e) { + logException(LOG, WARNING, e); + androidExecutor.runOnUiThread(onError); + } + logDuration(LOG, "Storing attachment", start); + })); + } + + @Override + public List getAttachments() { + return attachmentController.getUnsentAttachments(); + } + + @Override + public void removeAttachments() { + unsentItems.clear(); + dbExecutor.execute(attachmentController::deleteUnsentAttachments); + } + private void loadGroupId() { if (contactId == null) throw new IllegalStateException(); dbExecutor.execute(() -> { @@ -252,58 +304,8 @@ public class ConversationViewModel extends AndroidViewModel { }); } - private void storeAttachments(GroupId groupId, @Nullable String text, - List uris, long timestamp) { - dbExecutor.execute(() -> { - long start = now(); - List attachments = new ArrayList<>(); - List items = new ArrayList<>(); - boolean needsSize = uris.size() == 1; - for (Uri uri : uris) { - Pair pair = - createAttachmentHeader(groupId, uri, timestamp, - needsSize); - if (pair == null) continue; - attachments.add(pair.getFirst()); - items.add(pair.getSecond()); - } - logDuration(LOG, "Storing attachments", start); - createMessage(groupId, text, attachments, items, timestamp); - }); - } - - @Nullable - @DatabaseExecutor - private Pair createAttachmentHeader( - GroupId groupId, Uri uri, long timestamp, boolean needsSize) { - InputStream is = null; - try { - ContentResolver contentResolver = - getApplication().getContentResolver(); - is = contentResolver.openInputStream(uri); - if (is == null) throw new IOException(); - String contentType = contentResolver.getType(uri); - if (contentType == null) throw new IOException("null content type"); - AttachmentHeader h = messagingManager - .addLocalAttachment(groupId, timestamp, contentType, is); - is.close(); - // re-open stream to get AttachmentItem - is = contentResolver.openInputStream(uri); - if (is == null) throw new IOException(); - AttachmentItem item = attachmentController - .getAttachmentItem(h, new Attachment(is), needsSize); - return new Pair<>(h, item); - } catch (DbException | IOException e) { - logException(LOG, WARNING, e); - return null; - } finally { - if (is != null) tryToClose(is, LOG, WARNING); - } - } - private void createMessage(GroupId groupId, @Nullable String text, - List attachments, List aItems, - long timestamp) { + List attachments, long timestamp) { cryptoExecutor.execute(() -> { try { // TODO remove when text can be null in the backend @@ -311,7 +313,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); @@ -331,6 +332,10 @@ public class ConversationViewModel extends AndroidViewModel { message.getId(), message.getGroupId(), message.getTimestamp(), true, true, false, false, text != null, attachments); + attachmentController.put(m.getMessage().getId(), + new ArrayList<>(unsentItems.values())); + unsentItems.clear(); + attachmentController.markAttachmentsSent(); // TODO add text to cache when available here addedHeader.postValue(h); } catch (DbException e) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java index dee769cf3..70478b884 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.introduction; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; @@ -26,6 +25,7 @@ import org.briarproject.briar.android.view.TextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.api.introduction.IntroductionManager; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; import java.util.logging.Logger; @@ -193,7 +193,8 @@ public class IntroductionMessageFragment extends BaseFragment } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { // disable button to prevent accidental double invitations ui.message.setReady(false); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java index 57c299fa4..eaff51eb4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.sharing; import android.content.Context; -import android.net.Uri; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.annotation.StringRes; @@ -19,6 +18,7 @@ import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.view.LargeTextInputView; import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; @@ -83,7 +83,8 @@ public abstract class BaseMessageFragment extends BaseFragment } @Override - public void onSendClick(@Nullable String text, List imageUris) { + public void onSendClick(@Nullable String text, + List headers) { // disable button to prevent accidental double actions sendController.setReady(false); message.hideSoftKeyboard(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index f74626b67..80456c453 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.threaded; import android.content.Intent; -import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.CallSuper; @@ -34,6 +33,7 @@ import org.briarproject.briar.android.view.TextSendController; import org.briarproject.briar.android.view.TextSendController.SendListener; import org.briarproject.briar.android.view.UnreadMessageButton; import org.briarproject.briar.api.client.NamedGroup; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.Collection; import java.util.List; @@ -341,7 +341,8 @@ public abstract class ThreadListActivity imageUris) { + public void onSendClick(@Nullable String text, + List headers) { if (isNullOrEmpty(text)) throw new AssertionError(); I replyItem = adapter.getHighlightedItem(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java index f4158a495..78f74a189 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreview.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.view; import android.content.Context; -import android.net.Uri; import android.support.annotation.Nullable; import android.support.constraint.ConstraintLayout; import android.support.v7.widget.RecyclerView; @@ -60,21 +59,22 @@ public class ImagePreview extends ConstraintLayout { this.listener = listener; } - void showPreview(Collection imageUris) { + void showPreview(Collection items) { if (listener == null) throw new IllegalStateException(); - if (imageUris.size() == 1) { + if (items.size() == 1) { LayoutParams params = (LayoutParams) imageList.getLayoutParams(); params.width = MATCH_PARENT; imageList.setLayoutParams(params); } setVisibility(VISIBLE); - imageList.setAdapter(new ImagePreviewAdapter(imageUris, listener)); + ImagePreviewAdapter adapter = new ImagePreviewAdapter(items, listener); + imageList.setAdapter(adapter); } - void removeUri(Uri uri) { + void loadPreviewImage(ImagePreviewItem item) { ImagePreviewAdapter adapter = - (ImagePreviewAdapter) imageList.getAdapter(); - requireNonNull(adapter).removeUri(uri); + ((ImagePreviewAdapter) imageList.getAdapter()); + requireNonNull(adapter).loadItemPreview(item); } interface ImagePreviewListener { @@ -86,7 +86,7 @@ public class ImagePreview extends ConstraintLayout { * * Warning: Glide may call this multiple times. */ - void onUriError(Uri uri); + void onError(); void onCancel(); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java index a6508e4eb..70735fdd6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewAdapter.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.view; -import android.net.Uri; import android.support.annotation.LayoutRes; import android.support.v7.widget.RecyclerView.Adapter; import android.view.LayoutInflater; @@ -15,17 +14,19 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import static android.support.v7.widget.RecyclerView.NO_POSITION; import static java.util.Objects.requireNonNull; @NotNullByDefault class ImagePreviewAdapter extends Adapter { - private final List items; + private final List items; private final ImagePreviewListener listener; @LayoutRes private final int layout; - ImagePreviewAdapter(Collection items, ImagePreviewListener listener) { + ImagePreviewAdapter(Collection items, + ImagePreviewListener listener) { this.items = new ArrayList<>(items); this.listener = listener; this.layout = items.size() == 1 ? @@ -52,11 +53,12 @@ class ImagePreviewAdapter extends Adapter { return items.size(); } - void removeUri(Uri uri) { - int pos = items.indexOf(uri); - if (pos == -1) return; - items.remove(uri); - notifyItemRemoved(pos); + void loadItemPreview(ImagePreviewItem item) { + int pos = items.indexOf(item); + if (pos == NO_POSITION) throw new AssertionError(); + ImagePreviewItem newItem = items.get(pos); + newItem.setWaitForLoading(false); + notifyItemChanged(pos, newItem); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java new file mode 100644 index 000000000..71576015f --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewItem.java @@ -0,0 +1,53 @@ +package org.briarproject.briar.android.view; + +import android.net.Uri; +import android.support.annotation.Nullable; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@NotNullByDefault +class ImagePreviewItem { + + private final Uri uri; + private boolean waitForLoading = true; + + private ImagePreviewItem(Uri uri) { + this.uri = uri; + } + + Uri getUri() { + return uri; + } + + void setWaitForLoading(boolean waitForLoading) { + this.waitForLoading = waitForLoading; + } + + boolean waitForLoading() { + return waitForLoading; + } + + static List fromUris(Collection uris) { + List items = new ArrayList<>(uris.size()); + for (Uri uri : uris) { + items.add(new ImagePreviewItem(uri)); + } + return items; + } + + @Override + public boolean equals(@Nullable Object o) { + return o instanceof ImagePreviewItem && + uri.equals(((ImagePreviewItem) o).uri); + } + + @Override + public int hashCode() { + return uri.hashCode(); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java index c16ce14ca..a18ae3c17 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/ImagePreviewViewHolder.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.view; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView.ViewHolder; @@ -42,9 +41,10 @@ class ImagePreviewViewHolder extends ViewHolder { this.progressBar = v.findViewById(R.id.progressBar); } - void bind(Uri uri) { + void bind(ImagePreviewItem item) { + if (item.waitForLoading()) return; GlideApp.with(imageView) - .load(uri) + .load(item.getUri()) .diskCacheStrategy(NONE) .error(ERROR_RES) .downsample(FIT_CENTER) @@ -55,7 +55,7 @@ class ImagePreviewViewHolder extends ViewHolder { Object model, Target target, boolean isFirstResource) { progressBar.setVisibility(INVISIBLE); - listener.onUriError(uri); + listener.onError(); return false; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java index 3a73ba695..bbfeca9b4 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextAttachmentController.java @@ -16,6 +16,7 @@ import android.widget.Toast; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.view.ImagePreview.ImagePreviewListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.ArrayList; import java.util.List; @@ -46,6 +47,7 @@ public class TextAttachmentController extends TextSendController private final ImagePreview imagePreview; private final AttachImageListener imageListener; private final CompositeSendButton sendButton; + private final AttachmentManager attachmentManager; private CharSequence textHint; private List imageUris = emptyList(); @@ -53,10 +55,12 @@ public class TextAttachmentController extends TextSendController private boolean loadingPreviews = 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; @@ -84,7 +88,8 @@ public class TextAttachmentController extends TextSendController @Override public void onSendEvent() { if (canSend()) { - listener.onSendClick(textInput.getText(), imageUris); + listener.onSendClick(textInput.getText(), + attachmentManager.getAttachments()); reset(); } } @@ -139,7 +144,15 @@ public class TextAttachmentController extends TextSendController loadingPreviews = true; updateViewState(); textInput.setHint(R.string.image_caption_hint); - imagePreview.showPreview(imageUris); + List items = ImagePreviewItem.fromUris(imageUris); + imagePreview.showPreview(items); + // store attachments and show preview when successful + boolean needsSize = items.size() == 1; + for (ImagePreviewItem item : items) { + attachmentManager.storeAttachment(item.getUri(), needsSize, + () -> imagePreview.loadPreviewImage(item), + this::onError); + } } private void reset() { @@ -180,22 +193,16 @@ public class TextAttachmentController extends TextSendController } @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; - } - imagePreview.removeUri(uri); - if (imageUris.isEmpty()) onCancel(); + public void onError() { Toast.makeText(textInput.getContext(), R.string.image_attach_error, LENGTH_LONG).show(); - checkAllPreviewsLoaded(); + onCancel(); } @Override public void onCancel() { textInput.clearText(); + attachmentManager.removeAttachments(); reset(); } @@ -265,4 +272,20 @@ public class TextAttachmentController extends TextSendController void onAttachImage(Intent intent); } + public interface AttachmentManager { + /** + * Stores a new attachment in the database. + * + * @param uri The Uri of the attachment to store. + * @param onSuccess will be run on the UiThread when the attachment was stored successfully. + * @param onError will be run on the UiThread when the attachment could not be stored. + */ + void storeAttachment(Uri uri, boolean needsSize, Runnable onSuccess, + Runnable onError); + + List getAttachments(); + + void removeAttachments(); + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java index f0a864afe..725d23baa 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextSendController.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.view; -import android.net.Uri; import android.os.Parcelable; import android.support.annotation.Nullable; import android.support.annotation.UiThread; @@ -10,6 +9,7 @@ import android.view.View; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.view.EmojiTextInputView.TextInputListener; +import org.briarproject.briar.api.messaging.AttachmentHeader; import java.util.List; @@ -85,7 +85,7 @@ public class TextSendController implements TextInputListener { } public interface SendListener { - void onSendClick(@Nullable String text, List imageUris); + void onSendClick(@Nullable String text, List headers); } } diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 144581e5d..4565d2a55 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -129,7 +129,7 @@ Type message Add a caption (optional) Attach image - Could not attach image + Could not attach image(s) Change contact name Contact name Change diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java index 685e2ea3a..970401211 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/AttachmentHeader.java @@ -25,4 +25,15 @@ public class AttachmentHeader { return contentType; } + @Override + public boolean equals(Object o) { + return o instanceof AttachmentHeader && + messageId.equals(((AttachmentHeader) o).messageId); + } + + @Override + public int hashCode() { + return messageId.hashCode(); + } + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java index 1efdd748d..c6114dc66 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingConstants.java @@ -8,4 +8,19 @@ 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. + */ + int MAX_IMAGE_SIZE = 6 * 1024 * 1024; + } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java b/briar-api/src/main/java/org/briarproject/briar/api/messaging/MessagingManager.java index a4f91af5c..d3ba4acf1 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 @@ -41,6 +41,11 @@ public interface MessagingManager extends ConversationClient { AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp, String contentType, InputStream is) throws DbException, IOException; + /** + * Removes an unsent attachment. + */ + void removeAttachment(AttachmentHeader header) throws DbException; + /** * Returns the ID of the contact with the given private conversation. */ diff --git a/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/messaging/MessagingManagerImpl.java index 44073b377..d46d9d8e0 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 @@ -166,6 +166,11 @@ class MessagingManagerImpl extends ConversationClientImpl return new AttachmentHeader(new MessageId(b), "image/png"); } + @Override + public void removeAttachment(AttachmentHeader header) throws DbException { + // TODO add real implementation + } + private ContactId getContactId(Transaction txn, GroupId g) throws DbException { try { -- GitLab