mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 02:39:05 +01:00
Merge branch '1438-send-image-attachments' into 'master'
Store attachments and actually attach them to sent messages Closes #1438 See merge request briar/briar!1006
This commit is contained in:
@@ -16,6 +16,7 @@ import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
@@ -90,6 +91,11 @@ class AttachmentController {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link AttachmentItem}s from the passed headers and Attachments.
|
||||
*
|
||||
* Note: This closes the {@link Attachment}'s {@link InputStream}.
|
||||
*/
|
||||
List<AttachmentItem> getAttachmentItems(
|
||||
List<Pair<AttachmentHeader, Attachment>> attachments) {
|
||||
List<AttachmentItem> items = new ArrayList<>(attachments.size());
|
||||
@@ -101,11 +107,15 @@ class AttachmentController {
|
||||
return items;
|
||||
}
|
||||
|
||||
private AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) {
|
||||
/**
|
||||
* Creates an {@link AttachmentItem} from the {@link Attachment}'s
|
||||
* {@link InputStream} which will be closed when this method returns.
|
||||
*/
|
||||
AttachmentItem getAttachmentItem(AttachmentHeader h, Attachment a) {
|
||||
MessageId messageId = h.getMessageId();
|
||||
Size size = new Size();
|
||||
|
||||
InputStream is = a.getStream();
|
||||
InputStream is = new BufferedInputStream(a.getStream());
|
||||
is.mark(Integer.MAX_VALUE);
|
||||
try {
|
||||
// use exif to get size
|
||||
|
||||
@@ -29,7 +29,6 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
@@ -49,7 +48,6 @@ import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
||||
import org.briarproject.bramble.api.settings.Settings;
|
||||
import org.briarproject.bramble.api.settings.SettingsManager;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
|
||||
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
|
||||
@@ -83,7 +81,6 @@ import org.briarproject.briar.api.introduction.IntroductionManager;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessage;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager;
|
||||
@@ -196,8 +193,6 @@ public class ConversationActivity extends BriarActivity
|
||||
ViewModelProvider.Factory viewModelFactory;
|
||||
|
||||
private volatile ContactId contactId;
|
||||
@Nullable
|
||||
private volatile GroupId messagingGroupId;
|
||||
|
||||
private final Observer<String> contactNameObserver = name -> {
|
||||
requireNonNull(name);
|
||||
@@ -245,6 +240,8 @@ public class ConversationActivity extends BriarActivity
|
||||
requireNonNull(deleted);
|
||||
if (deleted) finish();
|
||||
});
|
||||
viewModel.getAddedPrivateMessage()
|
||||
.observe(this, this::onAddedPrivateMessage);
|
||||
|
||||
setTransitionName(toolbarAvatar, getAvatarTransitionName(contactId));
|
||||
setTransitionName(toolbarStatus, getBulbTransitionName(contactId));
|
||||
@@ -600,16 +597,11 @@ public class ConversationActivity extends BriarActivity
|
||||
|
||||
@Override
|
||||
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
|
||||
if (!imageUris.isEmpty()) {
|
||||
Toast.makeText(this, "Not yet implemented.", LENGTH_LONG).show();
|
||||
textInputView.clearText();
|
||||
return;
|
||||
}
|
||||
if (isNullOrEmpty(text)) throw new AssertionError();
|
||||
if (isNullOrEmpty(text) && imageUris.isEmpty())
|
||||
throw new AssertionError();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
|
||||
if (messagingGroupId == null) loadGroupId(text, timestamp);
|
||||
else createMessage(text, timestamp);
|
||||
viewModel.sendMessage(text, imageUris, timestamp);
|
||||
textInputView.clearText();
|
||||
}
|
||||
|
||||
@@ -619,48 +611,10 @@ public class ConversationActivity extends BriarActivity
|
||||
return item == null ? 0 : item.getTime() + 1;
|
||||
}
|
||||
|
||||
private void loadGroupId(String text, long timestamp) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
messagingGroupId =
|
||||
messagingManager.getConversationId(contactId);
|
||||
createMessage(text, timestamp);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private void createMessage(String text, long timestamp) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
try {
|
||||
//noinspection ConstantConditions init in loadGroupId()
|
||||
storeMessage(privateMessageFactory.createPrivateMessage(
|
||||
messagingGroupId, timestamp, text, emptyList()), text);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeMessage(PrivateMessage m, String text) {
|
||||
runOnDbThread(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.addLocalMessage(m);
|
||||
logDuration(LOG, "Storing message", start);
|
||||
Message message = m.getMessage();
|
||||
PrivateMessageHeader h = new PrivateMessageHeader(
|
||||
message.getId(), message.getGroupId(),
|
||||
message.getTimestamp(), true, false, false, false,
|
||||
true, emptyList());
|
||||
textCache.put(message.getId(), text);
|
||||
addConversationItem(h.accept(visitor));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
private void onAddedPrivateMessage(@Nullable PrivateMessageHeader h) {
|
||||
if (h == null) return;
|
||||
addConversationItem(h.accept(visitor));
|
||||
viewModel.onAddedPrivateMessageSeen();
|
||||
}
|
||||
|
||||
private void askToRemoveContact() {
|
||||
|
||||
@@ -5,19 +5,36 @@ import android.arch.lifecycle.AndroidViewModel;
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.Transformations;
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.UiThread;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.Pair;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.bramble.api.db.DatabaseExecutor;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.briar.android.util.UiUtils;
|
||||
import org.briarproject.briar.api.messaging.Attachment;
|
||||
import org.briarproject.briar.api.messaging.AttachmentHeader;
|
||||
import org.briarproject.briar.api.messaging.MessagingManager;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessage;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageFactory;
|
||||
import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
@@ -26,9 +43,11 @@ import javax.inject.Inject;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static java.util.logging.Logger.getLogger;
|
||||
import static org.briarproject.bramble.util.IoUtils.tryToClose;
|
||||
import static org.briarproject.bramble.util.LogUtils.logDuration;
|
||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||
import static org.briarproject.bramble.util.LogUtils.now;
|
||||
import static org.briarproject.briar.android.util.UiUtils.observeForeverOnce;
|
||||
|
||||
@NotNullByDefault
|
||||
public class ConversationViewModel extends AndroidViewModel {
|
||||
@@ -38,7 +57,11 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
|
||||
@DatabaseExecutor
|
||||
private final Executor dbExecutor;
|
||||
@CryptoExecutor
|
||||
private final Executor cryptoExecutor;
|
||||
private final MessagingManager messagingManager;
|
||||
private final ContactManager contactManager;
|
||||
private final PrivateMessageFactory privateMessageFactory;
|
||||
private final AttachmentController attachmentController;
|
||||
|
||||
@Nullable
|
||||
@@ -50,14 +73,24 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
Transformations.map(contact, UiUtils::getContactDisplayName);
|
||||
private final MutableLiveData<Boolean> contactDeleted =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<GroupId> messagingGroupId =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<PrivateMessageHeader> addedHeader =
|
||||
new MutableLiveData<>();
|
||||
|
||||
@Inject
|
||||
ConversationViewModel(Application application,
|
||||
@DatabaseExecutor Executor dbExecutor,
|
||||
ContactManager contactManager, MessagingManager messagingManager) {
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
MessagingManager messagingManager,
|
||||
ContactManager contactManager,
|
||||
PrivateMessageFactory privateMessageFactory) {
|
||||
super(application);
|
||||
this.dbExecutor = dbExecutor;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
this.messagingManager = messagingManager;
|
||||
this.contactManager = contactManager;
|
||||
this.privateMessageFactory = privateMessageFactory;
|
||||
this.attachmentController = new AttachmentController(messagingManager,
|
||||
application.getResources());
|
||||
contactDeleted.setValue(false);
|
||||
@@ -100,6 +133,117 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
void sendMessage(@Nullable String text, List<Uri> uris, long timestamp) {
|
||||
if (messagingGroupId.getValue() == null) loadGroupId();
|
||||
observeForeverOnce(messagingGroupId, groupId -> {
|
||||
if (groupId == null) return;
|
||||
// calls through to creating and storing the message
|
||||
storeAttachments(groupId, text, uris, timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadGroupId() {
|
||||
if (contactId == null) throw new IllegalStateException();
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
messagingGroupId.postValue(
|
||||
messagingManager.getConversationId(contactId));
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeAttachments(GroupId groupId, @Nullable String text,
|
||||
List<Uri> uris, long timestamp) {
|
||||
dbExecutor.execute(() -> {
|
||||
long start = now();
|
||||
List<AttachmentHeader> attachments = new ArrayList<>();
|
||||
List<AttachmentItem> items = new ArrayList<>();
|
||||
for (Uri uri : uris) {
|
||||
Pair<AttachmentHeader, AttachmentItem> pair =
|
||||
createAttachmentHeader(groupId, uri, timestamp);
|
||||
if (pair == null) continue;
|
||||
attachments.add(pair.getFirst());
|
||||
items.add(pair.getSecond());
|
||||
}
|
||||
logDuration(LOG, "Storing attachments", start);
|
||||
createMessage(groupId, text, attachments, items, timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@DatabaseExecutor
|
||||
private Pair<AttachmentHeader, AttachmentItem> createAttachmentHeader(
|
||||
GroupId groupId, Uri uri, long timestamp) {
|
||||
InputStream is = null;
|
||||
try {
|
||||
ContentResolver contentResolver =
|
||||
getApplication().getContentResolver();
|
||||
is = contentResolver.openInputStream(uri);
|
||||
if (is == null) throw new IOException();
|
||||
String contentType = contentResolver.getType(uri);
|
||||
if (contentType == null) throw new IOException("null content type");
|
||||
AttachmentHeader h = messagingManager
|
||||
.addLocalAttachment(groupId, timestamp, contentType, is);
|
||||
is.close();
|
||||
// re-open stream to get AttachmentItem
|
||||
is = contentResolver.openInputStream(uri);
|
||||
if (is == null) throw new IOException();
|
||||
AttachmentItem item = attachmentController
|
||||
.getAttachmentItem(h, new Attachment(is));
|
||||
return new Pair<>(h, item);
|
||||
} catch (DbException | IOException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
return null;
|
||||
} finally {
|
||||
if (is != null) tryToClose(is, LOG, WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
private void createMessage(GroupId groupId, @Nullable String text,
|
||||
List<AttachmentHeader> attachments, List<AttachmentItem> aItems,
|
||||
long timestamp) {
|
||||
cryptoExecutor.execute(() -> {
|
||||
try {
|
||||
// TODO remove when text can be null in the backend
|
||||
String msgText = text == null ? "null" : text;
|
||||
PrivateMessage pm = privateMessageFactory
|
||||
.createPrivateMessage(groupId, timestamp, msgText,
|
||||
attachments);
|
||||
attachmentController.put(pm.getMessage().getId(), aItems);
|
||||
storeMessage(pm, msgText, attachments);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void storeMessage(PrivateMessage m, @Nullable String text,
|
||||
List<AttachmentHeader> attachments) {
|
||||
dbExecutor.execute(() -> {
|
||||
try {
|
||||
long start = now();
|
||||
messagingManager.addLocalMessage(m);
|
||||
logDuration(LOG, "Storing message", start);
|
||||
Message message = m.getMessage();
|
||||
PrivateMessageHeader h = new PrivateMessageHeader(
|
||||
message.getId(), message.getGroupId(),
|
||||
message.getTimestamp(), true, true, false, false,
|
||||
text != null, attachments);
|
||||
// TODO add text to cache when available here
|
||||
addedHeader.postValue(h);
|
||||
} catch (DbException e) {
|
||||
logException(LOG, WARNING, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@UiThread
|
||||
void onAddedPrivateMessageSeen() {
|
||||
addedHeader.setValue(null);
|
||||
}
|
||||
|
||||
AttachmentController getAttachmentController() {
|
||||
return attachmentController;
|
||||
}
|
||||
@@ -120,4 +264,8 @@ public class ConversationViewModel extends AndroidViewModel {
|
||||
return contactDeleted;
|
||||
}
|
||||
|
||||
LiveData<PrivateMessageHeader> getAddedPrivateMessage() {
|
||||
return addedHeader;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ import android.os.PowerManager;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.ColorRes;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.support.annotation.UiThread;
|
||||
import android.support.design.widget.TextInputLayout;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@@ -342,7 +342,7 @@ public class UiUtils {
|
||||
* If the LiveData's value is available, the {@link Observer} will be
|
||||
* called right away.
|
||||
*/
|
||||
@MainThread
|
||||
@UiThread
|
||||
public static <T> void observeOnce(LiveData<T> liveData,
|
||||
LifecycleOwner owner, Observer<T> observer) {
|
||||
liveData.observe(owner, new Observer<T>() {
|
||||
@@ -354,4 +354,22 @@ public class UiUtils {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #observeOnce(LiveData, LifecycleOwner, Observer)},
|
||||
* but without a {@link LifecycleOwner}.
|
||||
*
|
||||
* Warning: Do NOT call from objects that have a lifecycle.
|
||||
*/
|
||||
@UiThread
|
||||
public static <T> void observeForeverOnce(LiveData<T> liveData,
|
||||
Observer<T> observer) {
|
||||
liveData.observeForever(new Observer<T>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable T t) {
|
||||
observer.onChanged(t);
|
||||
liveData.removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface MessagingManager extends ConversationClient {
|
||||
@@ -37,7 +38,7 @@ public interface MessagingManager extends ConversationClient {
|
||||
* Stores a local attachment message.
|
||||
*/
|
||||
AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
|
||||
String contentType, ByteBuffer data) throws DbException;
|
||||
String contentType, InputStream is) throws DbException, IOException;
|
||||
|
||||
/**
|
||||
* Returns the ID of the contact with the given private conversation.
|
||||
|
||||
@@ -32,7 +32,9 @@ import org.briarproject.briar.api.messaging.PrivateMessageHeader;
|
||||
import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent;
|
||||
import org.briarproject.briar.client.ConversationClientImpl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
@@ -42,6 +44,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static org.briarproject.bramble.util.StringUtils.fromHexString;
|
||||
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
|
||||
|
||||
@Immutable
|
||||
@@ -152,8 +155,9 @@ class MessagingManagerImpl extends ConversationClientImpl
|
||||
|
||||
@Override
|
||||
public AttachmentHeader addLocalAttachment(GroupId groupId, long timestamp,
|
||||
String contentType, ByteBuffer data) {
|
||||
String contentType, InputStream is) throws IOException {
|
||||
// TODO add real implementation
|
||||
if (is.available() == 0) throw new IOException();
|
||||
byte[] b = new byte[MessageId.LENGTH];
|
||||
new Random().nextBytes(b);
|
||||
return new AttachmentHeader(new MessageId(b), "image/png");
|
||||
@@ -237,7 +241,11 @@ class MessagingManagerImpl extends ConversationClientImpl
|
||||
@Override
|
||||
public Attachment getAttachment(MessageId m) {
|
||||
// TODO add real implementation
|
||||
throw new IllegalStateException("Not yet implemented");
|
||||
byte[] bytes = fromHexString("89504E470D0A1A0A0000000D49484452" +
|
||||
"000000010000000108060000001F15C4" +
|
||||
"890000000A49444154789C6300010000" +
|
||||
"0500010D0A2DB40000000049454E44AE426082");
|
||||
return new Attachment(new ByteArrayInputStream(bytes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user