[android] display image attachments for conversation messages

This commit is contained in:
Torsten Grote
2018-11-05 18:51:52 -03:00
parent fbf8642edb
commit 8a839fb5e4
27 changed files with 1027 additions and 74 deletions

View File

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

View File

@@ -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<MessageId, List<AttachmentItem>> 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<AttachmentItem> attachments) {
attachmentCache.put(messageId, attachments);
}
@Nullable
List<AttachmentItem> get(MessageId messageId) {
return attachmentCache.get(messageId);
}
@DatabaseExecutor
List<Pair<AttachmentHeader, Attachment>> getMessageAttachments(
List<AttachmentHeader> headers) throws DbException {
long start = now();
List<Pair<AttachmentHeader, Attachment>> 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<AttachmentItem> getAttachmentItems(
List<Pair<AttachmentHeader, Attachment>> attachments) {
List<AttachmentItem> items = new ArrayList<>(attachments.size());
for (Pair<AttachmentHeader, Attachment> 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;
}
}
}

View File

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

View File

@@ -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<MessageId, String> 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<AttachmentItem> 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<ConversationMessageItem> 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<Integer, ConversationMessageItem> 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<AttachmentHeader> headers) {
runOnDbThread(() -> {
try {
displayMessageAttachments(messageId,
attachmentController.getMessageAttachments(headers));
} catch (DbException e) {
logException(LOG, WARNING, e);
}
});
}
private void displayMessageAttachments(MessageId m,
List<Pair<AttachmentHeader, Attachment>> attachments) {
runOnUiThreadUnlessDestroyed(() -> {
List<AttachmentItem> items =
attachmentController.getAttachmentItems(attachments);
attachmentController.put(m, items);
Pair<Integer, ConversationMessageItem> 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<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers) {
List<AttachmentItem> attachments = attachmentController.get(m);
if (attachments == null) loadMessageAttachments(m, headers);
return attachments;
}
}

View File

@@ -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<ConversationMessageItem> getMessageItems() {
SparseArray<ConversationMessageItem> messages = new SparseArray<>();
@Nullable
Pair<Integer, ConversationMessageItem> 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;
}
}

View File

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

View File

@@ -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<AttachmentHeader> attachments;
@Nullable
private List<AttachmentItem> attachments;
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h) {
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
@Nullable List<AttachmentItem> attachments) {
super(layoutRes, h);
this.attachments = h.getAttachmentHeaders();
this.attachments = attachments;
}
List<AttachmentHeader> getAttachments() {
@Nullable
List<AttachmentItem> getAttachments() {
return attachments;
}
void setAttachments(List<AttachmentItem> attachments) {
this.attachments = attachments;
}
}

View File

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

View File

@@ -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<Contact> getContact() {
return contact;
}

View File

@@ -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<String> contactName;
ConversationVisitor(Context ctx, TextCache textCache,
LiveData<String> contactName) {
AttachmentCache attachmentCache, LiveData<String> contactName) {
this.ctx = ctx;
this.textCache = textCache;
this.attachmentCache = attachmentCache;
this.contactName = contactName;
}
@Override
public ConversationItem visitPrivateMessageHeader(PrivateMessageHeader h) {
ConversationItem item;
List<AttachmentItem> 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<AttachmentItem> getAttachmentItems(MessageId m,
List<AttachmentHeader> headers);
}
}

View File

@@ -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<InputStream> {
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<? super InputStream> 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<InputStream> getDataClass() {
return InputStream.class;
}
@Override
public DataSource getDataSource() {
return LOCAL;
}
public void setAttachment(AttachmentItem attachment) {
this.attachment = attachment;
}
}

View File

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

View File

@@ -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<AttachmentItem, InputStream> {
private final BriarApplication app;
@Inject
BriarDataFetcher dataFetcher;
public BriarModelLoader(BriarApplication app) {
this.app = app;
}
@Override
public LoadData<InputStream> 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;
}
}

View File

@@ -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<AttachmentItem, InputStream> {
private final BriarApplication app;
public BriarModelLoaderFactory(BriarApplication app) {
this.app = app;
}
@Override
public ModelLoader<AttachmentItem, InputStream> build(
MultiModelLoaderFactory multiFactory) {
return new BriarModelLoader(app);
}
@Override
public void teardown() {
// noop
}
}