mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
Merge branch '1242-display-image-attachments' into 'master'
[android] display image attachments for conversation messages See merge request briar/briar!997
This commit is contained in:
@@ -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,14 @@ 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
|
||||
}
|
||||
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
compileOnly 'javax.annotation:jsr250-api:1.0'
|
||||
|
||||
|
||||
@@ -30,3 +30,6 @@
|
||||
|
||||
# Emoji
|
||||
-keep class com.vanniktech.emoji.**
|
||||
|
||||
# Glide
|
||||
-dontwarn com.bumptech.glide.load.engine.cache.DiskLruCacheWrapper
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
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);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
} finally {
|
||||
try {
|
||||
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);
|
||||
if (scaleFactor > 1) scaleFactor = 1f;
|
||||
int thumbnailWidth = (int) (width * scaleFactor);
|
||||
int thumbnailHeight = (int) (height * scaleFactor);
|
||||
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);
|
||||
}
|
||||
|
||||
private static class Size {
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,42 @@ 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 {
|
||||
List<Pair<AttachmentHeader, Attachment>> attachments =
|
||||
attachmentController.getMessageAttachments(headers);
|
||||
// TODO move getting the items off to the IoExecutor
|
||||
List<AttachmentItem> items =
|
||||
attachmentController.getAttachmentItems(attachments);
|
||||
displayMessageAttachments(messageId, items);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayMessageAttachments(MessageId m,
|
||||
List<AttachmentItem> items) {
|
||||
runOnUiThreadUnlessDestroyed(() -> {
|
||||
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 +830,15 @@ public class ConversationActivity extends BriarActivity
|
||||
if (text == null) loadMessageText(m);
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AttachmentItem> getAttachmentItems(MessageId m,
|
||||
List<AttachmentHeader> headers) {
|
||||
List<AttachmentItem> attachments = attachmentController.get(m);
|
||||
if (attachments == null) {
|
||||
loadMessageAttachments(m, headers);
|
||||
return emptyList();
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.briarproject.briar.android.conversation;
|
||||
import android.support.annotation.LayoutRes;
|
||||
|
||||
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 +13,20 @@ import javax.annotation.concurrent.NotThreadSafe;
|
||||
@NotNullByDefault
|
||||
class ConversationMessageItem extends ConversationItem {
|
||||
|
||||
private final List<AttachmentHeader> attachments;
|
||||
private List<AttachmentItem> attachments;
|
||||
|
||||
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h) {
|
||||
ConversationMessageItem(@LayoutRes int layoutRes, PrivateMessageHeader h,
|
||||
List<AttachmentItem> attachments) {
|
||||
super(layoutRes, h);
|
||||
this.attachments = h.getAttachmentHeaders();
|
||||
this.attachments = attachments;
|
||||
}
|
||||
|
||||
List<AttachmentHeader> getAttachments() {
|
||||
List<AttachmentItem> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
void setAttachments(List<AttachmentItem> attachments) {
|
||||
this.attachments = attachments;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,157 @@
|
||||
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;
|
||||
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.Transformation;
|
||||
|
||||
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 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;
|
||||
|
||||
@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 boolean isRtl;
|
||||
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);
|
||||
|
||||
// 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);
|
||||
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().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) {
|
||||
boolean leftCornerSmall =
|
||||
(isIncoming() && !isRtl) || (!isIncoming() && isRtl);
|
||||
boolean bottomRound = item.getText() == null;
|
||||
Transformation<Bitmap> transformation = new BriarImageTransformation(
|
||||
radiusSmall, radiusBig, leftCornerSmall, bottomRound);
|
||||
|
||||
GlideApp.with(imageView)
|
||||
.load(attachment)
|
||||
.diskCacheStrategy(NONE)
|
||||
.error(errorRes)
|
||||
.transform(transformation)
|
||||
.transition(withCrossFade())
|
||||
.into(imageView)
|
||||
.waitForLayout();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,9 @@ class ConversationVisitor implements
|
||||
@Nullable
|
||||
String getText(MessageId m);
|
||||
}
|
||||
|
||||
interface AttachmentCache {
|
||||
List<AttachmentItem> getAttachmentItems(MessageId m,
|
||||
List<AttachmentHeader> headers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
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.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;
|
||||
private final AttachmentItem attachment;
|
||||
|
||||
@Nullable
|
||||
private volatile InputStream inputStream;
|
||||
private volatile boolean cancel = false;
|
||||
|
||||
@Inject
|
||||
public BriarDataFetcher(MessagingManager messagingManager,
|
||||
@DatabaseExecutor Executor dbExecutor, AttachmentItem attachment) {
|
||||
this.messagingManager = messagingManager;
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.attachment = attachment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(Priority priority,
|
||||
DataCallback<? super InputStream> callback) {
|
||||
MessageId id = attachment.getMessageId();
|
||||
dbExecutor.execute(() -> {
|
||||
if (cancel) return;
|
||||
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() {
|
||||
cancel = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return LOCAL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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) {
|
||||
BriarApplication app =
|
||||
(BriarApplication) context.getApplicationContext();
|
||||
BriarModelLoaderFactory factory = new BriarModelLoaderFactory(app);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Bitmap> {
|
||||
|
||||
public BriarImageTransformation(int smallRadius, int radius,
|
||||
boolean leftCornerSmall, boolean bottomRound) {
|
||||
super(new CenterCrop(), new ImageCornerTransformation(
|
||||
smallRadius, radius, leftCornerSmall, bottomRound));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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> {
|
||||
|
||||
@Inject
|
||||
BriarDataFetcherFactory dataFetcherFactory;
|
||||
|
||||
public BriarModelLoader(BriarApplication app) {
|
||||
app.getApplicationComponent().inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoadData<InputStream> buildLoadData(AttachmentItem model, int width,
|
||||
int height, Options options) {
|
||||
ObjectKey key = new ObjectKey(model.getMessageId());
|
||||
BriarDataFetcher dataFetcher =
|
||||
dataFetcherFactory.createBriarDataFetcher(model);
|
||||
return new LoadData<>(key, dataFetcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(AttachmentItem model) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 << 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));
|
||||
}
|
||||
|
||||
}
|
||||
9
briar-android/src/main/res/drawable/ic_image_broken.xml
Normal file
9
briar-android/src/main/res/drawable/ic_image_broken.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0">
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
|
||||
</vector>
|
||||
@@ -8,10 +8,10 @@
|
||||
android:topLeftRadius="@dimen/message_bubble_radius_top_inner"
|
||||
android:topRightRadius="@dimen/message_bubble_radius_top_outer"/>
|
||||
<padding
|
||||
android:bottom="@dimen/message_bubble_padding_bottom"
|
||||
android:left="@dimen/message_bubble_padding_sides"
|
||||
android:right="@dimen/message_bubble_padding_sides"
|
||||
android:top="@dimen/message_bubble_padding_top"/>
|
||||
android:bottom="@dimen/message_bubble_border"
|
||||
android:left="@dimen/message_bubble_border"
|
||||
android:right="@dimen/message_bubble_border"
|
||||
android:top="@dimen/message_bubble_border"/>
|
||||
<solid
|
||||
android:color="@color/msg_in"/>
|
||||
<stroke
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
android:topLeftRadius="@dimen/message_bubble_radius_top_outer"
|
||||
android:topRightRadius="@dimen/message_bubble_radius_top_inner"/>
|
||||
<padding
|
||||
android:bottom="@dimen/message_bubble_padding_bottom"
|
||||
android:left="@dimen/message_bubble_padding_sides"
|
||||
android:right="@dimen/message_bubble_padding_sides"
|
||||
android:top="@dimen/message_bubble_padding_top"/>
|
||||
android:bottom="@dimen/message_bubble_border"
|
||||
android:left="@dimen/message_bubble_border"
|
||||
android:right="@dimen/message_bubble_border"
|
||||
android:top="@dimen/message_bubble_border"/>
|
||||
<solid
|
||||
android:color="@color/msg_out"/>
|
||||
<stroke
|
||||
|
||||
14
briar-android/src/main/res/drawable/msg_status_bubble.xml
Normal file
14
briar-android/src/main/res/drawable/msg_status_bubble.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:radius="@dimen/message_bubble_radius_big"/>
|
||||
<padding
|
||||
android:bottom="3dp"
|
||||
android:left="7dp"
|
||||
android:right="7dp"
|
||||
android:top="2dp"/>
|
||||
<solid
|
||||
android:color="@color/msg_status_bubble_background"/>
|
||||
</shape>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_margin"
|
||||
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
|
||||
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
|
||||
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
|
||||
android:layout_marginStart="@dimen/message_bubble_margin_tail"
|
||||
android:layout_marginTop="@dimen/message_bubble_margin"
|
||||
android:background="@drawable/msg_in"
|
||||
android:elevation="@dimen/message_bubble_elevation">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="@dimen/message_bubble_image_default"
|
||||
android:layout_height="@dimen/message_bubble_image_default"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/alerts_and_states_error"/>
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
style="@style/TextMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginTop="@dimen/message_bubble_padding_top_inner"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:visibility="gone"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/imageView"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
tools:text="The text of a message which can sometimes be a bit longer as well"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/statusLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_top"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:background="@drawable/msg_status_bubble"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/imageView"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="@style/TextMessage.Timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/layout"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_margin"
|
||||
android:layout_marginEnd="@dimen/message_bubble_margin_non_tail"
|
||||
android:layout_marginLeft="@dimen/message_bubble_margin_tail"
|
||||
android:layout_marginRight="@dimen/message_bubble_margin_non_tail"
|
||||
android:layout_marginStart="@dimen/message_bubble_margin_tail"
|
||||
android:layout_marginTop="@dimen/message_bubble_margin"
|
||||
android:background="@drawable/msg_in"
|
||||
android:elevation="@dimen/message_bubble_elevation">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="@dimen/message_bubble_image_default"
|
||||
android:layout_height="@dimen/message_bubble_image_default"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/alerts_and_states_error"/>
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
style="@style/TextMessage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginTop="@dimen/message_bubble_padding_top_inner"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
|
||||
app:layout_constraintEnd_toEndOf="@+id/imageView"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
tools:text="The text of a message which can sometimes be a bit longer as well"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/statusLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="@style/TextMessage.Timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
@@ -15,29 +15,55 @@
|
||||
android:background="@drawable/msg_in"
|
||||
android:elevation="@dimen/message_bubble_elevation">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="@dimen/message_bubble_image_default"
|
||||
android:layout_height="@dimen/message_bubble_image_default"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
style="@style/TextMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginTop="@dimen/message_bubble_padding_top_inner"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/time"
|
||||
app:layout_constraintEnd_toEndOf="@+id/time"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Short message"/>
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
tools:text="The text of a message which can sometimes be a bit longer as well"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="@style/TextMessage.Timestamp"
|
||||
<LinearLayout
|
||||
android:id="@+id/statusLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
app:layout_constraintTop_toBottomOf="@+id/text">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="@style/TextMessage.Timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
@@ -21,39 +21,68 @@
|
||||
android:background="@drawable/msg_out"
|
||||
android:elevation="@dimen/message_bubble_elevation">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="@dimen/message_bubble_image_default"
|
||||
android:layout_height="@dimen/message_bubble_image_default"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/alerts_and_states_error"/>
|
||||
|
||||
<com.vanniktech.emoji.EmojiTextView
|
||||
android:id="@+id/text"
|
||||
style="@style/TextMessage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginTop="@dimen/message_bubble_padding_top_inner"
|
||||
android:textColor="@color/briar_text_primary_inverse"
|
||||
app:layout_constraintBottom_toTopOf="@+id/time"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/statusLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||
tools:text="This is a long long long message that spans over several lines.\n\nIt ends here."/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="@style/TextMessage.Timestamp"
|
||||
<LinearLayout
|
||||
android:id="@+id/statusLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/message_bubble_timestamp_margin"
|
||||
android:textColor="@color/private_message_date_inverse"
|
||||
android:layout_marginBottom="@dimen/message_bubble_padding_bottom_inner"
|
||||
android:layout_marginLeft="@dimen/message_bubble_padding_sides_inner"
|
||||
android:layout_marginRight="@dimen/message_bubble_padding_sides_inner"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/text"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/margin_medium"
|
||||
android:layout_marginStart="@dimen/margin_medium"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/time"
|
||||
app:layout_constraintStart_toEndOf="@+id/time"
|
||||
app:layout_constraintTop_toTopOf="@+id/time"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/message_delivered"/>
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
style="@style/TextMessage.Timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:textColor="@color/private_message_date_inverse"
|
||||
tools:text="Dec 24, 13:37"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/message_delivered"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
<color name="msg_stroke_light">#cbcbcb</color>
|
||||
<color name="msg_stroke_dark">#333333</color>
|
||||
<color name="msg_stroke">@color/msg_stroke_light</color>
|
||||
<color name="msg_status_bubble_background">#66000000</color>
|
||||
|
||||
<!-- text colors -->
|
||||
<color name="briar_text_link">@color/briar_blue_light</color>
|
||||
|
||||
@@ -43,9 +43,18 @@
|
||||
<dimen name="message_bubble_radius_top_inner">@dimen/message_bubble_radius_small</dimen>
|
||||
<dimen name="message_bubble_radius_top_outer">@dimen/message_bubble_radius_big</dimen>
|
||||
<dimen name="message_bubble_margin">6dp</dimen>
|
||||
<dimen name="message_bubble_image_default">210dp</dimen>
|
||||
<dimen name="message_bubble_image_min_width">150dp</dimen>
|
||||
<dimen name="message_bubble_image_max_width">240dp</dimen>
|
||||
<dimen name="message_bubble_image_min_height">100dp</dimen>
|
||||
<dimen name="message_bubble_image_max_height">320dp</dimen>
|
||||
<dimen name="message_bubble_border">2dp</dimen>
|
||||
<dimen name="message_bubble_padding_sides">12dp</dimen>
|
||||
<dimen name="message_bubble_padding_sides_inner">10dp</dimen>
|
||||
<dimen name="message_bubble_padding_top">6dp</dimen>
|
||||
<dimen name="message_bubble_padding_top_inner">4dp</dimen>
|
||||
<dimen name="message_bubble_padding_bottom">4dp</dimen>
|
||||
<dimen name="message_bubble_padding_bottom_inner">2dp</dimen>
|
||||
<dimen name="message_bubble_timestamp_margin">4dp</dimen>
|
||||
<dimen name="message_bubble_elevation">2dp</dimen>
|
||||
<dimen name="message_bubble_margin_tail">8dp</dimen>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user