From 8a839fb5e4dfde3b14a96df1d53e48c67e844a7a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 5 Nov 2018 18:51:52 -0300 Subject: [PATCH 1/7] [android] display image attachments for conversation messages --- briar-android/build.gradle | 10 + briar-android/proguard-rules.txt | 3 + .../briar/android/AndroidComponent.java | 3 + .../conversation/AttachmentController.java | 198 ++++++++++++++++++ .../android/conversation/AttachmentItem.java | 51 +++++ .../conversation/ConversationActivity.java | 93 ++++++-- .../conversation/ConversationAdapter.java | 14 +- .../ConversationItemViewHolder.java | 6 +- .../conversation/ConversationMessageItem.java | 17 +- .../ConversationMessageViewHolder.java | 149 ++++++++++++- .../conversation/ConversationViewModel.java | 10 +- .../conversation/ConversationVisitor.java | 25 ++- .../conversation/glide/BriarDataFetcher.java | 96 +++++++++ .../conversation/glide/BriarGlideModule.java | 43 ++++ .../conversation/glide/BriarModelLoader.java | 44 ++++ .../glide/BriarModelLoaderFactory.java | 34 +++ .../src/main/res/drawable/ic_image_broken.xml | 9 + .../src/main/res/drawable/msg_in.xml | 8 +- .../src/main/res/drawable/msg_out.xml | 8 +- .../main/res/drawable/msg_status_bubble.xml | 14 ++ .../list_item_conversation_msg_image.xml | 70 +++++++ .../list_item_conversation_msg_image_text.xml | 69 ++++++ .../layout/list_item_conversation_msg_in.xml | 44 +++- .../layout/list_item_conversation_msg_out.xml | 67 ++++-- briar-android/src/main/res/values/color.xml | 1 + briar-android/src/main/res/values/dimens.xml | 9 + briar-android/witness.gradle | 6 + 27 files changed, 1027 insertions(+), 74 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java create mode 100644 briar-android/src/main/res/drawable/ic_image_broken.xml create mode 100644 briar-android/src/main/res/drawable/msg_status_bubble.xml create mode 100644 briar-android/src/main/res/layout/list_item_conversation_msg_image.xml create mode 100644 briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml diff --git a/briar-android/build.gradle b/briar-android/build.gradle index fa356238c..7a16fe6cc 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -104,6 +104,7 @@ dependencies { } implementation "com.android.support:cardview-v7:$supportVersion" implementation "com.android.support:support-annotations:$supportVersion" + implementation "com.android.support:exifinterface:$supportVersion" implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation "android.arch.lifecycle:extensions:1.1.1" @@ -117,8 +118,17 @@ dependencies { implementation 'com.google.zxing:core:3.3.3' implementation 'uk.co.samuelwall:material-tap-target-prompt:2.12.4' implementation 'com.vanniktech:emoji-google:0.5.1' + def glideVersion = '4.8.0' + implementation("com.github.bumptech.glide:glide:$glideVersion") { + exclude group: 'com.android.support' + exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it + } + implementation('jp.wasabeef:glide-transformations:3.3.0') { + exclude module: 'disklrucache' // this gets pulled in here otherwise + } annotationProcessor 'com.google.dagger:dagger-compiler:2.19' + annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" compileOnly 'javax.annotation:jsr250-api:1.0' diff --git a/briar-android/proguard-rules.txt b/briar-android/proguard-rules.txt index feb5984a4..b0148dae4 100644 --- a/briar-android/proguard-rules.txt +++ b/briar-android/proguard-rules.txt @@ -30,3 +30,6 @@ # Emoji -keep class com.vanniktech.emoji.** + +# Glide +-dontwarn com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index 2b1216288..eb3016781 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -29,6 +29,7 @@ import org.briarproject.bramble.api.system.LocationUtils; import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.briar.BriarCoreEagerSingletons; import org.briarproject.briar.BriarCoreModule; +import org.briarproject.briar.android.conversation.glide.BriarModelLoader; import org.briarproject.briar.android.login.SignInReminderReceiver; import org.briarproject.briar.android.reporting.BriarReportSender; import org.briarproject.briar.android.view.TextInputView; @@ -170,6 +171,8 @@ public interface AndroidComponent void inject(TextInputView textInputView); + void inject(BriarModelLoader briarModelLoader); + // Eager singleton load void inject(AppModule.EagerSingletons init); } 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 new file mode 100644 index 000000000..7fa5f3aba --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentController.java @@ -0,0 +1,198 @@ +package org.briarproject.briar.android.conversation; + +import android.content.res.Resources; +import android.graphics.BitmapFactory; +import android.support.annotation.Nullable; +import android.support.media.ExifInterface; + +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.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.MessagingManager; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import static android.support.media.ExifInterface.ORIENTATION_ROTATE_270; +import static android.support.media.ExifInterface.ORIENTATION_ROTATE_90; +import static android.support.media.ExifInterface.ORIENTATION_TRANSPOSE; +import static android.support.media.ExifInterface.ORIENTATION_TRANSVERSE; +import static android.support.media.ExifInterface.TAG_IMAGE_LENGTH; +import static android.support.media.ExifInterface.TAG_IMAGE_WIDTH; +import static android.support.media.ExifInterface.TAG_ORIENTATION; +import static java.util.logging.Level.WARNING; +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; + +@NotNullByDefault +class AttachmentController { + + private static final Logger LOG = + getLogger(AttachmentController.class.getName()); + + private final MessagingManager messagingManager; + private final int defaultSize; + private final int minWidth, maxWidth; + private final int minHeight, maxHeight; + + private final Map> attachmentCache = + new ConcurrentHashMap<>(); + + AttachmentController(MessagingManager messagingManager, Resources res) { + this.messagingManager = messagingManager; + defaultSize = + res.getDimensionPixelSize(R.dimen.message_bubble_image_default); + minWidth = res.getDimensionPixelSize( + R.dimen.message_bubble_image_min_width); + maxWidth = res.getDimensionPixelSize( + R.dimen.message_bubble_image_max_width); + minHeight = res.getDimensionPixelSize( + R.dimen.message_bubble_image_min_height); + maxHeight = res.getDimensionPixelSize( + R.dimen.message_bubble_image_max_height); + } + + void put(MessageId messageId, List attachments) { + attachmentCache.put(messageId, attachments); + } + + @Nullable + List get(MessageId messageId) { + return attachmentCache.get(messageId); + } + + @DatabaseExecutor + List> getMessageAttachments( + List headers) throws DbException { + long start = now(); + List> attachments = + new ArrayList<>(headers.size()); + for (AttachmentHeader h : headers) { + Attachment a = + messagingManager.getAttachment(h.getMessageId()); + attachments.add(new Pair<>(h, a)); + } + logDuration(LOG, "Loading attachment", start); + return attachments; + } + + List getAttachmentItems( + List> attachments) { + List items = new ArrayList<>(attachments.size()); + for (Pair a : attachments) { + AttachmentItem item = + getAttachmentItem(a.getFirst(), a.getSecond()); + items.add(item); + } + return items; + } + + private 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")) { + size = getSizeFromExif(is); + } + } catch (IOException e) { + logException(LOG, WARNING, e); + } + try { + // use BitmapFactory to get size + if (size.error) { + is.reset(); + size = getSizeFromBitmap(is); + } + is.close(); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + + // calculate thumbnail size + Size thumbnailSize = new Size(defaultSize, defaultSize); + if (!size.error) { + thumbnailSize = getThumbnailSize(size.width, size.height); + } + return new AttachmentItem(messageId, size.width, size.height, + thumbnailSize.width, thumbnailSize.height, size.error); + } + + /** + * Gets the size of a JPEG {@link InputStream} if EXIF info is available. + */ + private static Size getSizeFromExif(InputStream is) + throws IOException { + ExifInterface exif = new ExifInterface(is); + // these can return 0 independent of default value + int width = exif.getAttributeInt(TAG_IMAGE_WIDTH, 0); + int height = exif.getAttributeInt(TAG_IMAGE_LENGTH, 0); + if (width == 0 || height == 0) return new Size(); + int orientation = exif.getAttributeInt(TAG_ORIENTATION, 0); + if (orientation == ORIENTATION_ROTATE_90 || + orientation == ORIENTATION_ROTATE_270 || + orientation == ORIENTATION_TRANSVERSE || + orientation == ORIENTATION_TRANSPOSE) { + //noinspection SuspiciousNameCombination + return new Size(height, width); + } + return new Size(width, height); + } + + /** + * Gets the size of any image {@link InputStream}. + */ + private static Size getSizeFromBitmap(InputStream is) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + if (options.outWidth == -1 || options.outHeight == -1) + return new Size(); + return new Size(options.outWidth, options.outHeight); + } + + private Size getThumbnailSize(int width, int height) { + float widthPercentage = maxWidth / (float) width; + float heightPercentage = maxHeight / (float) height; + float scaleFactor = Math.min(widthPercentage, heightPercentage); + int thumbnailWidth = (int) (width * scaleFactor); + int thumbnailHeight = (int) (height * scaleFactor); + if (thumbnailWidth < minWidth) thumbnailWidth = minWidth; + if (thumbnailHeight < minHeight) thumbnailHeight = minHeight; + return new Size(thumbnailWidth, thumbnailHeight); + } + + private static class Size { + private int width; + private int height; + private boolean error; + + private Size(int width, int height) { + this.width = width; + this.height = height; + this.error = false; + } + + private Size() { + this.width = 0; + this.height = 0; + this.error = true; + } + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java new file mode 100644 index 000000000..5a5efb657 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/AttachmentItem.java @@ -0,0 +1,51 @@ +package org.briarproject.briar.android.conversation; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +public class AttachmentItem { + + private final MessageId messageId; + private final int width, height; + private final int thumbnailWidth, thumbnailHeight; + private final boolean hasError; + + AttachmentItem(MessageId messageId, int width, int height, + int thumbnailWidth, int thumbnailHeight, boolean hasError) { + this.messageId = messageId; + this.width = width; + this.height = height; + this.thumbnailWidth = thumbnailWidth; + this.thumbnailHeight = thumbnailHeight; + this.hasError = hasError; + } + + public MessageId getMessageId() { + return messageId; + } + + int getWidth() { + return width; + } + + int getHeight() { + return height; + } + + int getThumbnailWidth() { + return thumbnailWidth; + } + + int getThumbnailHeight() { + return thumbnailHeight; + } + + boolean hasError() { + return hasError; + } + +} 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 ae0d3ddad..5050bc3ec 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 @@ -24,6 +24,7 @@ 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; import org.briarproject.bramble.api.contact.event.ContactRemovedEvent; @@ -51,6 +52,7 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.blog.BlogActivity; +import org.briarproject.briar.android.conversation.ConversationVisitor.AttachmentCache; import org.briarproject.briar.android.conversation.ConversationVisitor.TextCache; import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.introduction.IntroductionActivity; @@ -69,6 +71,8 @@ import org.briarproject.briar.api.conversation.ConversationResponse; import org.briarproject.briar.api.conversation.event.ConversationMessageReceivedEvent; import org.briarproject.briar.api.forum.ForumSharingManager; 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; @@ -116,7 +120,7 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S @ParametersNotNullByDefault public class ConversationActivity extends BriarActivity implements EventListener, ConversationListener, TextInputListener, - TextCache { + TextCache, AttachmentCache { public static final String CONTACT_ID = "briar.CONTACT_ID"; @@ -134,6 +138,7 @@ public class ConversationActivity extends BriarActivity Executor cryptoExecutor; private final Map textCache = new ConcurrentHashMap<>(); + private AttachmentController attachmentController; private ConversationViewModel viewModel; private ConversationVisitor visitor; @@ -191,6 +196,7 @@ public class ConversationActivity extends BriarActivity viewModel = ViewModelProviders.of(this, viewModelFactory) .get(ConversationViewModel.class); viewModel.setContactId(contactId); + attachmentController = viewModel.getAttachmentController(); setContentView(R.layout.activity_conversation); @@ -217,7 +223,7 @@ public class ConversationActivity extends BriarActivity setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId)); setTransitionName(toolbarStatus, getBulbTransitionName(contactId)); - visitor = new ConversationVisitor(this, this, + visitor = new ConversationVisitor(this, this, this, viewModel.getContactDisplayName()); adapter = new ConversationAdapter(this, this); list = findViewById(R.id.conversationView); @@ -340,14 +346,30 @@ public class ConversationActivity extends BriarActivity // If the latest header is a private message, eagerly load // its text so we can set the scroll position correctly ConversationMessageHeader latest = sorted.get(0); - if (latest instanceof PrivateMessageHeader && - ((PrivateMessageHeader) latest).hasText()) { + if (latest instanceof PrivateMessageHeader) { MessageId id = latest.getId(); - String text = textCache.get(id); - if (text == null) { - LOG.info("Eagerly loading text of latest message"); - text = messagingManager.getMessageText(id); - textCache.put(id, text); + PrivateMessageHeader h = (PrivateMessageHeader) latest; + if (h.hasText()) { + String text = textCache.get(id); + if (text == null) { + LOG.info( + "Eagerly loading text of latest message"); + text = messagingManager.getMessageText(id); + textCache.put(id, text); + } + } + if (!h.getAttachmentHeaders().isEmpty()) { + List items = + attachmentController.get(id); + if (items == null) { + LOG.info( + "Eagerly loading image size for latest message"); + items = attachmentController.getAttachmentItems( + attachmentController + .getMessageAttachments( + h.getAttachmentHeaders())); + attachmentController.put(id, items); + } } } } @@ -408,16 +430,40 @@ public class ConversationActivity extends BriarActivity private void displayMessageText(MessageId m, String text) { runOnUiThreadUnlessDestroyed(() -> { textCache.put(m, text); - SparseArray messages = - adapter.getMessageItems(); - for (int i = 0; i < messages.size(); i++) { - ConversationItem item = messages.valueAt(i); - if (item.getId().equals(m)) { - item.setText(text); - adapter.notifyItemChanged(messages.keyAt(i)); - list.scrollToPosition(adapter.getItemCount() - 1); - return; - } + Pair pair = + adapter.getMessageItem(m); + if (pair != null) { + pair.getSecond().setText(text); + adapter.notifyItemChanged(pair.getFirst()); + list.scrollToPosition(adapter.getItemCount() - 1); + } + }); + } + + private void loadMessageAttachments(MessageId messageId, + List headers) { + runOnDbThread(() -> { + try { + displayMessageAttachments(messageId, + attachmentController.getMessageAttachments(headers)); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + private void displayMessageAttachments(MessageId m, + List> attachments) { + runOnUiThreadUnlessDestroyed(() -> { + List items = + attachmentController.getAttachmentItems(attachments); + attachmentController.put(m, items); + Pair pair = + adapter.getMessageItem(m); + if (pair != null) { + pair.getSecond().setAttachments(items); + adapter.notifyItemChanged(pair.getFirst()); + list.scrollToPosition(adapter.getItemCount() - 1); } }); } @@ -782,4 +828,13 @@ public class ConversationActivity extends BriarActivity if (text == null) loadMessageText(m); return text; } + + @Nullable + @Override + public List getAttachmentItems(MessageId m, + List headers) { + List attachments = attachmentController.get(m); + if (attachments == null) loadMessageAttachments(m, headers); + return attachments; + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java index 4b56c234a..5ddc1650a 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationAdapter.java @@ -7,7 +7,9 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.R; import org.briarproject.briar.android.util.BriarAdapter; @@ -98,16 +100,16 @@ class ConversationAdapter return messages; } - SparseArray getMessageItems() { - SparseArray messages = new SparseArray<>(); - + @Nullable + Pair getMessageItem(MessageId messageId) { for (int i = 0; i < items.size(); i++) { ConversationItem item = items.get(i); - if (item instanceof ConversationMessageItem) { - messages.put(i, (ConversationMessageItem) item); + if (item instanceof ConversationMessageItem && + item.getId().equals(messageId)) { + return new Pair<>(i, (ConversationMessageItem) item); } } - return messages; + return null; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java index 9718d7e47..7d0e25d94 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationItemViewHolder.java @@ -3,9 +3,9 @@ package org.briarproject.briar.android.conversation; import android.support.annotation.CallSuper; import android.support.annotation.Nullable; import android.support.annotation.UiThread; +import android.support.constraint.ConstraintLayout; import android.support.v7.widget.RecyclerView.ViewHolder; import android.view.View; -import android.view.ViewGroup; import android.widget.TextView; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; @@ -18,11 +18,11 @@ import static org.briarproject.briar.android.util.UiUtils.formatDate; @NotNullByDefault abstract class ConversationItemViewHolder extends ViewHolder { - protected final ViewGroup layout; + protected final ConstraintLayout layout; @Nullable private final OutItemViewHolder outViewHolder; private final TextView text; - private final TextView time; + protected final TextView time; ConversationItemViewHolder(View v, boolean isIncoming) { super(v); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java index 6aa92f51d..106b0aed2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java @@ -1,9 +1,9 @@ package org.briarproject.briar.android.conversation; import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader; import java.util.List; @@ -14,15 +14,22 @@ import javax.annotation.concurrent.NotThreadSafe; @NotNullByDefault class ConversationMessageItem extends ConversationItem { - private final List attachments; + @Nullable + private List attachments; - ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h) { + ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h, + @Nullable List attachments) { super(layoutRes, h); - this.attachments = h.getAttachmentHeaders(); + this.attachments = attachments; } - List getAttachments() { + @Nullable + List getAttachments() { return attachments; } + void setAttachments(List attachments) { + this.attachments = attachments; + } + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index a4a4360c8..3ced99cd9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -1,18 +1,165 @@ package org.briarproject.briar.android.conversation; +import android.graphics.Bitmap; +import android.support.annotation.DrawableRes; import android.support.annotation.UiThread; +import android.support.constraint.ConstraintSet; +import android.support.v4.content.ContextCompat; import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.bumptech.glide.load.MultiTransformation; +import com.bumptech.glide.load.Transformation; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.conversation.glide.GlideApp; + +import jp.wasabeef.glide.transformations.RoundedCornersTransformation; + +import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; +import static jp.wasabeef.glide.transformations.RoundedCornersTransformation.CornerType.BOTTOM; +import static jp.wasabeef.glide.transformations.RoundedCornersTransformation.CornerType.TOP_LEFT; +import static jp.wasabeef.glide.transformations.RoundedCornersTransformation.CornerType.TOP_RIGHT; @UiThread @NotNullByDefault class ConversationMessageViewHolder extends ConversationItemViewHolder { - // image support will be added here (#1242) + @DrawableRes + private static final int errorRes = R.drawable.ic_image_broken; + + private final ImageView imageView; + private final ViewGroup statusLayout; + private final int timeColor, timeColorBubble; + private final int radiusBig, radiusSmall; + private final ConstraintSet textConstraints = new ConstraintSet(); + private final ConstraintSet imageConstraints = new ConstraintSet(); + private final ConstraintSet imageTextConstraints = new ConstraintSet(); ConversationMessageViewHolder(View v, boolean isIncoming) { super(v, isIncoming); + imageView = v.findViewById(R.id.imageView); + statusLayout = v.findViewById(R.id.statusLayout); + radiusBig = v.getContext().getResources() + .getDimensionPixelSize(R.dimen.message_bubble_radius_big); + radiusSmall = v.getContext().getResources() + .getDimensionPixelSize(R.dimen.message_bubble_radius_small); + + // remember original status text color + timeColor = time.getCurrentTextColor(); + timeColorBubble = + ContextCompat.getColor(v.getContext(), R.color.briar_white); + + // clone constraint sets from layout files + textConstraints + .clone(v.getContext(), R.layout.list_item_conversation_msg_in); + imageConstraints.clone(v.getContext(), + R.layout.list_item_conversation_msg_image); + imageTextConstraints.clone(v.getContext(), + R.layout.list_item_conversation_msg_image_text); + + // in/out are different layouts, so we need to do this only once + textConstraints + .setHorizontalBias(R.id.statusLayout, isIncoming() ? 1 : 0); + imageConstraints + .setHorizontalBias(R.id.statusLayout, isIncoming() ? 1 : 0); + imageTextConstraints + .setHorizontalBias(R.id.statusLayout, isIncoming() ? 1 : 0); + } + + @Override + void bind(ConversationItem conversationItem, + ConversationListener listener) { + super.bind(conversationItem, listener); + ConversationMessageItem item = + (ConversationMessageItem) conversationItem; + if (item.getAttachments() == null || item.getAttachments().isEmpty()) { + bindTextItem(); + } else { + bindImageItem(item); + } + } + + private void bindTextItem() { + clearImage(); + statusLayout.setBackgroundResource(0); + // also reset padding (the background drawable defines some) + statusLayout.setPadding(0, 0, 0, 0); + time.setTextColor(timeColor); + textConstraints.applyTo(layout); + } + + private void bindImageItem(ConversationMessageItem item) { + // TODO show more than just the first image + AttachmentItem attachment = item.getAttachments().get(0); + + ConstraintSet constraintSet; + if (item.getText() == null) { + statusLayout + .setBackgroundResource(R.drawable.msg_status_bubble); + time.setTextColor(timeColorBubble); + constraintSet = imageConstraints; + } else { + statusLayout.setBackgroundResource(0); + // also reset padding (the background drawable defines some) + statusLayout.setPadding(0, 0, 0, 0); + time.setTextColor(timeColor); + constraintSet = imageTextConstraints; + } + + // apply image size constraints, so glides picks them up for scaling + int width = attachment.getThumbnailWidth(); + int height = attachment.getThumbnailHeight(); + constraintSet.constrainWidth(R.id.imageView, width); + constraintSet.constrainHeight(R.id.imageView, height); + constraintSet.applyTo(layout); + + if (attachment.hasError()) { + clearImage(); + imageView.setImageResource(errorRes); + } else { + loadImage(item, attachment); + } + } + + private void clearImage() { + GlideApp.with(imageView) + .clear(imageView); + } + + private void loadImage(ConversationMessageItem item, + AttachmentItem attachment) { + // these transformations can be optimized by writing our own + Transformation transformation; + if (item.getText() == null) { + transformation = new MultiTransformation<>(new CenterCrop(), + new RoundedCornersTransformation(radiusSmall, 0, + isIncoming() ? TOP_LEFT : TOP_RIGHT), + new RoundedCornersTransformation(radiusBig, 0, + isIncoming() ? TOP_RIGHT : TOP_LEFT), + new RoundedCornersTransformation(radiusBig, 0, BOTTOM) + ); + } else { + transformation = new MultiTransformation<>(new CenterCrop(), + new RoundedCornersTransformation(radiusSmall, 0, + isIncoming() ? TOP_LEFT : TOP_RIGHT), + new RoundedCornersTransformation(radiusBig, 0, + isIncoming() ? TOP_RIGHT : TOP_LEFT) + ); + } + + GlideApp.with(imageView) + .load(attachment) + .diskCacheStrategy(NONE) + .error(errorRes) + .transform(transformation) + .transition(withCrossFade()) + .into(imageView) + .waitForLayout(); } } 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 684fadf45..3a8e6e364 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 @@ -16,6 +16,7 @@ import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.android.util.UiUtils; +import org.briarproject.briar.api.messaging.MessagingManager; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -38,6 +39,7 @@ public class ConversationViewModel extends AndroidViewModel { @DatabaseExecutor private final Executor dbExecutor; private final ContactManager contactManager; + private final AttachmentController attachmentController; @Nullable private ContactId contactId = null; @@ -52,10 +54,12 @@ public class ConversationViewModel extends AndroidViewModel { @Inject ConversationViewModel(Application application, @DatabaseExecutor Executor dbExecutor, - ContactManager contactManager) { + ContactManager contactManager, MessagingManager messagingManager) { super(application); this.dbExecutor = dbExecutor; this.contactManager = contactManager; + this.attachmentController = new AttachmentController(messagingManager, + application.getResources()); contactDeleted.setValue(false); } @@ -96,6 +100,10 @@ public class ConversationViewModel extends AndroidViewModel { }); } + AttachmentController getAttachmentController() { + return attachmentController; + } + LiveData getContact() { return contact; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index a7e680c9b..b8b57488e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -14,12 +14,16 @@ import org.briarproject.briar.api.forum.ForumInvitationRequest; import org.briarproject.briar.api.forum.ForumInvitationResponse; import org.briarproject.briar.api.introduction.IntroductionRequest; import org.briarproject.briar.api.introduction.IntroductionResponse; +import org.briarproject.briar.api.messaging.AttachmentHeader; import org.briarproject.briar.api.messaging.PrivateMessageHeader; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; +import java.util.List; + import javax.annotation.Nullable; +import static java.util.Collections.emptyList; import static org.briarproject.briar.android.conversation.ConversationRequestItem.RequestType.BLOG; import static org.briarproject.briar.android.conversation.ConversationRequestItem.RequestType.FORUM; import static org.briarproject.briar.android.conversation.ConversationRequestItem.RequestType.GROUP; @@ -33,24 +37,33 @@ class ConversationVisitor implements private final Context ctx; private final TextCache textCache; + private final AttachmentCache attachmentCache; private final LiveData contactName; ConversationVisitor(Context ctx, TextCache textCache, - LiveData contactName) { + AttachmentCache attachmentCache, LiveData contactName) { this.ctx = ctx; this.textCache = textCache; + this.attachmentCache = attachmentCache; this.contactName = contactName; } @Override public ConversationItem visitPrivateMessageHeader(PrivateMessageHeader h) { ConversationItem item; + List attachments; + if (h.getAttachmentHeaders().isEmpty()) { + attachments = emptyList(); + } else { + attachments = attachmentCache + .getAttachmentItems(h.getId(), h.getAttachmentHeaders()); + } if (h.isLocal()) { item = new ConversationMessageItem( - R.layout.list_item_conversation_msg_out, h); + R.layout.list_item_conversation_msg_out, h, attachments); } else { item = new ConversationMessageItem( - R.layout.list_item_conversation_msg_in, h); + R.layout.list_item_conversation_msg_in, h, attachments); } if (h.hasText()) { String text = textCache.getText(h.getId()); @@ -279,4 +292,10 @@ class ConversationVisitor implements @Nullable String getText(MessageId m); } + + interface AttachmentCache { + @Nullable + List getAttachmentItems(MessageId m, + List headers); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java new file mode 100644 index 000000000..5ef97d6ed --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java @@ -0,0 +1,96 @@ +package org.briarproject.briar.android.conversation.glide; + +import android.support.annotation.Nullable; + +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.data.DataFetcher; + +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.api.messaging.MessagingManager; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static com.bumptech.glide.load.DataSource.LOCAL; +import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@NotNullByDefault +class BriarDataFetcher implements DataFetcher { + + private final static Logger LOG = + getLogger(BriarDataFetcher.class.getName()); + + private final MessagingManager messagingManager; + @DatabaseExecutor + private final Executor dbExecutor; + + @Nullable + private AttachmentItem attachment; + @Nullable + private volatile InputStream inputStream; + + @Inject + public BriarDataFetcher(MessagingManager messagingManager, + @DatabaseExecutor Executor dbExecutor) { + this.messagingManager = messagingManager; + this.dbExecutor = dbExecutor; + } + + @Override + public void loadData(Priority priority, + DataCallback callback) { + MessageId id = requireNonNull(attachment).getMessageId(); + dbExecutor.execute(() -> { + try { + inputStream = messagingManager.getAttachment(id).getStream(); + callback.onDataReady(inputStream); + } catch (DbException e) { + callback.onLoadFailed(e); + } + }); + } + + @Override + public void cleanup() { + final InputStream stream = inputStream; + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + logException(LOG, WARNING, e); + } + } + } + + @Override + public void cancel() { + // does it make sense to cancel a database load? + } + + @Override + public Class getDataClass() { + return InputStream.class; + } + + @Override + public DataSource getDataSource() { + return LOCAL; + } + + public void setAttachment(AttachmentItem attachment) { + this.attachment = attachment; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java new file mode 100644 index 000000000..f0776bae2 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java @@ -0,0 +1,43 @@ +package org.briarproject.briar.android.conversation.glide; + +import android.content.Context; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +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 java.io.InputStream; + +import static android.util.Log.DEBUG; +import static android.util.Log.WARN; +import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; + +@GlideModule +@NotNullByDefault +public final class BriarGlideModule extends AppGlideModule { + + @Override + public void registerComponents(Context context, Glide glide, + Registry registry) { + BriarModelLoaderFactory factory = + new BriarModelLoaderFactory((BriarApplication) context); + registry.prepend(AttachmentItem.class, InputStream.class, factory); + } + + @Override + public void applyOptions(Context context, GlideBuilder builder) { + builder.setLogLevel(IS_DEBUG_BUILD ? DEBUG : WARN); + } + + @Override + public boolean isManifestParsingEnabled() { + return false; + } + +} \ No newline at end of file diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java new file mode 100644 index 000000000..397910de9 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java @@ -0,0 +1,44 @@ +package org.briarproject.briar.android.conversation.glide; + + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.model.ModelLoader; +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 java.io.InputStream; + +import javax.inject.Inject; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public final class BriarModelLoader + implements ModelLoader { + + private final BriarApplication app; + + @Inject + BriarDataFetcher dataFetcher; + + public BriarModelLoader(BriarApplication app) { + this.app = app; + } + + @Override + public LoadData buildLoadData(AttachmentItem model, int width, + int height, Options options) { + app.getApplicationComponent().inject(this); + ObjectKey key = new ObjectKey(model.getMessageId()); + dataFetcher.setAttachment(model); + return new LoadData<>(key, dataFetcher); + } + + @Override + public boolean handles(AttachmentItem model) { + return true; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java new file mode 100644 index 000000000..9d2a6d5d1 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoaderFactory.java @@ -0,0 +1,34 @@ +package org.briarproject.briar.android.conversation.glide; + +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +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 java.io.InputStream; + +@NotNullByDefault +class BriarModelLoaderFactory + implements ModelLoaderFactory { + + private final BriarApplication app; + + public BriarModelLoaderFactory(BriarApplication app) { + this.app = app; + } + + @Override + public ModelLoader build( + MultiModelLoaderFactory multiFactory) { + return new BriarModelLoader(app); + } + + @Override + public void teardown() { + // noop + } + +} diff --git a/briar-android/src/main/res/drawable/ic_image_broken.xml b/briar-android/src/main/res/drawable/ic_image_broken.xml new file mode 100644 index 000000000..318d05b36 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_image_broken.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/src/main/res/drawable/msg_in.xml b/briar-android/src/main/res/drawable/msg_in.xml index b0a822b8c..d4e0d4da2 100644 --- a/briar-android/src/main/res/drawable/msg_in.xml +++ b/briar-android/src/main/res/drawable/msg_in.xml @@ -8,10 +8,10 @@ android:topLeftRadius="@dimen/message_bubble_radius_top_inner" android:topRightRadius="@dimen/message_bubble_radius_top_outer"/> + android:bottom="@dimen/message_bubble_border" + android:left="@dimen/message_bubble_border" + android:right="@dimen/message_bubble_border" + android:top="@dimen/message_bubble_border"/> + android:bottom="@dimen/message_bubble_border" + android:left="@dimen/message_bubble_border" + android:right="@dimen/message_bubble_border" + android:top="@dimen/message_bubble_border"/> + + + + + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml new file mode 100644 index 000000000..c36a3080c --- /dev/null +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml new file mode 100644 index 000000000..5507d041d --- /dev/null +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_image_text.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml index 2048bcacc..4e3950a87 100644 --- a/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_in.xml @@ -15,29 +15,55 @@ android:background="@drawable/msg_in" android:elevation="@dimen/message_bubble_elevation"> + + + app:layout_constraintTop_toBottomOf="@+id/imageView" + tools:text="The text of a message which can sometimes be a bit longer as well"/> - + app:layout_constraintTop_toBottomOf="@+id/text"> + + + + diff --git a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml index 0b346476c..d0dc1736e 100644 --- a/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml +++ b/briar-android/src/main/res/layout/list_item_conversation_msg_out.xml @@ -21,39 +21,68 @@ android:background="@drawable/msg_out" android:elevation="@dimen/message_bubble_elevation"> + + - + tools:ignore="UseCompoundDrawables"> - + + + + + diff --git a/briar-android/src/main/res/values/color.xml b/briar-android/src/main/res/values/color.xml index 3d57fec4d..870fdae52 100644 --- a/briar-android/src/main/res/values/color.xml +++ b/briar-android/src/main/res/values/color.xml @@ -40,6 +40,7 @@ #cbcbcb #333333 @color/msg_stroke_light + #66000000 @color/briar_blue_light diff --git a/briar-android/src/main/res/values/dimens.xml b/briar-android/src/main/res/values/dimens.xml index 1b7f86cd8..81f7d4377 100644 --- a/briar-android/src/main/res/values/dimens.xml +++ b/briar-android/src/main/res/values/dimens.xml @@ -43,9 +43,18 @@ @dimen/message_bubble_radius_small @dimen/message_bubble_radius_big 6dp + 210dp + 150dp + 240dp + 100dp + 320dp + 2dp 12dp + 10dp 6dp + 4dp 4dp + 2dp 4dp 2dp 8dp diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle index 2d092ffa5..a5213326c 100644 --- a/briar-android/witness.gradle +++ b/briar-android/witness.gradle @@ -36,6 +36,7 @@ dependencyVerification { 'com.android.support:design:28.0.0:design-28.0.0.aar:7874ad1904eedc74aa41cffffb7f759d8990056f3bbbc9264911651c67c42f5f', 'com.android.support:documentfile:28.0.0:documentfile-28.0.0.aar:47cdcd3e9302b7b064923f05487a5c03babbd9bbda4726b71e97791fab5d4779', 'com.android.support:drawerlayout:28.0.0:drawerlayout-28.0.0.aar:8f6809afae4793550c37461c9810e954ae6a23dbb4d23e5333bf18148df1150a', + 'com.android.support:exifinterface:28.0.0:exifinterface-28.0.0.aar:bbf44e519edd6333a24a3285aa21fd00181b920b81ca8aa89a8899f03ab4d6b0', 'com.android.support:interpolator:28.0.0:interpolator-28.0.0.aar:7bc7ee86a0db39a4b51956f3e89842d2bd962118d57d779eb6ed6b34ba0677ea', 'com.android.support:loader:28.0.0:loader-28.0.0.aar:920b85efd72dc33e915b0f88a883fe73b88483c6df8751a741e17611f2460341', 'com.android.support:localbroadcastmanager:28.0.0:localbroadcastmanager-28.0.0.aar:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806', @@ -84,6 +85,10 @@ dependencyVerification { 'com.android.tools:repository:26.2.1:repository-26.2.1.jar:fa74dae09103faef703df38550ad8fa244c5b6d1bf90d6198be932292b3d9cc1', 'com.android.tools:sdk-common:26.2.1:sdk-common-26.2.1.jar:759d4b292ca69a35cf961fca377b54158fc6c88108978006999442e80a011cf4', 'com.android.tools:sdklib:26.2.1:sdklib-26.2.1.jar:248df7ad5eac4aeb6f96c394c76760de4b7b89ac056e54d0c21a739368b91b45', + 'com.github.bumptech.glide:annotations:4.8.0:annotations-4.8.0.jar:4ea82e59874673105165820336c6ac268fc46149892486aad8e7a131a4449446', + 'com.github.bumptech.glide:compiler:4.8.0:compiler-4.8.0.jar:1fa93dd0cf7ef0b8b98a59a67a1ee84915416c2d677d83a771ea3e32ad15e6bf', + 'com.github.bumptech.glide:gifdecoder:4.8.0:gifdecoder-4.8.0.aar:b00c5454a023a9488ea49603930d9c25e09192e5ceaadf64977aa52946b3c1b4', + 'com.github.bumptech.glide:glide:4.8.0:glide-4.8.0.aar:5ddf08b12cc43332e812988f16c2c39e7fce49d1c4d94b7948dcde7f00bf49d6', 'com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:2.0:accessibility-test-framework-2.0.jar:cdf16ef8f5b8023d003ce3cc1b0d51bda737762e2dab2fedf43d1c4292353f7f', 'com.google.android.apps.common.testing.accessibility.framework:accessibility-test-framework:2.1:accessibility-test-framework-2.1.jar:7b0aa6ed7553597ce0610684a9f7eca8021eee218f2e2f427c04a7fbf5f920bd', 'com.google.code.findbugs:jsr305:1.3.9:jsr305-1.3.9.jar:905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed', @@ -125,6 +130,7 @@ dependencyVerification { 'javax.annotation:jsr250-api:1.0:jsr250-api-1.0.jar:a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f', 'javax.inject:javax.inject:1:javax.inject-1.jar:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'javax.xml.bind:jaxb-api:2.2.12-b140109.1041:jaxb-api-2.2.12-b140109.1041.jar:b5e60cd8b7b5ff01ce4a74c5dd008f4fbd14ced3495d0b47b85cfedc182211f2', + 'jp.wasabeef:glide-transformations:3.3.0:glide-transformations-3.3.0.aar:340c482364b84be768e7cb96975aa78b448b9f067913c8a23ae2339e0e908adf', 'junit:junit:4.12:junit-4.12.jar:59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a', 'nekohtml:nekohtml:1.9.6.2:nekohtml-1.9.6.2.jar:fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09', 'nekohtml:xercesMinimal:1.9.6.2:xercesMinimal-1.9.6.2.jar:95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438', From d6b52cf4ecc96677e2464c7cb1d65e4a73676b71 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 15 Nov 2018 18:54:05 -0200 Subject: [PATCH 2/7] [android] Use our own BitmapTransformation for rounded image corners --- briar-android/build.gradle | 3 - .../ConversationMessageViewHolder.java | 34 ++---- .../glide/BriarImageTransformation.java | 16 +++ .../glide/ImageCornerTransformation.java | 111 ++++++++++++++++++ briar-android/witness.gradle | 1 - 5 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 7a16fe6cc..87748420d 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -123,9 +123,6 @@ dependencies { exclude group: 'com.android.support' exclude module: 'disklrucache' // when there's no disk cache, we can't accidentally use it } - implementation('jp.wasabeef:glide-transformations:3.3.0') { - exclude module: 'disklrucache' // this gets pulled in here otherwise - } annotationProcessor 'com.google.dagger:dagger-compiler:2.19' annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion" diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index 3ced99cd9..50fe30e40 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -9,21 +9,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.Transformation; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; import org.briarproject.briar.android.conversation.glide.GlideApp; -import jp.wasabeef.glide.transformations.RoundedCornersTransformation; - import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; -import static jp.wasabeef.glide.transformations.RoundedCornersTransformation.CornerType.BOTTOM; -import static jp.wasabeef.glide.transformations.RoundedCornersTransformation.CornerType.TOP_LEFT; -import static jp.wasabeef.glide.transformations.RoundedCornersTransformation.CornerType.TOP_RIGHT; +import static java.util.Objects.requireNonNull; @UiThread @NotNullByDefault @@ -95,7 +90,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private void bindImageItem(ConversationMessageItem item) { // TODO show more than just the first image - AttachmentItem attachment = item.getAttachments().get(0); + AttachmentItem attachment = + requireNonNull(item.getAttachments()).get(0); ConstraintSet constraintSet; if (item.getText() == null) { @@ -133,24 +129,10 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private void loadImage(ConversationMessageItem item, AttachmentItem attachment) { - // these transformations can be optimized by writing our own - Transformation transformation; - if (item.getText() == null) { - transformation = new MultiTransformation<>(new CenterCrop(), - new RoundedCornersTransformation(radiusSmall, 0, - isIncoming() ? TOP_LEFT : TOP_RIGHT), - new RoundedCornersTransformation(radiusBig, 0, - isIncoming() ? TOP_RIGHT : TOP_LEFT), - new RoundedCornersTransformation(radiusBig, 0, BOTTOM) - ); - } else { - transformation = new MultiTransformation<>(new CenterCrop(), - new RoundedCornersTransformation(radiusSmall, 0, - isIncoming() ? TOP_LEFT : TOP_RIGHT), - new RoundedCornersTransformation(radiusBig, 0, - isIncoming() ? TOP_RIGHT : TOP_LEFT) - ); - } + boolean leftCornerSmall = isIncoming(); + boolean bottomRound = item.getText() == null; + Transformation transformation = new BriarImageTransformation( + radiusSmall, radiusBig, leftCornerSmall, bottomRound); GlideApp.with(imageView) .load(attachment) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java new file mode 100644 index 000000000..5488efa40 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarImageTransformation.java @@ -0,0 +1,16 @@ +package org.briarproject.briar.android.conversation.glide; + +import android.graphics.Bitmap; + +import com.bumptech.glide.load.MultiTransformation; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; + +public class BriarImageTransformation extends MultiTransformation { + + public BriarImageTransformation(int smallRadius, int radius, + boolean leftCornerSmall, boolean bottomRound) { + super(new CenterCrop(), new ImageCornerTransformation( + smallRadius, radius, leftCornerSmall, bottomRound)); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java new file mode 100644 index 000000000..fb418d6d7 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java @@ -0,0 +1,111 @@ +package org.briarproject.briar.android.conversation.glide; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.support.annotation.NonNull; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.security.MessageDigest; + +import javax.annotation.concurrent.Immutable; + +import static android.graphics.Bitmap.Config.ARGB_8888; +import static android.graphics.Shader.TileMode.CLAMP; + +@Immutable +@NotNullByDefault +class ImageCornerTransformation extends BitmapTransformation { + + private static final String ID = ImageCornerTransformation.class.getName(); + + private final int smallRadius, radius; + private final boolean leftCornerSmall, bottomRound; + + ImageCornerTransformation(int smallRadius, int radius, + boolean leftCornerSmall, boolean bottomRound) { + this.smallRadius = smallRadius; + this.radius = radius; + this.leftCornerSmall = leftCornerSmall; + this.bottomRound = bottomRound; + } + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap toTransform, + int outWidth, int outHeight) { + int width = toTransform.getWidth(); + int height = toTransform.getHeight(); + + Bitmap bitmap = pool.get(width, height, ARGB_8888); + bitmap.setHasAlpha(true); + + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setShader(new BitmapShader(toTransform, CLAMP, CLAMP)); + drawRect(canvas, paint, width, height); + return bitmap; + } + + private void drawRect(Canvas canvas, Paint paint, float width, + float height) { + drawSmallCorner(canvas, paint, width); + drawBigCorners(canvas, paint, width, height); + } + + private void drawSmallCorner(Canvas canvas, Paint paint, float width) { + float left = leftCornerSmall ? 0 : width - radius; + float right = leftCornerSmall ? radius : width; + canvas.drawRoundRect(new RectF(left, 0, right, radius), + smallRadius, smallRadius, paint); + } + + private void drawBigCorners(Canvas canvas, Paint paint, float width, + float height) { + float top = bottomRound ? 0 : radius; + RectF rect = new RectF(0, top, width, height); + if (bottomRound) { + canvas.drawRoundRect(rect, radius, radius, paint); + } else { + canvas.drawRect(rect, paint); + canvas.drawRoundRect(new RectF(0, 0, width, radius * 2), + radius, radius, paint); + } + } + + @Override + public String toString() { + return "ImageCornerTransformation(smallRadius=" + smallRadius + + ", radius=" + radius + ", leftCornerSmall=" + leftCornerSmall + + ", bottomRound=" + bottomRound + ")"; + } + + @Override + public boolean equals(Object o) { + return o instanceof ImageCornerTransformation && + ((ImageCornerTransformation) o).smallRadius == smallRadius && + ((ImageCornerTransformation) o).radius == radius && + ((ImageCornerTransformation) o).leftCornerSmall == + leftCornerSmall && + ((ImageCornerTransformation) o).bottomRound == bottomRound; + } + + @Override + public int hashCode() { + return ID.hashCode() + smallRadius * 100 + radius * 10 + + (leftCornerSmall ? 9 : 8) + (bottomRound ? 7 : 6); + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update((ID + smallRadius + radius + leftCornerSmall + + bottomRound).getBytes(CHARSET)); + } + +} diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle index a5213326c..29a9c2f01 100644 --- a/briar-android/witness.gradle +++ b/briar-android/witness.gradle @@ -130,7 +130,6 @@ dependencyVerification { 'javax.annotation:jsr250-api:1.0:jsr250-api-1.0.jar:a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f', 'javax.inject:javax.inject:1:javax.inject-1.jar:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', 'javax.xml.bind:jaxb-api:2.2.12-b140109.1041:jaxb-api-2.2.12-b140109.1041.jar:b5e60cd8b7b5ff01ce4a74c5dd008f4fbd14ced3495d0b47b85cfedc182211f2', - 'jp.wasabeef:glide-transformations:3.3.0:glide-transformations-3.3.0.aar:340c482364b84be768e7cb96975aa78b448b9f067913c8a23ae2339e0e908adf', 'junit:junit:4.12:junit-4.12.jar:59721f0805e223d84b90677887d9ff567dc534d7c502ca903c0c2b17f05c116a', 'nekohtml:nekohtml:1.9.6.2:nekohtml-1.9.6.2.jar:fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09', 'nekohtml:xercesMinimal:1.9.6.2:xercesMinimal-1.9.6.2.jar:95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438', From de8e95692a42fff984fbae479b85ef5813c3b796 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 19 Nov 2018 19:47:43 -0200 Subject: [PATCH 3/7] [android] support RTL languages when rounding thumbnail corners --- .../ConversationMessageViewHolder.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index 50fe30e40..aabab258e 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -1,5 +1,6 @@ package org.briarproject.briar.android.conversation; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.support.annotation.DrawableRes; import android.support.annotation.UiThread; @@ -16,6 +17,8 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.conversation.glide.BriarImageTransformation; import org.briarproject.briar.android.conversation.glide.GlideApp; +import static android.os.Build.VERSION.SDK_INT; +import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; import static java.util.Objects.requireNonNull; @@ -31,6 +34,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private final ViewGroup statusLayout; private final int timeColor, timeColorBubble; private final int radiusBig, radiusSmall; + private final boolean isRtl; private final ConstraintSet textConstraints = new ConstraintSet(); private final ConstraintSet imageConstraints = new ConstraintSet(); private final ConstraintSet imageTextConstraints = new ConstraintSet(); @@ -49,6 +53,13 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { timeColorBubble = ContextCompat.getColor(v.getContext(), R.color.briar_white); + // find out if we are showing a RTL language, Use the configuration, + // because getting the layout direction of views is not reliable + Configuration config = + imageView.getContext().getResources().getConfiguration(); + isRtl = SDK_INT >= 17 && + config.getLayoutDirection() == LAYOUT_DIRECTION_RTL; + // clone constraint sets from layout files textConstraints .clone(v.getContext(), R.layout.list_item_conversation_msg_in); @@ -129,7 +140,8 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private void loadImage(ConversationMessageItem item, AttachmentItem attachment) { - boolean leftCornerSmall = isIncoming(); + boolean leftCornerSmall = + (isIncoming() && !isRtl) || (!isIncoming() && isRtl); boolean bottomRound = item.getText() == null; Transformation transformation = new BriarImageTransformation( radiusSmall, radiusBig, leftCornerSmall, bottomRound); From 10e9fb308d971c95ddd47508c47992f392d8280b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 19 Nov 2018 20:32:43 -0200 Subject: [PATCH 4/7] [android] Display Image Attachements: Address first round of review comments --- .../conversation/AttachmentController.java | 16 +++++++++++----- .../conversation/ConversationActivity.java | 18 +++++++++++------- .../conversation/ConversationMessageItem.java | 5 +---- .../ConversationMessageViewHolder.java | 6 ++---- .../conversation/ConversationVisitor.java | 1 - .../conversation/glide/BriarGlideModule.java | 5 +++-- 6 files changed, 28 insertions(+), 23 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 7fa5f3aba..7c4339c3e 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 @@ -119,9 +119,14 @@ class AttachmentController { is.reset(); size = getSizeFromBitmap(is); } - is.close(); } catch (IOException e) { logException(LOG, WARNING, e); + } finally { + try { + is.close(); + } catch (IOException e) { + logException(LOG, WARNING, e); + } } // calculate thumbnail size @@ -161,7 +166,7 @@ class AttachmentController { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(is, null, options); - if (options.outWidth == -1 || options.outHeight == -1) + if (options.outWidth < 1 || options.outHeight < 1) return new Size(); return new Size(options.outWidth, options.outHeight); } @@ -178,9 +183,10 @@ class AttachmentController { } private static class Size { - private int width; - private int height; - private boolean error; + + private final int width; + private final int height; + private final boolean error; private Size(int width, int height) { this.width = width; 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 5050bc3ec..80d21d65b 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 @@ -444,8 +444,12 @@ public class ConversationActivity extends BriarActivity List headers) { runOnDbThread(() -> { try { - displayMessageAttachments(messageId, - attachmentController.getMessageAttachments(headers)); + List> attachments = + attachmentController.getMessageAttachments(headers); + // TODO move getting the items off to the IoExecutor + List items = + attachmentController.getAttachmentItems(attachments); + displayMessageAttachments(messageId, items); } catch (DbException e) { logException(LOG, WARNING, e); } @@ -453,10 +457,8 @@ public class ConversationActivity extends BriarActivity } private void displayMessageAttachments(MessageId m, - List> attachments) { + List items) { runOnUiThreadUnlessDestroyed(() -> { - List items = - attachmentController.getAttachmentItems(attachments); attachmentController.put(m, items); Pair pair = adapter.getMessageItem(m); @@ -829,12 +831,14 @@ public class ConversationActivity extends BriarActivity return text; } - @Nullable @Override public List getAttachmentItems(MessageId m, List headers) { List attachments = attachmentController.get(m); - if (attachments == null) loadMessageAttachments(m, headers); + if (attachments == null) { + loadMessageAttachments(m, headers); + return emptyList(); + } return attachments; } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java index 106b0aed2..1732e3ab8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageItem.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.conversation; import android.support.annotation.LayoutRes; -import android.support.annotation.Nullable; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.api.messaging.PrivateMessageHeader; @@ -14,16 +13,14 @@ import javax.annotation.concurrent.NotThreadSafe; @NotNullByDefault class ConversationMessageItem extends ConversationItem { - @Nullable private List attachments; ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h, - @Nullable List attachments) { + List attachments) { super(layoutRes, h); this.attachments = attachments; } - @Nullable List getAttachments() { return attachments; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java index aabab258e..74f269b19 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationMessageViewHolder.java @@ -21,7 +21,6 @@ import static android.os.Build.VERSION.SDK_INT; import static android.support.v4.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE; import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; -import static java.util.Objects.requireNonNull; @UiThread @NotNullByDefault @@ -83,7 +82,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { super.bind(conversationItem, listener); ConversationMessageItem item = (ConversationMessageItem) conversationItem; - if (item.getAttachments() == null || item.getAttachments().isEmpty()) { + if (item.getAttachments().isEmpty()) { bindTextItem(); } else { bindImageItem(item); @@ -101,8 +100,7 @@ class ConversationMessageViewHolder extends ConversationItemViewHolder { private void bindImageItem(ConversationMessageItem item) { // TODO show more than just the first image - AttachmentItem attachment = - requireNonNull(item.getAttachments()).get(0); + AttachmentItem attachment = item.getAttachments().get(0); ConstraintSet constraintSet; if (item.getText() == null) { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index b8b57488e..3d6a85db9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -294,7 +294,6 @@ class ConversationVisitor implements } interface AttachmentCache { - @Nullable List getAttachmentItems(MessageId m, List headers); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java index f0776bae2..a1a66ab33 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarGlideModule.java @@ -25,8 +25,9 @@ public final class BriarGlideModule extends AppGlideModule { @Override public void registerComponents(Context context, Glide glide, Registry registry) { - BriarModelLoaderFactory factory = - new BriarModelLoaderFactory((BriarApplication) context); + BriarApplication app = + (BriarApplication) context.getApplicationContext(); + BriarModelLoaderFactory factory = new BriarModelLoaderFactory(app); registry.prepend(AttachmentItem.class, InputStream.class, factory); } From dd5ad86db80f7649bc8cb87cd01fc2be2622e157 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 20 Nov 2018 11:39:29 -0200 Subject: [PATCH 5/7] [android] Use DataFetcherFactory to create data fetchers and allow cancelling loads --- .../conversation/ConversationModule.java | 19 ++++++++++++ .../conversation/glide/BriarDataFetcher.java | 14 ++++----- .../glide/BriarDataFetcherFactory.java | 30 +++++++++++++++++++ .../conversation/glide/BriarModelLoader.java | 10 +++---- 4 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationModule.java create mode 100644 briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationModule.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationModule.java new file mode 100644 index 000000000..287d7d00b --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationModule.java @@ -0,0 +1,19 @@ +package org.briarproject.briar.android.conversation; + +import org.briarproject.briar.android.activity.ActivityScope; +import org.briarproject.briar.android.conversation.glide.BriarDataFetcherFactory; + +import dagger.Module; +import dagger.Provides; + +@Module +public class ConversationModule { + + @ActivityScope + @Provides + BriarDataFetcherFactory provideBriarDataFetcherFactory( + BriarDataFetcherFactory dataFetcherFactory) { + return dataFetcherFactory; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java index 5ef97d6ed..252e8df57 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java @@ -35,17 +35,18 @@ class BriarDataFetcher implements DataFetcher { private final MessagingManager messagingManager; @DatabaseExecutor private final Executor dbExecutor; + private final AttachmentItem attachment; - @Nullable - private AttachmentItem attachment; @Nullable private volatile InputStream inputStream; + private volatile boolean cancel = false; @Inject public BriarDataFetcher(MessagingManager messagingManager, - @DatabaseExecutor Executor dbExecutor) { + @DatabaseExecutor Executor dbExecutor, AttachmentItem attachment) { this.messagingManager = messagingManager; this.dbExecutor = dbExecutor; + this.attachment = attachment; } @Override @@ -53,6 +54,7 @@ class BriarDataFetcher implements DataFetcher { DataCallback callback) { MessageId id = requireNonNull(attachment).getMessageId(); dbExecutor.execute(() -> { + if (cancel) return; try { inputStream = messagingManager.getAttachment(id).getStream(); callback.onDataReady(inputStream); @@ -76,7 +78,7 @@ class BriarDataFetcher implements DataFetcher { @Override public void cancel() { - // does it make sense to cancel a database load? + cancel = true; } @Override @@ -89,8 +91,4 @@ class BriarDataFetcher implements DataFetcher { return LOCAL; } - public void setAttachment(AttachmentItem attachment) { - this.attachment = attachment; - } - } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java new file mode 100644 index 000000000..87f55a170 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcherFactory.java @@ -0,0 +1,30 @@ +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.api.messaging.MessagingManager; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +@NotNullByDefault +public class BriarDataFetcherFactory { + + private final MessagingManager messagingManager; + @DatabaseExecutor + private final Executor dbExecutor; + + @Inject + public BriarDataFetcherFactory(MessagingManager messagingManager, + @DatabaseExecutor Executor dbExecutor) { + this.messagingManager = messagingManager; + this.dbExecutor = dbExecutor; + } + + BriarDataFetcher createBriarDataFetcher(AttachmentItem model) { + return new BriarDataFetcher(messagingManager, dbExecutor, model); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java index 397910de9..1891abdfe 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarModelLoader.java @@ -19,21 +19,19 @@ import javax.inject.Inject; public final class BriarModelLoader implements ModelLoader { - private final BriarApplication app; - @Inject - BriarDataFetcher dataFetcher; + BriarDataFetcherFactory dataFetcherFactory; public BriarModelLoader(BriarApplication app) { - this.app = app; + app.getApplicationComponent().inject(this); } @Override public LoadData buildLoadData(AttachmentItem model, int width, int height, Options options) { - app.getApplicationComponent().inject(this); ObjectKey key = new ObjectKey(model.getMessageId()); - dataFetcher.setAttachment(model); + BriarDataFetcher dataFetcher = + dataFetcherFactory.createBriarDataFetcher(model); return new LoadData<>(key, dataFetcher); } From 152ac3df43f44460e690ff1984268394c1524da1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 20 Nov 2018 11:48:44 -0200 Subject: [PATCH 6/7] [android] improve bitmap transformation hashKey and DiskCacheKey --- .../conversation/glide/ImageCornerTransformation.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java index fb418d6d7..b41c0cf31 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/ImageCornerTransformation.java @@ -98,14 +98,14 @@ class ImageCornerTransformation extends BitmapTransformation { @Override public int hashCode() { - return ID.hashCode() + smallRadius * 100 + radius * 10 + - (leftCornerSmall ? 9 : 8) + (bottomRound ? 7 : 6); + return ID.hashCode() + (smallRadius << 16) ^ (radius << 2) ^ + (leftCornerSmall ? 2 : 0) ^ (bottomRound ? 1 : 0); } @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { - messageDigest.update((ID + smallRadius + radius + leftCornerSmall + - bottomRound).getBytes(CHARSET)); + messageDigest.update((ID + '|' + smallRadius + '|' + radius + '|' + + leftCornerSmall + '|' + bottomRound).getBytes(CHARSET)); } } From 798bb6d4f7dc645032423b93c4a3261d519f5624 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 23 Nov 2018 11:25:18 -0200 Subject: [PATCH 7/7] [android] scale thumbnails to minimum size, don't upscale to maximum size --- .../android/conversation/AttachmentController.java | 12 ++++++++++-- .../android/conversation/glide/BriarDataFetcher.java | 3 +-- 2 files changed, 11 insertions(+), 4 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 7c4339c3e..a5d6ca146 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 @@ -175,10 +175,18 @@ class AttachmentController { float widthPercentage = maxWidth / (float) width; float heightPercentage = maxHeight / (float) height; float scaleFactor = Math.min(widthPercentage, heightPercentage); + if (scaleFactor > 1) scaleFactor = 1f; int thumbnailWidth = (int) (width * scaleFactor); int thumbnailHeight = (int) (height * scaleFactor); - if (thumbnailWidth < minWidth) thumbnailWidth = minWidth; - if (thumbnailHeight < minHeight) thumbnailHeight = minHeight; + if (thumbnailWidth < minWidth || thumbnailHeight < minHeight) { + widthPercentage = minWidth / (float) width; + heightPercentage = minHeight / (float) height; + scaleFactor = Math.max(widthPercentage, heightPercentage); + thumbnailWidth = (int) (width * scaleFactor); + thumbnailHeight = (int) (height * scaleFactor); + if (thumbnailWidth > maxWidth) thumbnailWidth = maxWidth; + if (thumbnailHeight > maxHeight) thumbnailHeight = maxHeight; + } return new Size(thumbnailWidth, thumbnailHeight); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java index 252e8df57..51d51434d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/glide/BriarDataFetcher.java @@ -21,7 +21,6 @@ import java.util.logging.Logger; import javax.inject.Inject; import static com.bumptech.glide.load.DataSource.LOCAL; -import static java.util.Objects.requireNonNull; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.util.LogUtils.logException; @@ -52,7 +51,7 @@ class BriarDataFetcher implements DataFetcher { @Override public void loadData(Priority priority, DataCallback callback) { - MessageId id = requireNonNull(attachment).getMessageId(); + MessageId id = attachment.getMessageId(); dbExecutor.execute(() -> { if (cancel) return; try {